├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.json ├── docs └── .nojekyll ├── examples ├── javascript │ ├── index.html │ └── main.js └── typescript │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.html │ └── index.ts │ ├── tsconfig.json │ └── webpack.config.js ├── images ├── ascii-screenshot.png └── emoji-dungeon-optimized.gif ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── create-2d-array.ts ├── debug.ts ├── dungeon.test.ts ├── dungeon.ts ├── index.ts ├── math.test.ts ├── math.ts ├── point.ts ├── random.test.ts ├── random.ts ├── room.ts └── tiles.ts ├── tsconfig.json ├── typedoc.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "prettier"], 3 | plugins: ["prettier"], 4 | rules: { 5 | "no-var": 1, 6 | "brace-style": ["warn", "1tbs"], 7 | "no-unused-vars": ["error", { args: "after-used" }], 8 | indent: ["warn", 2, { SwitchCase: 1 }], 9 | "max-len": ["warn", 100, { ignoreUrls: true, ignoreTemplateLiterals: true }], 10 | "no-console": "off" 11 | }, 12 | env: { 13 | browser: true, 14 | commonjs: true, 15 | es6: true 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 6, 19 | sourceType: "module", 20 | ecmaFeatures: { 21 | modules: true 22 | } 23 | }, 24 | overrides: [ 25 | { 26 | files: ["**/*test.js"], 27 | env: { 28 | jest: true 29 | } 30 | } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | /docs/** 4 | !/docs/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Nick Gravelyn. 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dungeon 2 | 3 | ![](./images/emoji-dungeon-optimized.gif) 4 | 5 | ![](./images/ascii-screenshot.png) 6 | 7 | ## Installation 8 | 9 | ### As a Script 10 | 11 | Grab the [minified js](https://github.com/mikewesthad/dungeon/releases/latest/download/dungeon.min.js) file & optional source [map](https://github.com/mikewesthad/dungeon/releases/latest/download/dungeon.min.js.map) (or the [unminified js](https://github.com/mikewesthad/dungeon/releases/latest/download/dungeon.js) file & optional source [map](https://github.com/mikewesthad/dungeon/releases/latest/download/dungeon.js.map)). 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | Or use the jsdelivr CDN: 18 | 19 | ```html 20 | 21 | ``` 22 | 23 | This will give you a `Dungeon` global variable. 24 | 25 | ### As a Module 26 | 27 | ``` 28 | npm i @mikewesthad/dungeon 29 | ``` 30 | 31 | ```js 32 | import Dungeon from "@mikewesthad/dungeon"; 33 | ``` 34 | 35 | ## Usage 36 | 37 | See the examples folder for [a JavaScript example](./examples/javascript/) and [a TypeScript example](./examples/typescript/). 38 | 39 | ```js 40 | const dungeon = new Dungeon({ 41 | width: 50, 42 | height: 50, 43 | doorPadding: 1, // Experimental, minimum number of tiles between a door and a room corner (>= 1) 44 | randomSeed: "hello", // Leave undefined if you don't want to control the seed 45 | rooms: { 46 | width: { 47 | min: 5, 48 | max: 10, 49 | onlyOdd: true // Or onlyEven: true 50 | }, 51 | height: { 52 | min: 8, 53 | max: 20, 54 | onlyOdd: true // Or onlyEven: true 55 | }, 56 | maxArea: 150, 57 | maxRooms: 50 58 | } 59 | }); 60 | 61 | // Make sure you resize your console (see guide that gets printed out in the console) 62 | dungeon.drawToConsole({ 63 | empty: " ", 64 | emptyColor: "rgb(0, 0, 0)", 65 | wall: "#", 66 | wallColor: "rgb(255, 0, 0)", 67 | floor: "0", 68 | floorColor: "rgb(210, 210, 210)", 69 | door: "x", 70 | doorColor: "rgb(0, 0, 255)", 71 | fontSize: "8px" 72 | }); 73 | 74 | // Helper method for debugging by dumping the map into an HTML fragment (
)
 75 | const html = dungeon.drawToHtml({
 76 |   empty: " ",
 77 |   emptyAttributes: { class: "dungeon__empty", style: "color: rgb(0, 0, 0)" },
 78 |   wall: "#",
 79 |   wallAttributes: { class: "dungeon__wall", style: "color: rgb(255, 0, 0)" },
 80 |   floor: "0",
 81 |   floorAttributes: { class: "dungeon__floor", style: "color: rgb(210, 210, 210)" },
 82 |   door: "x",
 83 |   doorAttributes: { class: "dungeon__door", style: "color: rgb(0, 0, 255)" },
 84 |   containerAttributes: { class: "dungeon", style: "font-size: 15px" }
 85 | });
 86 | document.body.appendChild(html);
 87 | 
 88 | dungeon.rooms; // Array of Room instances
 89 | dungeon.tiles; // 2D array of tile IDs - see Tile.js for types
 90 | 
 91 | // Get a 2D array of tiles where each tile type is remapped to a custom value. Useful if you are
 92 | // using this in a tilemap, or if you want to map the tiles to something else, e.g. this is used
 93 | // internally to convert a dungeon to an HTML string.
 94 | var mappedTiles = dungeon.getMappedTiles({
 95 |   empty: 0,
 96 |   floor: 1,
 97 |   door: 2,
 98 |   wall: 3
 99 | });
100 | ```
101 | 
102 | ## Docs
103 | 
104 | See the web docs for the API [here](https://mikewesthad.github.com/dungeon/). The most useful page is the [Dungeon class](https://mikewesthad.github.com/dungeon/classes/_dungeon_.dungeon.html)
105 | 
106 | ## Changelog
107 | 
108 | - 2.0.0
109 |   - Rewrite in Typescript. API changes requiring more of the DungeonConfig to be specified in the constructor.
110 | 
111 | ## Contributors
112 | 
113 | - [@mktcode](https://github.com/mktcode) - fix filename case. 
114 | - [@gobah](https://github.com/gobah) - PR for getting the library working on node.
115 | - And, of course, [@nickgravelyn](https://github.com/nickgravelyn/dungeon) who wrote the library that this fork is based on.
116 | 


--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "presets": [
 3 |     [
 4 |       "@babel/preset-env",
 5 |       {
 6 |         "useBuiltIns": "usage",
 7 |         "targets": "> 0.25%, not dead",
 8 |         "corejs": 3
 9 |       }
10 |     ]
11 |   ]
12 | }


--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikewesthad/dungeon/0e125f46ff33ea40bb18576d747711afcd168c90/docs/.nojekyll


--------------------------------------------------------------------------------
/examples/javascript/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     
 7 |     Dungeon Example
 8 | 
 9 |     
30 |   
31 | 
32 |   
33 |     
34 |     
35 |   
36 | 
37 | 


--------------------------------------------------------------------------------
/examples/javascript/main.js:
--------------------------------------------------------------------------------
 1 | const dungeon = new Dungeon({
 2 |   width: 100,
 3 |   height: 30,
 4 |   doorPadding: 1, // Experimental, minimum number of tiles between a door and a room corner (>= 1)
 5 |   randomSeed: "hello", // Leave undefined if you don't want to control the seed
 6 |   rooms: {
 7 |     width: {
 8 |       min: 5,
 9 |       max: 15,
10 |       onlyOdd: true // or onlyEven: true
11 |     },
12 |     height: {
13 |       min: 5,
14 |       max: 15,
15 |       onlyOdd: true // or onlyEven: true
16 |     },
17 |     maxArea: 150,
18 |     maxRooms: 50
19 |   }
20 | });
21 | 
22 | // Make sure you resize your console (see guide that gets printed out in the console)
23 | dungeon.drawToConsole({
24 |   empty: " ",
25 |   emptyColor: "rgb(0, 0, 0)",
26 |   wall: "#",
27 |   wallColor: "rgb(255, 0, 0)",
28 |   floor: "0",
29 |   floorColor: "rgb(210, 210, 210)",
30 |   door: "x",
31 |   doorColor: "rgb(0, 0, 255)",
32 |   fontSize: "8px"
33 | });
34 | 
35 | // Helper method for debugging by dumping the map into an HTML fragment (
) 36 | const html = dungeon.drawToHtml({ 37 | empty: " ", 38 | emptyAttributes: { class: "dungeon__empty" }, 39 | wall: "#", 40 | wallAttributes: { class: "dungeon__wall", style: "color: #950fe2" }, 41 | floor: "x", 42 | floorAttributes: { class: "dungeon__floor", style: "color: #d2e9ef" }, 43 | door: "*", 44 | doorAttributes: { class: "dungeon__door", style: "font-weight: bold; color: #f900c3" }, 45 | containerAttributes: { class: "dungeon", style: "font-size: 15px" } 46 | }); 47 | document.body.appendChild(html); 48 | 49 | dungeon.rooms; // Array of Room instances 50 | dungeon.tiles; // 2D array of tile IDs - see Tile.js for types 51 | 52 | // Get a 2D array of tiles where each tile type is remapped to a custom value. Useful if you are 53 | // using this in a tilemap, or if you want to map the tiles to something else, e.g. this is used 54 | // internally to convert a dungeon to an HTML string. 55 | const mappedTiles = dungeon.getMappedTiles({ 56 | empty: 0, 57 | floor: 1, 58 | door: 2, 59 | wall: 3 60 | }); 61 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dunegon-typescript-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --mode development --open" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "html-webpack-plugin": "^4.3.0", 13 | "ts-loader": "^7.0.5", 14 | "typescript": "^3.9.5", 15 | "webpack": "^4.43.0", 16 | "webpack-cli": "^3.3.11", 17 | "webpack-dev-server": "^3.11.0" 18 | } 19 | } -------------------------------------------------------------------------------- /examples/typescript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dungeon Example 8 | 9 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import Dungeon from "../../../dist/dungeon"; 2 | 3 | const dungeon = new Dungeon({ 4 | width: 100, 5 | height: 30, 6 | doorPadding: 1, // Experimental, minimum number of tiles between a door and a room corner (>= 1) 7 | randomSeed: "hello", // Leave undefined if you don't want to control the seed 8 | rooms: { 9 | width: { 10 | min: 5, 11 | max: 15, 12 | onlyOdd: true // or onlyEven: true 13 | }, 14 | height: { 15 | min: 5, 16 | max: 15, 17 | onlyOdd: true // or onlyEven: true 18 | }, 19 | maxArea: 150, 20 | maxRooms: 50 21 | } 22 | }); 23 | 24 | // Make sure you resize your console (see guide that gets printed out in the console) 25 | dungeon.drawToConsole({ 26 | empty: " ", 27 | emptyColor: "rgb(0, 0, 0)", 28 | wall: "#", 29 | wallColor: "rgb(255, 0, 0)", 30 | floor: "0", 31 | floorColor: "rgb(210, 210, 210)", 32 | door: "x", 33 | doorColor: "rgb(0, 0, 255)", 34 | fontSize: "8px" 35 | }); 36 | 37 | // Helper method for debugging by dumping the map into an HTML fragment (
) 38 | const html = dungeon.drawToHtml({ 39 | empty: " ", 40 | emptyAttributes: { class: "dungeon__empty" }, 41 | wall: "#", 42 | wallAttributes: { class: "dungeon__wall", style: "color: #950fe2" }, 43 | floor: "x", 44 | floorAttributes: { class: "dungeon__floor", style: "color: #d2e9ef" }, 45 | door: "*", 46 | doorAttributes: { class: "dungeon__door", style: "font-weight: bold; color: #f900c3" }, 47 | containerAttributes: { class: "dungeon", style: "font-size: 15px" } 48 | }); 49 | document.body.appendChild(html); 50 | 51 | dungeon.rooms; // Array of Room instances 52 | dungeon.tiles; // 2D array of tile IDs - see Tile.js for types 53 | 54 | // Get a 2D array of tiles where each tile type is remapped to a custom value. Useful if you are 55 | // using this in a tilemap, or if you want to map the tiles to something else, e.g. this is used 56 | // internally to convert a dungeon to an HTML string. 57 | const mappedTiles = dungeon.getMappedTiles({ 58 | empty: 0, 59 | floor: 1, 60 | door: 2, 61 | wall: 3 62 | }); 63 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "es6", 5 | "sourceMap": true, 6 | "allowSyntheticDefaultImports": true, 7 | "lib": ["dom", "esnext"], 8 | "outDir": "dist/", 9 | "moduleResolution": "node" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: "./index.ts", 6 | context: path.resolve(__dirname, "src"), 7 | module: { 8 | rules: [{ test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ }] 9 | }, 10 | resolve: { 11 | extensions: [".tsx", ".ts", ".js"] 12 | }, 13 | plugins: [new HtmlWebpackPlugin({ template: "./index.html" })] 14 | }; 15 | -------------------------------------------------------------------------------- /images/ascii-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/dungeon/0e125f46ff33ea40bb18576d747711afcd168c90/images/ascii-screenshot.png -------------------------------------------------------------------------------- /images/emoji-dungeon-optimized.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/dungeon/0e125f46ff33ea40bb18576d747711afcd168c90/images/emoji-dungeon-optimized.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node" 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mikewesthad/dungeon", 3 | "version": "2.0.1", 4 | "description": "A simple 2D dungeon generator", 5 | "types": "dist/index.d.ts", 6 | "dependencies": { 7 | "@types/seedrandom": "^2.4.28", 8 | "core-js": "^3.6.4", 9 | "seedrandom": "^3.0.5" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.8.4", 13 | "@babel/preset-env": "^7.8.4", 14 | "@types/jest": "^25.1.3", 15 | "babel-jest": "^25.1.0", 16 | "eslint": "^6.8.0", 17 | "eslint-config-prettier": "^6.10.0", 18 | "eslint-plugin-prettier": "^3.1.2", 19 | "gh-pages": "^2.2.0", 20 | "jest": "^25.1.0", 21 | "prettier": "^1.19.1", 22 | "release-it": "^12.6.1", 23 | "ts-jest": "^25.2.1", 24 | "ts-loader": "^6.2.1", 25 | "typedoc": "^0.16.10", 26 | "typescript": "^3.8.2", 27 | "uglifyjs-webpack-plugin": "^2.2.0", 28 | "webpack": "^4.41.6", 29 | "webpack-cli": "^3.3.11" 30 | }, 31 | "scripts": { 32 | "build": "webpack --mode production", 33 | "dev": "webpack --mode development --watch", 34 | "doc": "typedoc --options typedoc.json && cp -r images docs/images", 35 | "deploy:doc": "gh-pages -d docs -b gh-pages --dotfiles", 36 | "prepublishOnly": "npm run build", 37 | "release": "release-it", 38 | "test": "jest" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/mikewesthad/dungeon.git" 43 | }, 44 | "prettier": { 45 | "printWidth": 100 46 | }, 47 | "author": "", 48 | "license": "ISC", 49 | "bugs": { 50 | "url": "https://github.com/mikewesthad/dungeon/issues" 51 | }, 52 | "homepage": "https://github.com/mikewesthad/dungeon#readme", 53 | "keywords": [ 54 | "dungeon generator", 55 | "2D" 56 | ], 57 | "main": "dist/dungeon.min.js", 58 | "files": [ 59 | "dist/**/*", 60 | "src/**/*" 61 | ] 62 | } -------------------------------------------------------------------------------- /src/create-2d-array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a 2D array of width x height, filled with the given value. 3 | */ 4 | export default function create2DArray(width: number, height: number, value?: T): T[][] { 5 | return [...Array(height)].map(() => Array(width).fill(value)); 6 | } 7 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | import TILES from "./tiles"; 2 | import Dungeon from "./dungeon"; 3 | 4 | type AttributesObject = { [key: string]: string }; 5 | 6 | const attributesToHtmlString = (attrObj: AttributesObject) => 7 | Object.entries(attrObj) 8 | .map(([key, val]) => `${key}="${val}"`) 9 | .join(" "); 10 | 11 | // Debug by dumping a table to the console where each element in the map is the number of rooms in 12 | // that location 13 | export function debugRoomGrid(dungeon: Dungeon) { 14 | const table = dungeon.roomGrid.map(row => row.map(elem => `${elem.length}`.padStart(2))); 15 | console.log(table.map(row => row.join(" ")).join("\n")); 16 | } 17 | 18 | // Debug by dumping the dungeon into an HTML fragment that can be inserted into HTML. The structure 19 | // is: 20 | //
 21 | //    
22 | // 23 | // 24 | // 25 | // 26 | // 27 | // 28 | // 29 | //
# # # #
# #
# /
# #
# # # #
30 | //
31 | 32 | export type DebugHtmlConfig = { 33 | empty?: string; 34 | emptyAttributes?: AttributesObject; 35 | wall?: string; 36 | wallAttributes?: AttributesObject; 37 | floor?: string; 38 | floorAttributes?: AttributesObject; 39 | door?: string; 40 | doorAttributes?: AttributesObject; 41 | containerAttributes?: AttributesObject; 42 | }; 43 | type DebugHtmlConfigRequired = Required; 44 | 45 | export function debugHtmlStringMap(dungeon: Dungeon, config: DebugHtmlConfig = {}) { 46 | const c: DebugHtmlConfigRequired = Object.assign( 47 | {}, 48 | { 49 | empty: " ", 50 | emptyAttributes: { class: "dungeon__empty" }, 51 | wall: "#", 52 | wallAttributes: { class: "dungeon__wall" }, 53 | floor: "_", 54 | floorAttributes: { class: "dungeon__wall" }, 55 | door: ".", 56 | doorAttributes: { class: "dungeon__door" }, 57 | containerAttributes: { class: "dungeon" } 58 | }, 59 | config 60 | ); 61 | 62 | const tiles = dungeon.getMappedTiles({ 63 | empty: `${c.empty}`, 64 | floor: `${c.floor}`, 65 | door: `${c.door}`, 66 | wall: `${c.wall}` 67 | }); 68 | 69 | const tilesHtml = tiles.map(row => `${row.join("")}`).join(""); 70 | const htmlString = `
${tilesHtml}
`; 73 | 74 | return htmlString; 75 | } 76 | 77 | export function debugHtmlMap(dungeon: Dungeon, config: DebugHtmlConfig = {}) { 78 | const htmlString = debugHtmlStringMap(dungeon, config); 79 | const htmlFragment = document.createRange().createContextualFragment(htmlString); 80 | return htmlFragment; 81 | } 82 | 83 | // Debug by returning a colored(!) table string where each tile in the map is represented with an 84 | // ASCII string 85 | export type DebugConsoleConfig = { 86 | empty?: string; 87 | emptyColor?: string; 88 | wall?: string; 89 | wallColor?: string; 90 | floor?: string; 91 | floorColor?: string; 92 | door?: string; 93 | doorColor?: string; 94 | fontSize?: string; 95 | }; 96 | type DebugConsoleConfigRequired = Required; 97 | export function debugMap(dungeon: Dungeon, config: DebugConsoleConfig = {}) { 98 | const c: DebugConsoleConfigRequired = Object.assign( 99 | {}, 100 | { 101 | empty: " ", 102 | emptyColor: "rgb(0, 0, 0)", 103 | wall: "#", 104 | wallColor: "rgb(255, 0, 0)", 105 | floor: "_", 106 | floorColor: "rgb(210, 210, 210)", 107 | door: ".", 108 | doorColor: "rgb(0, 0, 255)", 109 | fontSize: "15px" 110 | }, 111 | config 112 | ); 113 | 114 | let string = ""; 115 | let styles = []; 116 | 117 | // First line in the browser console window has console line mapping (e.g. "dungeon.js:724") which 118 | // throws off the table. Kill two birds by displaying a guide on the first two lines. 119 | string += `Dungeon: the console window should be big enough to see all of the guide on the next line:\n`; 120 | string += `%c|${"=".repeat(dungeon.width * 2 - 2)}|\n\n`; 121 | styles.push(`font-size: ${c.fontSize}`); 122 | 123 | for (let y = 0; y < dungeon.height; y += 1) { 124 | for (let x = 0; x < dungeon.width; x += 1) { 125 | const tile = dungeon.tiles[y][x]; 126 | if (tile === TILES.EMPTY) { 127 | string += `%c${c.empty}`; 128 | styles.push(`color: ${c.emptyColor}; font-size: ${c.fontSize}`); 129 | } else if (tile === TILES.WALL) { 130 | string += `%c${c.wall}`; 131 | styles.push(`color: ${c.wallColor}; font-size: ${c.fontSize}`); 132 | } else if (tile === TILES.FLOOR) { 133 | string += `%c${c.floor}`; 134 | styles.push(`color: ${c.floorColor}; font-size: ${c.fontSize}`); 135 | } else if (tile === TILES.DOOR) { 136 | string += `%c${c.door}`; 137 | styles.push(`color: ${c.doorColor}; font-size: ${c.fontSize}`); 138 | } 139 | string += " "; 140 | } 141 | string += "\n"; 142 | } 143 | console.log(string, ...styles); 144 | } 145 | -------------------------------------------------------------------------------- /src/dungeon.test.ts: -------------------------------------------------------------------------------- 1 | import Dungeon from "./dungeon"; 2 | import TILES from "./tiles"; 3 | 4 | describe("Dungeon constructor", () => { 5 | test("should apply default config values for optional properties not specified", () => { 6 | const config = { 7 | width: 20, 8 | height: 10, 9 | rooms: { 10 | width: { min: 4, max: 9 }, 11 | height: { min: 3, max: 4 } 12 | } 13 | }; 14 | const d = new Dungeon(config); 15 | const maxArea = config.rooms.width.max * config.rooms.height.max; 16 | const minArea = config.rooms.width.min * config.rooms.height.min; 17 | const maxRooms = Math.floor((config.width * config.height) / minArea); 18 | expect(d.getConfig()).toMatchObject({ 19 | width: config.width, 20 | height: config.height, 21 | doorPadding: 1, 22 | randomSeed: undefined, 23 | rooms: { 24 | width: { 25 | min: config.rooms.width.min, 26 | max: config.rooms.width.max, 27 | onlyOdd: false, 28 | onlyEven: false 29 | }, 30 | height: { 31 | min: config.rooms.height.min, 32 | max: config.rooms.height.max, 33 | onlyOdd: false, 34 | onlyEven: false 35 | }, 36 | maxArea, 37 | maxRooms 38 | } 39 | }); 40 | }); 41 | 42 | test("should throw error if max room size is bigger than dungeon size", () => { 43 | expect(() => { 44 | const config = { 45 | width: 20, 46 | height: 10, 47 | rooms: { 48 | width: { min: 4, max: 20 }, 49 | height: { min: 3, max: 20 } 50 | } 51 | }; 52 | const d = new Dungeon(config); 53 | }).toThrowError(/exceed dungeon/); 54 | }); 55 | 56 | test("should throw error if maxArea too small", () => { 57 | expect(() => { 58 | const config = { 59 | width: 20, 60 | height: 10, 61 | rooms: { 62 | width: { min: 3, max: 5 }, 63 | height: { min: 3, max: 5 }, 64 | maxArea: 2 65 | } 66 | }; 67 | const d = new Dungeon(config); 68 | }).toThrowError(/exceeds the given maxArea/); 69 | }); 70 | 71 | test("should not allow rooms smaller than 3 x 3", () => { 72 | expect(() => { 73 | const config = { 74 | width: 20, 75 | height: 10, 76 | rooms: { 77 | width: { min: 2, max: 3 }, 78 | height: { min: 0, max: 3 } 79 | } 80 | }; 81 | const d = new Dungeon(config); 82 | }).toThrowError(/width and height must be >= 3/); 83 | }); 84 | 85 | test("should not allow room size min less than max", () => { 86 | expect(() => { 87 | const config = { 88 | width: 20, 89 | height: 10, 90 | rooms: { 91 | width: { min: 5, max: 3 }, 92 | height: { min: 5, max: 3 } 93 | } 94 | }; 95 | const d = new Dungeon(config); 96 | }).toThrowError(/width and height max must be >= min/); 97 | }); 98 | 99 | test("should not allow room min/max that cannot match even parity", () => { 100 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 101 | expect(() => { 102 | const config = { 103 | width: 20, 104 | height: 10, 105 | rooms: { 106 | width: { min: 3, max: 3, onlyEven: true }, 107 | height: { min: 3, max: 5 } 108 | } 109 | }; 110 | const d = new Dungeon(config); 111 | }).toThrowError(/width and height max must be >= min/); 112 | spy.mockRestore(); 113 | }); 114 | 115 | test("should not allow room min/max that cannot match odd parity", () => { 116 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 117 | expect(() => { 118 | const config = { 119 | width: 20, 120 | height: 10, 121 | rooms: { 122 | width: { min: 4, max: 4, onlyOdd: true }, 123 | height: { min: 3, max: 5 } 124 | } 125 | }; 126 | const d = new Dungeon(config); 127 | }).toThrowError(/width and height max must be >= min/); 128 | spy.mockRestore(); 129 | }); 130 | }); 131 | 132 | describe("A dungeon", () => { 133 | test("should not have doors in the corners of rooms", () => { 134 | const d = new Dungeon({ 135 | width: 500, 136 | height: 500, 137 | rooms: { 138 | width: { min: 3, max: 11 }, 139 | height: { min: 3, max: 11 }, 140 | maxRooms: 1000 141 | } 142 | }); 143 | for (const room of d.rooms) { 144 | expect(room.getTileAt(0, 0)).not.toBe(TILES.DOOR); 145 | expect(room.getTileAt(0, room.height - 1)).not.toBe(TILES.DOOR); 146 | expect(room.getTileAt(room.width - 1, 0)).not.toBe(TILES.DOOR); 147 | expect(room.getTileAt(room.width - 1, room.height - 1)).not.toBe(TILES.DOOR); 148 | } 149 | }); 150 | 151 | test("should only have doors and walls along the edges", () => { 152 | const d = new Dungeon({ 153 | width: 500, 154 | height: 500, 155 | rooms: { 156 | width: { min: 3, max: 11 }, 157 | height: { min: 3, max: 11 }, 158 | maxRooms: 1000 159 | } 160 | }); 161 | const acceptableEdgeTiles = new RegExp(`${TILES.DOOR}|${TILES.WALL}`); 162 | for (const room of d.rooms) { 163 | for (let y = 0; y < room.height; y++) { 164 | expect(room.getTileAt(0, y).toString()).toMatch(acceptableEdgeTiles); 165 | expect(room.getTileAt(room.width - 1, y).toString()).toMatch(acceptableEdgeTiles); 166 | } 167 | for (let x = 0; x < room.width; x++) { 168 | expect(room.getTileAt(x, 0).toString()).toMatch(acceptableEdgeTiles); 169 | expect(room.getTileAt(x, room.height - 1).toString()).toMatch(acceptableEdgeTiles); 170 | } 171 | } 172 | }); 173 | 174 | test("should generate rooms that match the dimensions", () => { 175 | const config = { 176 | width: 500, 177 | height: 500, 178 | rooms: { 179 | width: { min: 4, max: 7 }, 180 | height: { min: 3, max: 9 }, 181 | maxRooms: 1000 182 | } 183 | }; 184 | const d = new Dungeon(config); 185 | for (const room of d.rooms) { 186 | expect(room.width).toBeGreaterThanOrEqual(config.rooms.width.min); 187 | expect(room.width).toBeLessThanOrEqual(config.rooms.width.max); 188 | expect(room.height).toBeGreaterThanOrEqual(config.rooms.height.min); 189 | expect(room.height).toBeLessThanOrEqual(config.rooms.height.max); 190 | } 191 | }); 192 | 193 | test("should generate only odd dimension rooms when requested", () => { 194 | const config = { 195 | width: 500, 196 | height: 500, 197 | rooms: { 198 | width: { min: 5, max: 7, onlyOdd: true }, 199 | height: { min: 3, max: 9, onlyOdd: true }, 200 | maxRooms: 1000 201 | } 202 | }; 203 | const d = new Dungeon(config); 204 | for (const room of d.rooms) { 205 | expect(room.width % 2 == 0).toBe(false); 206 | expect(room.height % 2 == 0).toBe(false); 207 | } 208 | }); 209 | 210 | test("should generate only even dimension rooms when requested", () => { 211 | const config = { 212 | width: 500, 213 | height: 500, 214 | rooms: { 215 | width: { min: 4, max: 6, onlyEven: true }, 216 | height: { min: 4, max: 8, onlyEven: true }, 217 | maxRooms: 1000 218 | } 219 | }; 220 | const d = new Dungeon(config); 221 | for (const room of d.rooms) { 222 | expect(room.width % 2 == 0).toBe(true); 223 | expect(room.height % 2 == 0).toBe(true); 224 | } 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /src/dungeon.ts: -------------------------------------------------------------------------------- 1 | import Random from "./random"; 2 | import Room from "./room"; 3 | import TILES, { DebugTileMap } from "./tiles"; 4 | import { debugMap, debugHtmlMap, DebugHtmlConfig, DebugConsoleConfig } from "./debug"; 5 | import create2DArray from "./create-2d-array"; 6 | import Point from "./point"; 7 | import { isOdd, isEven } from "./math"; 8 | 9 | type DimensionConfig = { min: number; max: number; onlyOdd?: boolean; onlyEven?: boolean }; 10 | type DimensionConfigRequired = Required; 11 | type RoomConfig = { 12 | width: DimensionConfig; 13 | height: DimensionConfig; 14 | maxArea?: number; 15 | maxRooms?: number; 16 | }; 17 | export type DungeonConfig = { 18 | width: number; 19 | height: number; 20 | randomSeed?: string; 21 | doorPadding?: number; 22 | rooms: RoomConfig; 23 | }; 24 | 25 | export default class Dungeon { 26 | public height: number; 27 | public width: number; 28 | public tiles: TILES[][]; 29 | public rooms: Room[] = []; 30 | private doorPadding: number; 31 | private r: Random; 32 | // 2D grid matching map dimensions where every element contains an array of all the rooms in 33 | // that location. 34 | public roomGrid: Room[][][] = []; 35 | private maxRooms: number; 36 | private maxRoomArea: number; 37 | private roomWidthConfig: DimensionConfigRequired; 38 | private roomHeightConfig: DimensionConfigRequired; 39 | private randomSeed?: string; 40 | 41 | constructor(config: DungeonConfig) { 42 | this.width = config.width; 43 | this.height = config.height; 44 | this.doorPadding = config.doorPadding ?? 1; 45 | this.rooms = []; 46 | this.randomSeed = config.randomSeed; 47 | this.r = new Random(this.randomSeed); 48 | 49 | const rooms = config.rooms; 50 | const roomWidth = rooms.width; 51 | const roomHeight = rooms.height; 52 | const maxPossibleRoomArea = roomWidth.max * roomHeight.max; 53 | const minPossibleRoomArea = roomWidth.min * roomHeight.min; 54 | const maxPossibleRooms = Math.floor((this.width * this.height) / minPossibleRoomArea); 55 | this.roomWidthConfig = { 56 | min: roomWidth.min, 57 | max: roomWidth.max, 58 | onlyOdd: roomWidth.onlyOdd ?? false, 59 | onlyEven: roomWidth.onlyEven ?? false 60 | }; 61 | this.roomHeightConfig = { 62 | min: roomHeight.min, 63 | max: roomHeight.max, 64 | onlyOdd: roomHeight.onlyOdd ?? false, 65 | onlyEven: roomHeight.onlyEven ?? false 66 | }; 67 | this.maxRooms = rooms.maxRooms ?? maxPossibleRooms; 68 | this.maxRoomArea = rooms.maxArea ?? maxPossibleRoomArea; 69 | 70 | this.adjustDimensionConfigForParity(this.roomWidthConfig); 71 | this.adjustDimensionConfigForParity(this.roomHeightConfig); 72 | this.checkDimensionConfig(this.roomWidthConfig); 73 | this.checkDimensionConfig(this.roomHeightConfig); 74 | 75 | // Validate the room width and height settings. 76 | if (this.roomWidthConfig.max > this.width) { 77 | throw new Error("Room max width cannot exceed dungeon width."); 78 | } 79 | if (this.roomHeightConfig.max > this.height) { 80 | throw new Error("Room max height cannot exceed dungeon height."); 81 | } 82 | // Validate the max area based on min dimensions. 83 | if (this.maxRoomArea < minPossibleRoomArea) { 84 | throw new Error("The minimum dimensions specified exceeds the given maxArea."); 85 | } 86 | 87 | this.generate(); 88 | this.tiles = this.getTiles(); 89 | } 90 | 91 | /** 92 | * Adjust the given dimension config for parity settings (onlyOdd, onlyEven) so that min/max are 93 | * adjusted to reflect actual possible values. 94 | * @param dimensionConfig 95 | */ 96 | private adjustDimensionConfigForParity(dimensionConfig: DimensionConfigRequired) { 97 | if (dimensionConfig.onlyOdd) { 98 | if (isEven(dimensionConfig.min)) { 99 | dimensionConfig.min++; 100 | console.log("Dungeon: warning, min dimension adjusted to match onlyOdd setting."); 101 | } 102 | if (isEven(dimensionConfig.max)) { 103 | dimensionConfig.max--; 104 | console.log("Dungeon: warning, max dimension adjusted to match onlyOdd setting."); 105 | } 106 | } else if (dimensionConfig.onlyEven) { 107 | if (isOdd(dimensionConfig.min)) { 108 | dimensionConfig.min++; 109 | console.log("Dungeon: warning, min dimension adjusted to match onlyEven setting."); 110 | } 111 | if (isOdd(dimensionConfig.max)) { 112 | dimensionConfig.max--; 113 | console.log("Dungeon: warning, max dimension adjusted to match onlyEven setting."); 114 | } 115 | } 116 | } 117 | 118 | private checkDimensionConfig(dimensionConfig: DimensionConfigRequired) { 119 | const { max, min, onlyEven, onlyOdd } = dimensionConfig; 120 | if (onlyEven && onlyOdd) { 121 | throw new Error("Cannot use both onlyEven and onlyOdd in room's width/height config."); 122 | } 123 | if (max < min) { 124 | throw new Error("Room width and height max must be >= min."); 125 | } 126 | if (min < 3) { 127 | throw new Error("Room width and height must be >= 3."); 128 | } 129 | } 130 | 131 | public getConfig(): DungeonConfig { 132 | return { 133 | width: this.width, 134 | height: this.height, 135 | doorPadding: this.doorPadding, 136 | randomSeed: this.randomSeed, 137 | rooms: { 138 | width: this.roomWidthConfig, 139 | height: this.roomHeightConfig, 140 | maxArea: this.maxRoomArea, 141 | maxRooms: this.maxRooms 142 | } 143 | }; 144 | } 145 | 146 | public drawToConsole(config: DebugConsoleConfig) { 147 | debugMap(this, config); 148 | } 149 | 150 | public drawToHtml(config: DebugHtmlConfig) { 151 | return debugHtmlMap(this, config); 152 | } 153 | 154 | public getMappedTiles(tileMapping: DebugTileMap = {}) { 155 | tileMapping = Object.assign({}, { empty: 0, wall: 1, floor: 2, door: 3 }, tileMapping); 156 | return this.tiles.map(row => 157 | row.map(tile => { 158 | if (tile === TILES.EMPTY) return tileMapping.empty; 159 | else if (tile === TILES.WALL) return tileMapping.wall; 160 | else if (tile === TILES.FLOOR) return tileMapping.floor; 161 | else if (tile === TILES.DOOR) return tileMapping.door; 162 | }) 163 | ); 164 | } 165 | 166 | public getCenter(): Point { 167 | return { 168 | x: Math.floor(this.width / 2), 169 | y: Math.floor(this.height / 2) 170 | }; 171 | } 172 | 173 | public generate() { 174 | this.rooms = []; 175 | this.roomGrid = []; 176 | 177 | for (let y = 0; y < this.height; y++) { 178 | this.roomGrid.push([]); 179 | for (let x = 0; x < this.width; x++) { 180 | this.roomGrid[y].push([]); 181 | } 182 | } 183 | 184 | // Seed the map with a starting randomly sized room in the center of the map. 185 | const mapCenter = this.getCenter(); 186 | const room = this.createRandomRoom(); 187 | room.setPosition( 188 | mapCenter.x - Math.floor(room.width / 2), 189 | mapCenter.y - Math.floor(room.height / 2) 190 | ); 191 | this.addRoom(room); 192 | 193 | // Continue generating rooms until we hit our cap or have hit our maximum iterations (generally 194 | // due to not being able to fit any more rooms in the map). 195 | let attempts = 0; 196 | const maxAttempts = this.maxRooms * 5; 197 | while (this.rooms.length < this.maxRooms && attempts < maxAttempts) { 198 | this.generateRoom(); 199 | attempts++; 200 | } 201 | } 202 | 203 | public hasRoomAt(x: number, y: number) { 204 | return x < 0 || y < 0 || x >= this.width || y >= this.height || this.roomGrid[y][x].length > 0; 205 | } 206 | 207 | public getRoomAt(x: number, y: number): Room | null { 208 | if (this.hasRoomAt(x, y)) { 209 | // Assumes 1 room per tile, which is valid for now 210 | return this.roomGrid[y][x][0]; 211 | } else { 212 | return null; 213 | } 214 | } 215 | 216 | /** 217 | * Attempt to add a room and return true/false based on whether it was successful. 218 | * @param room 219 | */ 220 | private addRoom(room: Room): Boolean { 221 | if (!this.canFitRoom(room)) return false; 222 | this.rooms.push(room); 223 | // Update all tiles in the roomGrid to indicate that this room is sitting on them. 224 | for (let y = room.top; y <= room.bottom; y++) { 225 | for (let x = room.left; x <= room.right; x++) { 226 | this.roomGrid[y][x].push(room); 227 | } 228 | } 229 | return true; 230 | } 231 | 232 | private canFitRoom(room: Room) { 233 | // Make sure the room fits inside the dungeon. 234 | if (room.x < 0 || room.right > this.width - 1) return false; 235 | if (room.y < 0 || room.bottom > this.height - 1) return false; 236 | 237 | // Make sure this room doesn't intersect any existing rooms. 238 | for (let i = 0; i < this.rooms.length; i++) { 239 | if (room.overlaps(this.rooms[i])) return false; 240 | } 241 | 242 | return true; 243 | } 244 | 245 | private createRandomRoom(): Room { 246 | let width = 0; 247 | let height = 0; 248 | let area = 0; 249 | 250 | // Find width and height using min/max sizes while keeping under the maximum area. 251 | const { roomWidthConfig, roomHeightConfig } = this; 252 | do { 253 | width = this.r.randomInteger(roomWidthConfig.min, roomWidthConfig.max, { 254 | onlyEven: roomWidthConfig.onlyEven, 255 | onlyOdd: roomWidthConfig.onlyOdd 256 | }); 257 | height = this.r.randomInteger(roomHeightConfig.min, roomHeightConfig.max, { 258 | onlyEven: roomHeightConfig.onlyEven, 259 | onlyOdd: roomHeightConfig.onlyOdd 260 | }); 261 | area = width * height; 262 | } while (area > this.maxRoomArea); 263 | 264 | return new Room(width, height); 265 | } 266 | 267 | private generateRoom() { 268 | const room = this.createRandomRoom(); 269 | 270 | // Only allow 150 tries at placing the room 271 | let i = 150; 272 | while (i > 0) { 273 | // Attempt to find another room to attach this one to 274 | const result = this.findRoomAttachment(room); 275 | 276 | room.setPosition(result.x, result.y); 277 | // Try to add it. If successful, add the door between the rooms and break the loop. 278 | if (this.addRoom(room)) { 279 | const [door1, door2] = this.findNewDoorLocation(room, result.target); 280 | this.addDoor(door1); 281 | this.addDoor(door2); 282 | break; 283 | } 284 | 285 | i -= 1; 286 | } 287 | } 288 | 289 | public getTiles() { 290 | const tiles = create2DArray(this.width, this.height, TILES.EMPTY); 291 | this.rooms.forEach(room => { 292 | room.forEachTile((point, tile) => { 293 | tiles[room.y + point.y][room.x + point.x] = tile; 294 | }); 295 | }); 296 | return tiles; 297 | } 298 | 299 | private getPotentiallyTouchingRooms(room: Room) { 300 | const touchingRooms: Room[] = []; 301 | 302 | // function that checks the list of rooms at a point in our grid for any potential touching 303 | // rooms 304 | const checkRoomList = (x: number, y: number) => { 305 | const r = this.roomGrid[y][x]; 306 | for (let i = 0; i < r.length; i++) { 307 | // make sure this room isn't the one we're searching around and that it isn't already in the 308 | // list 309 | if (r[i] != room && touchingRooms.indexOf(r[i]) === -1) { 310 | // make sure this isn't a corner of the room (doors can't go into corners) 311 | const lx = x - r[i].x; 312 | const ly = y - r[i].y; 313 | if ((lx > 0 && lx < r[i].width - 1) || (ly > 0 && ly < r[i].height - 1)) { 314 | touchingRooms.push(r[i]); 315 | } 316 | } 317 | } 318 | }; 319 | 320 | // iterate the north and south walls, looking for other rooms in those tile locations 321 | for (let x = room.x + 1; x < room.x + room.width - 1; x++) { 322 | checkRoomList(x, room.y); 323 | checkRoomList(x, room.y + room.height - 1); 324 | } 325 | 326 | // iterate the west and east walls, looking for other rooms in those tile locations 327 | for (let y = room.y + 1; y < room.y + room.height - 1; y++) { 328 | checkRoomList(room.x, y); 329 | checkRoomList(room.x + room.width - 1, y); 330 | } 331 | 332 | return touchingRooms; 333 | } 334 | 335 | private findNewDoorLocation(room1: Room, room2: Room): [Point, Point] { 336 | const door1 = { x: -1, y: -1 }; 337 | const door2 = { x: -1, y: -1 }; 338 | 339 | if (room1.y === room2.y - room1.height) { 340 | // North 341 | door1.x = door2.x = this.r.randomInteger( 342 | Math.floor(Math.max(room2.left, room1.left) + this.doorPadding), 343 | Math.floor(Math.min(room2.right, room1.right) - this.doorPadding) 344 | ); 345 | door1.y = room1.y + room1.height - 1; 346 | door2.y = room2.y; 347 | } else if (room1.x == room2.x - room1.width) { 348 | // West 349 | door1.x = room1.x + room1.width - 1; 350 | door2.x = room2.x; 351 | door1.y = door2.y = this.r.randomInteger( 352 | Math.floor(Math.max(room2.top, room1.top) + this.doorPadding), 353 | Math.floor(Math.min(room2.bottom, room1.bottom) - this.doorPadding) 354 | ); 355 | } else if (room1.x == room2.x + room2.width) { 356 | // East 357 | door1.x = room1.x; 358 | door2.x = room2.x + room2.width - 1; 359 | door1.y = door2.y = this.r.randomInteger( 360 | Math.floor(Math.max(room2.top, room1.top) + this.doorPadding), 361 | Math.floor(Math.min(room2.bottom, room1.bottom) - this.doorPadding) 362 | ); 363 | } else if (room1.y == room2.y + room2.height) { 364 | // South 365 | door1.x = door2.x = this.r.randomInteger( 366 | Math.floor(Math.max(room2.left, room1.left) + this.doorPadding), 367 | Math.floor(Math.min(room2.right, room1.right) - this.doorPadding) 368 | ); 369 | door1.y = room1.y; 370 | door2.y = room2.y + room2.height - 1; 371 | } 372 | 373 | return [door1, door2]; 374 | } 375 | 376 | private findRoomAttachment(room: Room) { 377 | const r = this.r.randomPick(this.rooms); 378 | 379 | let x = 0; 380 | let y = 0; 381 | const pad = 2 * this.doorPadding; // 2x padding to account for the padding both rooms need 382 | 383 | // Randomly position this room on one of the sides of the random room. 384 | switch (this.r.randomInteger(0, 3)) { 385 | // north 386 | case 0: 387 | // x = r.left - (room.width - 1) would have rooms sharing exactly 1x tile 388 | x = this.r.randomInteger(r.left - (room.width - 1) + pad, r.right - pad); 389 | y = r.top - room.height; 390 | break; 391 | // west 392 | case 1: 393 | x = r.left - room.width; 394 | y = this.r.randomInteger(r.top - (room.height - 1) + pad, r.bottom - pad); 395 | break; 396 | // east 397 | case 2: 398 | x = r.right + 1; 399 | y = this.r.randomInteger(r.top - (room.height - 1) + pad, r.bottom - pad); 400 | break; 401 | // south 402 | case 3: 403 | x = this.r.randomInteger(r.left - (room.width - 1) + pad, r.right - pad); 404 | y = r.bottom + 1; 405 | break; 406 | } 407 | 408 | // Return the position for this new room and the target room 409 | return { 410 | x: x, 411 | y: y, 412 | target: r 413 | }; 414 | } 415 | 416 | private addDoor(doorPos: Point) { 417 | // Get all the rooms at the location of the door 418 | const rooms = this.roomGrid[doorPos.y][doorPos.x]; 419 | rooms.forEach(room => { 420 | room.setTileAt(doorPos.x - room.x, doorPos.y - room.y, TILES.DOOR); 421 | }); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Dungeon, { DungeonConfig } from "./dungeon"; 2 | import Point from "./point"; 3 | import Room from "./room"; 4 | import TILES, { DebugTileMap } from "./tiles"; 5 | import { DebugHtmlConfig, DebugConsoleConfig } from "./debug"; 6 | 7 | export default Dungeon; 8 | export { DungeonConfig, Point, Room, TILES, DebugTileMap, DebugHtmlConfig, DebugConsoleConfig }; 9 | -------------------------------------------------------------------------------- /src/math.test.ts: -------------------------------------------------------------------------------- 1 | import { isEven, isOdd } from "./math"; 2 | 3 | describe("Math parity methods", () => { 4 | test("isEven should return true for even integers", () => { 5 | expect(isEven(0)).toBe(true); 6 | expect(isEven(2)).toBe(true); 7 | expect(isEven(-2)).toBe(true); 8 | }); 9 | 10 | test("isEven should return false for odd integers", () => { 11 | expect(isEven(1)).toBe(false); 12 | expect(isEven(-1)).toBe(false); 13 | }); 14 | 15 | test("isEven should return false for floating point numbers", () => { 16 | expect(isEven(0.5)).toBe(false); 17 | expect(isEven(-0.5)).toBe(false); 18 | }); 19 | 20 | test("isOdd should return true for odd integers", () => { 21 | expect(isOdd(1)).toBe(true); 22 | expect(isOdd(-1)).toBe(true); 23 | }); 24 | 25 | test("isOdd should return false for even integers", () => { 26 | expect(isOdd(0)).toBe(false); 27 | expect(isOdd(2)).toBe(false); 28 | expect(isOdd(-2)).toBe(false); 29 | }); 30 | 31 | test("isOdd should return false for floating point numbers", () => { 32 | expect(isOdd(0.5)).toBe(false); 33 | expect(isOdd(-0.5)).toBe(false); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | function isEven(value: number) { 2 | return Number.isInteger(value) && value % 2 === 0; 3 | } 4 | 5 | function isOdd(value: number) { 6 | return Number.isInteger(value) && value % 2 !== 0; 7 | } 8 | 9 | export { isEven, isOdd }; 10 | -------------------------------------------------------------------------------- /src/point.ts: -------------------------------------------------------------------------------- 1 | export default interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/random.test.ts: -------------------------------------------------------------------------------- 1 | import Random from "./random"; 2 | 3 | describe("randomInteger", () => { 4 | test("should return a number between min and max", () => { 5 | const r = new Random("seed"); 6 | Array(100) 7 | .map(() => r.randomInteger(3, 5)) 8 | .forEach(r => { 9 | expect(r).toBeGreaterThanOrEqual(3); 10 | expect(r).toBeLessThanOrEqual(5); 11 | }); 12 | }); 13 | 14 | test("should return a number between min and max when min > max", () => { 15 | const r = new Random("seed"); 16 | Array(100) 17 | .map(() => r.randomInteger(5, 3)) 18 | .forEach(r => { 19 | expect(r).toBeGreaterThanOrEqual(3); 20 | expect(r).toBeLessThanOrEqual(5); 21 | }); 22 | }); 23 | 24 | test("should return an even number when onlyEven option is used", () => { 25 | const r = new Random("seed"); 26 | Array(100) 27 | .map(() => r.randomInteger(2, 30, { onlyEven: true })) 28 | .forEach(r => { 29 | expect(r % 2).toBe(0); 30 | }); 31 | }); 32 | 33 | test("should return an even number when onlyEven option is used and parameters are odd", () => { 34 | const r = new Random("seed"); 35 | Array(100) 36 | .map(() => r.randomInteger(3, 31, { onlyEven: true })) 37 | .forEach(r => { 38 | expect(r % 2).toBe(0); 39 | }); 40 | }); 41 | 42 | test("should return an odd number when onlyOdd option is used", () => { 43 | const r = new Random("seed"); 44 | Array(100) 45 | .map(() => r.randomInteger(3, 31, { onlyOdd: true })) 46 | .forEach(r => { 47 | expect(r % 2).toBe(1); 48 | }); 49 | }); 50 | 51 | test("should return an odd number when onlyOdd option is used and parameters are even", () => { 52 | const r = new Random("seed"); 53 | Array(100) 54 | .map(() => r.randomInteger(2, 30, { onlyOdd: true })) 55 | .forEach(r => { 56 | expect(r % 2).toBe(1); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import * as seedrandom from "seedrandom"; 2 | 3 | export default class Random { 4 | private rng: seedrandom.prng; 5 | 6 | constructor(seedValue?: string) { 7 | this.rng = seedrandom(seedValue); 8 | } 9 | 10 | randomInteger(min: number, max: number, { onlyOdd = false, onlyEven = false } = {}) { 11 | if (onlyOdd) return this.randomOddInteger(min, max); 12 | else if (onlyEven) return this.randomEvenInteger(min, max); 13 | else return Math.floor(this.rng() * (max - min + 1) + min); 14 | } 15 | 16 | randomEvenInteger(min: number, max: number) { 17 | if (min % 2 !== 0 && min < max) min++; 18 | if (max % 2 !== 0 && max > min) max--; 19 | const range = (max - min) / 2; 20 | return Math.floor(this.rng() * (range + 1)) * 2 + min; 21 | } 22 | 23 | randomOddInteger(min: number, max: number) { 24 | if (min % 2 === 0) min++; 25 | if (max % 2 === 0) max--; 26 | const range = (max - min) / 2; 27 | return Math.floor(this.rng() * (range + 1)) * 2 + min; 28 | } 29 | 30 | randomPick(array: T[]): T { 31 | return array[this.randomInteger(0, array.length - 1)]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/room.ts: -------------------------------------------------------------------------------- 1 | import TILES from "./tiles"; 2 | import Point from "./point"; 3 | import create2DArray from "./create-2d-array"; 4 | 5 | export default class Room { 6 | private tiles: TILES[][]; 7 | public x = 0; 8 | public y = 0; 9 | public left = 0; 10 | public right = 0; 11 | public top = 0; 12 | public bottom = 0; 13 | public centerX = 0; 14 | public centerY = 0; 15 | 16 | constructor(public width: number, public height: number) { 17 | this.width = width; 18 | this.height = height; 19 | 20 | this.setPosition(0, 0); 21 | 22 | this.tiles = create2DArray(width, height, TILES.FLOOR); 23 | 24 | // Place walls around edges of room. 25 | for (let y = 0; y < this.height; y++) { 26 | this.setTileAt(0, y, TILES.WALL); 27 | this.setTileAt(width - 1, y, TILES.WALL); 28 | } 29 | for (let x = 0; x < this.width; x++) { 30 | this.setTileAt(x, 0, TILES.WALL); 31 | this.setTileAt(x, height - 1, TILES.WALL); 32 | } 33 | } 34 | 35 | public forEachTile(fn: (p: Point, t: TILES) => void) { 36 | for (let y = 0; y < this.height; y++) { 37 | for (let x = 0; x < this.width; x++) { 38 | fn({ x, y }, this.getTileAt(x, y)); 39 | } 40 | } 41 | } 42 | 43 | public setPosition(x: number, y: number) { 44 | this.x = x; 45 | this.y = y; 46 | this.left = x; 47 | this.right = x + (this.width - 1); 48 | this.top = y; 49 | this.bottom = y + (this.height - 1); 50 | this.centerX = x + Math.floor(this.width / 2); 51 | this.centerY = y + Math.floor(this.height / 2); 52 | } 53 | 54 | public getDoorLocations(): Point[] { 55 | const doors = []; 56 | for (let y = 0; y < this.height; y++) { 57 | for (let x = 0; x < this.width; x++) { 58 | if (this.getTileAt(x, y) == TILES.DOOR) { 59 | doors.push({ x, y }); 60 | } 61 | } 62 | } 63 | return doors; 64 | } 65 | 66 | public overlaps(otherRoom: Room) { 67 | if (this.right < otherRoom.left) return false; 68 | else if (this.left > otherRoom.right) return false; 69 | else if (this.bottom < otherRoom.top) return false; 70 | else if (this.top > otherRoom.bottom) return false; 71 | else return true; 72 | } 73 | 74 | /** 75 | * Check if the given local coordinates are within the dimensions of the room. 76 | * @param x 77 | * @param y 78 | */ 79 | public isInBounds(x: number, y: number) { 80 | return x >= 0 && x < this.width - 1 && y >= 0 && y < this.height - 1; 81 | } 82 | 83 | /** 84 | * Get the tile at the given local coordinates of the room. 85 | * @param x 86 | * @param y 87 | */ 88 | public getTileAt(x: number, y: number): TILES { 89 | return this.tiles[y][x]; 90 | } 91 | 92 | /** 93 | * Set the tile at the given local coordinates of the room. 94 | * @param x 95 | * @param y 96 | * @param tile 97 | */ 98 | public setTileAt(x: number, y: number, tile: TILES) { 99 | this.tiles[y][x] = tile; 100 | } 101 | 102 | /** 103 | * Check if two rooms share a door between them. 104 | * @param otherRoom 105 | */ 106 | public isConnectedTo(otherRoom: Room) { 107 | const doors = this.getDoorLocations(); 108 | for (const d of doors) { 109 | // Find door position relative to otherRoom's local coordinate system. 110 | const dx = this.x + d.x - otherRoom.x; 111 | const dy = this.y + d.y - otherRoom.y; 112 | 113 | if (otherRoom.isInBounds(dx, dy) && otherRoom.getTileAt(dx, dy) === TILES.DOOR) { 114 | return true; 115 | } 116 | } 117 | return false; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/tiles.ts: -------------------------------------------------------------------------------- 1 | enum TILES { 2 | EMPTY = 0, 3 | WALL = 1, 4 | FLOOR = 2, 5 | DOOR = 3 6 | } 7 | 8 | type DebugTileMap = { 9 | empty?: number | string; 10 | wall?: number | string; 11 | floor?: number | string; 12 | door?: number | string; 13 | }; 14 | 15 | export default TILES; 16 | export { DebugTileMap }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "es6", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "declaration": true, 9 | "lib": ["dom", "esnext"], 10 | "outDir": "dist/", 11 | "moduleResolution": "node" 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "src/**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "inputFiles": [ 3 | "./src" 4 | ], 5 | "mode": "modules", 6 | "out": "docs", 7 | "excludeNotExported": false, 8 | "excludePrivate": true, 9 | "theme": "default", 10 | "readme": "README.md" 11 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 5 | const root = __dirname; 6 | 7 | module.exports = function(env, argv) { 8 | const isDev = argv.mode === "development"; 9 | 10 | return { 11 | context: path.join(root, "src"), 12 | entry: { 13 | dungeon: "./index.ts", 14 | "dungeon.min": "./index.ts" 15 | }, 16 | output: { 17 | filename: "[name].js", 18 | path: path.join(root, "dist"), 19 | library: "Dungeon", 20 | libraryTarget: "umd", 21 | libraryExport: "default", 22 | globalObject: `typeof self !== 'undefined' ? self : this` 23 | }, 24 | optimization: { 25 | minimizer: [new UglifyJsPlugin({ include: /\.min\.js$/, sourceMap: true })] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | use: "ts-loader", 32 | exclude: /node_modules/ 33 | }, 34 | { 35 | test: /\.js$/, 36 | exclude: /node_modules/, 37 | use: "babel-loader" 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | extensions: [".ts", ".js"] 43 | }, 44 | devtool: isDev ? "eval-source-map" : "source-map" 45 | }; 46 | }; 47 | --------------------------------------------------------------------------------