├── .gitignore ├── public ├── sounds │ ├── key │ ├── door │ ├── flag │ └── wall ├── sprites │ ├── key.png │ ├── door.png │ ├── flag.png │ ├── player.png │ ├── wall.png │ ├── lockeddoor.png │ └── original │ │ ├── door.png │ │ ├── flag.png │ │ ├── key.png │ │ ├── wall.png │ │ ├── player.png │ │ └── lockeddoor.png ├── static │ ├── css │ │ └── game.css │ └── js │ │ └── game.js └── index.html ├── redis_rpg_map.jpg ├── redis_kaboom_game.gif ├── Dockerfile ├── screenshots ├── screenshot1.png └── screenshot2.png ├── images └── app_preview_image.png ├── src ├── apis │ └── apis.http ├── data_loader.js └── server.js ├── package.json ├── LICENSE ├── docker-compose.yml ├── marketplace.json ├── README.md ├── game_map.json └── game_map2.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | *.tmp 4 | *.swp 5 | *.bak 6 | redisdata/ -------------------------------------------------------------------------------- /public/sounds/key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sounds/key -------------------------------------------------------------------------------- /redis_rpg_map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/redis_rpg_map.jpg -------------------------------------------------------------------------------- /public/sounds/door: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sounds/door -------------------------------------------------------------------------------- /public/sounds/flag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sounds/flag -------------------------------------------------------------------------------- /public/sounds/wall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sounds/wall -------------------------------------------------------------------------------- /public/sprites/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/key.png -------------------------------------------------------------------------------- /redis_kaboom_game.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/redis_kaboom_game.gif -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | WORKDIR /app/ 3 | COPY package.json ./ 4 | RUN npm install 5 | COPY . . 6 | EXPOSE 8080 -------------------------------------------------------------------------------- /public/sprites/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/door.png -------------------------------------------------------------------------------- /public/sprites/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/flag.png -------------------------------------------------------------------------------- /public/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/player.png -------------------------------------------------------------------------------- /public/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/wall.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/screenshots/screenshot2.png -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/images/app_preview_image.png -------------------------------------------------------------------------------- /public/sprites/lockeddoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/lockeddoor.png -------------------------------------------------------------------------------- /public/sprites/original/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/door.png -------------------------------------------------------------------------------- /public/sprites/original/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/flag.png -------------------------------------------------------------------------------- /public/sprites/original/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/key.png -------------------------------------------------------------------------------- /public/sprites/original/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/wall.png -------------------------------------------------------------------------------- /public/sprites/original/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/player.png -------------------------------------------------------------------------------- /public/sprites/original/lockeddoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/HEAD/public/sprites/original/lockeddoor.png -------------------------------------------------------------------------------- /src/apis/apis.http: -------------------------------------------------------------------------------- 1 | ### 2 | # Api to get gameIds of active games 3 | # {API} /api/activegames 4 | # {Returns} { gameIds: String[], length: Number } 5 | 6 | GET http://localhost:8080/api/activegames 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/static/css/game.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background-color: #000000; 9 | height: 100vh; 10 | } 11 | 12 | p { 13 | color: #ffffff; 14 | } 15 | 16 | a:link, a:visited { 17 | color: #ffff00; 18 | } 19 | 20 | .title { 21 | color: #ffffff; 22 | } 23 | 24 | .canvas-holder { 25 | width: 100%; 26 | text-align:center; 27 | margin: auto; 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-kaboom-rpg", 3 | "version": "1.0.0", 4 | "description": "RPG type game example with Redis and Kaboom.js", 5 | "main": "src/server.js", 6 | "scripts": { 7 | "start": "node src/server.js", 8 | "dev": "node ./node_modules/nodemon/bin/nodemon.js", 9 | "load": "node src/data_loader.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "Redis" 14 | ], 15 | "author": "Simon Prickett", 16 | "license": "MIT", 17 | "dependencies": { 18 | "express": "^4.17.1", 19 | "ioredis": "^4.27.5", 20 | "nodemon": "^2.0.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program. If not, see . -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | redis: 4 | container_name: redis_kaboom 5 | image: "redislabs/redismod" 6 | ports: 7 | - 6379:6379 8 | volumes: 9 | - ./redisdata:/data 10 | entrypoint: 11 | redis-server 12 | --loadmodule /usr/lib/redis/modules/rejson.so 13 | --appendonly yes 14 | deploy: 15 | replicas: 1 16 | restart_policy: 17 | condition: on-failure 18 | node: 19 | container_name: node_kaboom 20 | build: . 21 | volumes: 22 | - .:/app 23 | - /app/node_modules 24 | command: sh -c "npm run load && npm run dev" 25 | depends_on: 26 | - redis 27 | ports: 28 | - 8080:8080 29 | environment: 30 | - REDIS_HOST=redis 31 | -------------------------------------------------------------------------------- /src/data_loader.js: -------------------------------------------------------------------------------- 1 | const Redis = require('ioredis'); 2 | 3 | const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1'; 4 | const REDIS_PORT = process.env.REDIS_PORT || 6379; 5 | const REDIS_PASSWORD = process.env.REDIS_PASSWORD; 6 | 7 | const loadGameData = async () => { 8 | // Connect to Redis... 9 | const redis = new Redis({ 10 | host: REDIS_HOST, 11 | port: REDIS_PORT, 12 | password: REDIS_PASSWORD 13 | }); 14 | 15 | // Where we'll store the room data in Redis. 16 | const GAME_MAP_KEY = 'kaboom:rooms'; 17 | 18 | // Load the room data from JSON file. 19 | const gameData = require('../game_map.json'); 20 | 21 | // Delete any previous data in Redis and store the room data 22 | // as a JSON document. 23 | await redis.del(GAME_MAP_KEY); 24 | await redis.call('JSON.SET', GAME_MAP_KEY, '.', JSON.stringify(gameData)); 25 | 26 | console.log('Data loaded!'); 27 | 28 | redis.quit(); 29 | }; 30 | 31 | loadGameData(); 32 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Redis Kaboom RPG Game 9 | 10 | 11 |
12 |
13 |
14 |

Redis Kaboom RPG Game

15 |
16 | 17 |
18 |

Source code: https://github.com/redis-developer/redis-kaboom-rpg.

19 |
20 |
21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Redis Kaboom RPG Game", 3 | "description": "A Role playing maze game with Redis, the Kaboom.js game framework, Express.js and Bulma using Redis JSON", 4 | "rank": "280", 5 | "type": "Full App", 6 | "contributed_by": "Redis", 7 | "repo_url": "https://github.com/redis-developer/redis-kaboom-rpg", 8 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/master/images/app_preview_image.png", 9 | "download_url": "https://github.com/redis-developer/redis-kaboom-rpg/archive/refs/heads/main.zip", 10 | "hosted_url": "", 11 | "quick_deploy": "false", 12 | "deploy_buttons": [ 13 | { 14 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/redis-kaboom-rpg" 15 | }, 16 | 17 | { 18 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/redis-kaboom-rpg" 19 | } 20 | ], 21 | 22 | "language": ["JavaScript", "Express"], 23 | "redis_commands": ["JSON.SET", "JSON.GET", "JSON.ARRLEN", "SADD", "SISMEMBER", "SCARD", "SMEMBERS", "XADD", "XRANGE", "XREVRANGE"], 24 | "redis_use_cases": [], 25 | "redis_features": ["JSON"], 26 | "app_image_urls": ["https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/main/screenshots/screenshot1.png", "https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/main/screenshots/screenshot2.png"], 27 | "youtube_url": "", 28 | "special_tags": [], 29 | "verticals": ["Gaming"], 30 | "markdown": "https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/main/README.md" 31 | } 32 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const Redis = require('ioredis'); 4 | 5 | // Redis configuration. 6 | const PORT = process.env.PORT||8080; 7 | const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1'; 8 | const REDIS_PORT = process.env.REDIS_PORT || 6379; 9 | const REDIS_PASSWORD = process.env.REDIS_PASSWORD; 10 | 11 | const app = express(); 12 | 13 | // Connect to Redis. 14 | const redis = new Redis({ 15 | host: REDIS_HOST, 16 | port: REDIS_PORT, 17 | password: REDIS_PASSWORD 18 | }); 19 | 20 | // Keep our Redis keys in a namespace. 21 | const getRedisKeyName = n => `kaboom:${n}`; 22 | 23 | // We'll use this key a lot to get data about rooms, stored 24 | // in Redis as a JSON document. 25 | const ROOM_KEY_NAME = getRedisKeyName('rooms'); 26 | 27 | // Serve the front end statically from the 'public' folder. 28 | app.use(express.static(path.join(__dirname, '../public'))); 29 | 30 | // Start a new game. 31 | app.get('/api/newgame', async (req, res) => { 32 | const gameId = Date.now(); 33 | const gameMovesKey = getRedisKeyName(`moves:${gameId}`); 34 | 35 | // Start a new stream for this game and set a long expiry in case 36 | // the user abandons it. 37 | await redis.xadd(gameMovesKey, '*', 'event', 'start'); 38 | redis.expire(gameMovesKey, 86400); 39 | 40 | // Pick 3 random room numbers to place the keys in for this game. 41 | let keysPlaced = 0; 42 | 43 | // We'll store these in a Redis set, so we'll need a key for that... 44 | const keyLocationsKey = getRedisKeyName(`keylocations:${gameId}`); 45 | 46 | // Figure out how many rooms are available so we know what the range 47 | // of room numbers to pick from is. 48 | const numRooms = await redis.call('JSON.ARRLEN', ROOM_KEY_NAME, '.'); 49 | 50 | do { 51 | const roomNumber = Math.floor(Math.random() * (numRooms - 1)); 52 | await redis.sadd(keyLocationsKey, roomNumber); 53 | keysPlaced = await redis.scard(keyLocationsKey); 54 | } while (keysPlaced < 3); 55 | 56 | // Set a long expiry on the key locations key in case the user 57 | // abandons the game. 58 | redis.expire(keyLocationsKey, 86400); 59 | 60 | console.log(`Started game ${gameId}.`); 61 | 62 | res.json({ gameId: gameId }); 63 | }); 64 | 65 | // Get JSON array of all currently active game IDs. 66 | app.get('/api/activegames', async (req, res) => { 67 | 68 | // Scan through all keys in the stream starting with "kaboom:moves". 69 | const stream = redis.scanStream({ 70 | match: 'kaboom:moves:*' 71 | }); 72 | 73 | const gameIds = []; 74 | 75 | stream.on('data', (keys) => { 76 | // Extract the gameId from the key and append to gameIds array. 77 | keys.forEach((key) => gameIds.push(key.split(':')[2])); 78 | }); 79 | 80 | stream.on('end', () => { 81 | res.status(200).json({ 82 | data: { 83 | gameIds, 84 | length: gameIds.length 85 | }, 86 | status: 'success', 87 | }) 88 | }) 89 | }); 90 | 91 | // Get details for a specified room number from Redis. 92 | app.get('/api/room/:gameId/:roomNumber', async (req, res) => { 93 | const { gameId, roomNumber } = req.params; 94 | 95 | const minRoomNumber = 0; 96 | const roomCount = JSON.parse(await redis.call('JSON.ARRLEN', ROOM_KEY_NAME, '.')); 97 | const maxRoomNumber = roomCount - 1; 98 | const roomNumberInteger = parseInt(roomNumber); 99 | 100 | if (roomNumberInteger < minRoomNumber || roomNumberInteger > maxRoomNumber) { 101 | console.log(`/api/room/:gameId/:roomNumber called with invalid room number of ${roomNumber}`) 102 | return res.status(400).send('Invalid room number') 103 | } 104 | 105 | // Store this movement in Redis. 106 | redis.xadd(getRedisKeyName(`moves:${gameId}`), '*', 'roomEntry', roomNumber); 107 | 108 | // Get the room details for this room. 109 | const roomDetails = JSON.parse(await redis.call('JSON.GET', ROOM_KEY_NAME, `[${roomNumber}]`)); 110 | 111 | // Does this room have a key in it for this specific game? 112 | const roomHasKey = await redis.sismember(getRedisKeyName(`keylocations:${gameId}`), roomNumber); 113 | 114 | if (roomHasKey === 0) { 115 | // No key here, so remove the 'k' placeholder from the room map. 116 | // String.replaceAll not available until Node 15... 117 | roomDetails.layout = roomDetails.layout.map(row => row.split('k').join(' ')); 118 | } 119 | 120 | res.json(roomDetails); 121 | }); 122 | 123 | // Get details for a specified room number. 124 | app.get('/api/randomroom/', async (req, res) => { 125 | // Figure out how many rooms are available. 126 | const numRooms = await redis.call('JSON.ARRLEN', ROOM_KEY_NAME, '.'); 127 | 128 | // Get a random number from room 0 to room (numRooms - 1). 129 | res.json({ room: Math.floor(Math.random() * (numRooms - 1)) }); 130 | }); 131 | 132 | // End the current game and get the stats. 133 | app.get('/api/endgame/:gameId', async (req, res) => { 134 | const { gameId } = req.params; 135 | 136 | // Check the format of the gameId in the request 137 | if (isNaN(gameId) || !/^[1-9]+[0-9]*$/.test(gameId)) { 138 | console.log(`/api/endgame/:gameId called with invalid gameId: ${gameId}.`); 139 | return res.status(400).send('Invalid gameId specified, should be a whole number greater than zero'); 140 | } 141 | 142 | // Check for gameIds that could only exist in the future 143 | if (gameId > Date.now()) { 144 | console.log(`/api/endgame/:gameId called with future gameId: ${gameId}.`); 145 | return res.status(400).send('Time travelling not allowed, this game hasn\'t started yet!'); 146 | } 147 | 148 | const gameMovesKey = getRedisKeyName(`moves:${gameId}`); 149 | 150 | // Does this gameMovesKey (still) exist? 151 | const gameMovesKeyExists = await redis.exists(gameMovesKey); 152 | if (!gameMovesKeyExists) { 153 | console.log(`Request for invalid or completed gameId: ${gameId}.`); 154 | return res.status(400).send('Game not found, invalid gameId or game has ended'); 155 | } 156 | 157 | // How many times did they enter a room (length of stream minus 1 for 158 | // the start event). 159 | const roomEntries = await redis.xlen(gameMovesKey) - 1; 160 | 161 | // Get the first and last entries in the stream, and the overall 162 | // elapsed game time will be the difference between the timestamp 163 | // components of their IDs. 164 | const streamStartAndEnd = await Promise.all([ 165 | redis.xrange(gameMovesKey, '-', '+', 'COUNT', 1), 166 | redis.xrevrange(gameMovesKey, '+', '-', 'COUNT', 1), 167 | ]); 168 | 169 | // Parse out the timestamps from the Redis return values. 170 | const startTimeStamp = parseInt(streamStartAndEnd[0][0][0].split('-')[0], 10); 171 | const endTimeStamp = parseInt(streamStartAndEnd[1][0][0].split('-')[0], 10); 172 | const elapsedTime = Math.floor((endTimeStamp - startTimeStamp) / 1000); 173 | 174 | // Tidy up, delete the stream and key locations keys as 175 | // we don't need them any more. 176 | redis.del(gameMovesKey); 177 | redis.del(getRedisKeyName(`keylocations:${gameId}`)); 178 | 179 | console.log(`Game ${gameId} has ended.`); 180 | 181 | res.json({ roomEntries, elapsedTime }); 182 | }); 183 | 184 | // Start the server. 185 | app.listen(PORT, () => { 186 | console.log(`Redis Kaboom RPG server listening on port ${PORT}, Redis at ${REDIS_HOST}:${REDIS_PORT}.`); 187 | }); 188 | -------------------------------------------------------------------------------- /public/static/js/game.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | // Initialize Kaboom... 3 | const k = kaboom({ 4 | global: true, 5 | scale: 3, 6 | clearColor: [0, 0, 0, 1], 7 | canvas: document.getElementById('game'), 8 | width: 180, 9 | height: 180 10 | }); 11 | 12 | // Load the various sprite graphics. 13 | loadRoot('/'); 14 | loadSprite('player', 'sprites/player.png'); 15 | loadSprite('wall', 'sprites/wall.png'); 16 | loadSprite('key', 'sprites/key.png'); 17 | loadSprite('flag', 'sprites/flag.png'); 18 | loadSprite('door', 'sprites/door.png'); 19 | loadSprite('lockeddoor', 'sprites/lockeddoor.png'); 20 | 21 | // Load the various sound effects 22 | loadSound("key", "sounds/key"); 23 | loadSound("wall", "sounds/wall"); 24 | loadSound("flag", "sounds/flag"); 25 | loadSound("door","sounds/door"); 26 | 27 | // Globals to remember which rooms the player found 28 | // keys in and the ID of the game they're playing. 29 | let keysHeld = []; 30 | let gameId; 31 | 32 | // Render a particular room... 33 | scene('play', async (roomNumber) => { 34 | // Get the room details from the server. 35 | const res = await fetch(`/api/room/${gameId}/${roomNumber}`); 36 | const roomDetails = await res.json(); 37 | 38 | let popupMsg = null; 39 | let keysHeldMsg = null; 40 | 41 | // Show a message e.g. one to tell the player how many 42 | // keys they need to open a locked door. 43 | const showMsg = (msg) => { 44 | popupMsg = add([ 45 | text(msg, 6), 46 | pos(width() / 2, 10), 47 | origin('center') 48 | ]); 49 | }; 50 | 51 | // Update the keys held message at the bottom of the screen. 52 | const updateKeysHeld = () => { 53 | if (keysHeldMsg) { 54 | destroy(keysHeldMsg); 55 | } 56 | 57 | keysHeldMsg = add([ 58 | text(`Room ${roomNumber}. Keys held: ${keysHeld.length}`, 6), 59 | pos(80, 150), 60 | origin('center') 61 | ]); 62 | }; 63 | 64 | // Mapping between characters in the room layout and sprites. 65 | const roomConf = { 66 | width: roomDetails.layout[0].length, 67 | height: roomDetails.layout.length, 68 | pos: vec2(20, 20), 69 | '@': [ 70 | sprite('player'), 71 | 'player' 72 | ], 73 | '=': [ 74 | sprite('wall'), 75 | "wall", 76 | solid() 77 | ], 78 | 'k': [ 79 | sprite('key'), 80 | 'key', 81 | solid() 82 | ], 83 | 'f': [ 84 | sprite('flag'), 85 | 'flag', 86 | solid() 87 | ] 88 | }; 89 | 90 | // Mapping for each door, determines whether to show a locked 91 | // or unlocked door... 92 | for (const doorId in roomDetails.doors) { 93 | const door = roomDetails.doors[doorId]; 94 | 95 | roomConf[doorId] = [ 96 | sprite(door.keysRequired > 0 ? 'lockeddoor' : 'door'), 97 | 'door', 98 | // Extra properties to store about this door - need 99 | // these when the player touches it to determine what 100 | // to do then. 101 | { 102 | leadsTo: door.leadsTo, 103 | keysRequired: door.keysRequired, 104 | isEnd: door.isEnd || false 105 | }, 106 | solid() 107 | ]; 108 | } 109 | 110 | addLevel(roomDetails.layout, roomConf); 111 | updateKeysHeld(); 112 | 113 | // Delete any key in this room if the player already collected it. 114 | const keys = get('key'); 115 | if (keys.length > 0 && keysHeld.includes(roomNumber)) { 116 | destroy(keys[0]); 117 | } 118 | 119 | const player = get('player')[0]; 120 | 121 | const directions = { 122 | 'left': vec2(-1, 0), 123 | 'right': vec2(1, 0), 124 | 'up': vec2(0, -1), 125 | 'down': vec2(0, 1) 126 | }; 127 | 128 | // Map key presses to player movement actions. 129 | for (const direction in directions) { 130 | keyPress(direction, () => { 131 | // Destroy any popup message 1/2 a second after 132 | // the player starts to move again. 133 | if (popupMsg) { 134 | wait(0.5, () => { 135 | if (popupMsg) { 136 | destroy(popupMsg); 137 | popupMsg = null; 138 | } 139 | }); 140 | } 141 | }); 142 | keyDown(direction, () => { 143 | // Move the player. 144 | player.move(directions[direction].scale(60)); 145 | }); 146 | } 147 | 148 | 149 | // What to do when the player touches a door. 150 | player.overlaps('door', (d) => { 151 | wait(0.3, () => { 152 | // Does opening this door require more keys than the player holds? 153 | if (d.keysRequired && d.keysRequired > keysHeld.length) { 154 | showMsg(`You need ${d.keysRequired - keysHeld.length} more keys!`); 155 | camShake(10); 156 | } else { 157 | // Does this door lead to the end state, or another room? 158 | play('door'); 159 | if (d.isEnd) { 160 | go('winner'); 161 | } else { 162 | go('play', d.leadsTo); 163 | } 164 | } 165 | }); 166 | }); 167 | 168 | player.overlaps("wall", (e) => { 169 | play("wall"); 170 | }); 171 | 172 | // What to do when the player touches a key. 173 | player.overlaps('key', (k) => { 174 | play("key"); 175 | destroy(k); 176 | showMsg('You got a key!'); 177 | // Remember the player has this key, so we don't 178 | // render it next time they enter this room and 179 | // so we know they can unlock some doors now. 180 | keysHeld.push(roomNumber); 181 | updateKeysHeld(); 182 | }); 183 | 184 | // What to do when the player touches a flag. 185 | player.overlaps('flag', async () => { 186 | play('flag'); 187 | // Go to a random room number and spin the 188 | // camera around and around. 189 | let angle = 0.1; 190 | const timer = setInterval(async () => { 191 | camRot(angle); 192 | angle += 0.1; 193 | 194 | if (angle >= 6.0) { 195 | // Stop spinning and go to the new room. 196 | camRot(0); 197 | clearInterval(timer); 198 | 199 | const res = await fetch('/api/randomroom'); 200 | const roomDetails = await res.json(); 201 | 202 | go('play', roomDetails.room); 203 | } 204 | }, 10); 205 | }); 206 | 207 | // Update the player position etc - run every frame. 208 | player.action(() => { 209 | player.resolve(); 210 | }); 211 | }); 212 | 213 | // Get a new game ID and start a new game. 214 | const newGame = async () => { 215 | const res = await fetch('/api/newgame'); 216 | const newGameResponse = await res.json(); 217 | 218 | gameId = newGameResponse.gameId; 219 | 220 | // New game always starts in room 0. 221 | go('play', 0); 222 | } 223 | 224 | // Display a message telling the player how to start 225 | // a new game. 226 | scene('start', () => { 227 | keysHeld = []; 228 | 229 | add([ 230 | text('press space to begin!', 6), 231 | pos(width() / 2, height() / 2), 232 | origin('center'), 233 | ]); 234 | 235 | keyPress('space', () => { 236 | newGame(); 237 | }); 238 | }); 239 | 240 | // This is the scene for when the player solves the 241 | // puzzle and escapes the maze with all the keys. 242 | scene('winner', async () => { 243 | // Reset for next game. 244 | keysHeld = []; 245 | 246 | // Get the number of times a room was entered and the 247 | // overall elapsed time for this game. 248 | const res = await fetch(`/api/endgame/${gameId}`); 249 | const { roomEntries, elapsedTime } = await res.json(); 250 | 251 | add([ 252 | text(`you escaped in:\n\n${roomEntries} moves.\n\n${elapsedTime} seconds.\n\nspace restarts!`, 6), 253 | pos(width() / 2, height() / 2), 254 | origin('center'), 255 | ]); 256 | 257 | keyPress('space', () => { 258 | newGame(); 259 | }); 260 | }); 261 | 262 | start('start'); 263 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Kaboom RPG Game 2 | 3 | This is an RPG maze type game built with [Kaboom.js](https://kaboomjs.com/), [Node.js](https://nodejs.org/) and [Redis](https://redis.io). It makes use of the [Redis JSON](https://redisjson.io) module from [Redis Inc](https://redis.com). 4 | 5 | ![Demo of the game running](https://raw.githubusercontent.com/redis-developer/redis-kaboom-rpg/main/redis_kaboom_game.gif) 6 | 7 | [Watch the video on YouTube!](https://www.youtube.com/watch?v=cowIZWASJNs) 8 | 9 | Since making the video and GIF above, we changed out the sprite images as part of our Hacktoberfest initiative. If you're looking for the originals that you see in the video, they're in this repo in `public/sprites/original`. 10 | 11 | ## Setup 12 | 13 | To run this game, you'll need [Docker](https://www.docker.com/) (or a local Redis instance, version 5 or higher with Redis JSON installed) and [Node.js](https://nodejs.org/) (use the current LTS version). First, clone the repo and install the dependencies: 14 | 15 | ```bash 16 | $ git clone https://github.com/redis-developer/redis-kaboom-rpg.git 17 | $ cd redis-kaboom-rpg 18 | $ npm install 19 | ``` 20 | 21 | ### Docker setup 22 | 23 | With Docker - you need to have Docker installed and there are no other requirements. You can use Docker to get a Redis instance with Redis JSON: 24 | 25 | ```bash 26 | $ docker-compose up -d 27 | ⠿ Network redis-kaboom-rpg_default Created 28 | ⠿ Container redis_kaboom Started 29 | ⠿ Container node_kaboom Started 30 | $ 31 | ``` 32 | 33 | Redis creates a folder named `redisdata` (inside the `redis-kaboom-rpg` folder that you cloned the GitHub repo to) and writes its append-only file there. This ensures that your data is persisted periodically and will still be there if you stop and restart the Docker container. 34 | 35 | Note that when using Docker, there is no need to load the game data as this is done for you. Once the containers are running you should be able to start a game simply by pointing the browser at http://localhost:8080/. 36 | 37 | ### Stopping Redis (Docker) 38 | 39 | If you started Redis using `docker-compose`, stop it as follows when you are done playing the game: 40 | 41 | ```bash 42 | $ docker-compose down 43 | Container node_kaboom Removed 44 | Container redis_kaboom Removed 45 | Network redis-kaboom-rpg_default Removed 46 | $ 47 | ``` 48 | 49 | ### Redis Setup (without Docker) 50 | 51 | Without Docker - you will need Redis 5 or higher, Redis JSON and Node.js (current LTS version recommended) 52 | 53 | This game uses Redis as a data store. The code assumes that Redis is running on localhost port 6379. You can configure an alternative Redis host and port by setting the `REDIS_HOST` and `REDIS_PORT` environment variables. If your Redis instance requires a password, supply that in the `REDIS_PASSWORD` environment variable. You'll need to have Redis JSON installed. 54 | 55 | ### Loading the Game Data 56 | 57 | Next, load the game map into Redis. This stores the map data from the `game_map.json` file in Redis, using Redis JSON: 58 | 59 | ```bash 60 | $ npm run load 61 | 62 | > redis-kaboom-rpg@1.0.0 load 63 | > node src/data_loader.js 64 | 65 | Data loaded! 66 | $ 67 | ``` 68 | 69 | You only need to do this once. Verify that the data loaded by ensuring that the key `kaboom:rooms` exists in Redis and is a Redis JSON document: 70 | 71 | ```bash 72 | 127.0.0.1:6379> type kaboom:rooms 73 | ReJSON-RL 74 | ``` 75 | 76 | ### Starting the Server 77 | 78 | To start the game server: 79 | 80 | ```bash 81 | $ npm run dev 82 | ``` 83 | 84 | Once the server is running, point your browser at `http://localhost:8080`. 85 | 86 | This starts the server using [nodemon](https://www.npmjs.com/package/nodemon), so saving changes to the source code files restarts the server for you automatically. 87 | 88 | If the server logs an error similar to this one, then Redis isn't running on the expected / configured host / port: 89 | 90 | ``` 91 | [ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379 92 | at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1146:16) 93 | ``` 94 | 95 | ### Stopping the Server 96 | 97 | To stop the Node.js server, press Ctrl-C. 98 | 99 | 100 | ### Playing the Game 101 | 102 | Press space to start, then use the arrow keys to move your character. Red doors are locked until you have found the appropriate number of keys. Touch a red door to find out how many keys are required, or pass through it if you have enough keys. Green doors are unlocked and don't require keys. 103 | 104 | Find all 3 keys and unlock the door in the room you started in (room 0) to escape. Touching a flag teleports you to a random other room. 105 | 106 | At the end of the game, you'll see how long you took to complete the challenge, and how many times you moved between rooms. 107 | 108 | ## How it Works 109 | 110 | Let's take a look at how the different components of the game architecture fit together. 111 | 112 | ### Project Structure 113 | 114 | The project consists of a Node.js back end that has API routes for some of the game logic and a static server for the front end. 115 | 116 | The back end code lives in the `src` folder, along with the data loader code, used to load the game room map into Redis. It uses the [Express framework](https://expressjs.com/) both to serve the front end HTML / JavaScript / CSS and image files, and also to implement a small API for starting and tracking game events. Redis connectivity is handled using the [ioredis client](https://www.npmjs.com/package/ioredis). 117 | 118 | The front end is written in JavaScript using [Kaboom.js](https://kaboomjs.com/) as the game engine, and the [Bulma CSS framework](https://bulma.io/) for some basic layout. It lives in the `public` folder. 119 | 120 | ### Working with Kaboom.js 121 | 122 | [Kaboom.js](https://kaboomjs.com/) describes itself as "...a JavaScript library that helps you make games fast and fun!". It renders games as a set of scenes in a HTML `` element, the ID of and size of which can be configured, along with some other properties: 123 | 124 | ```javascript 125 | const k = kaboom({ 126 | global: true, // imports all kaboom functions to global namespace. 127 | scale: 3, // pixel scale. 128 | clearColor: [0, 0, 0, 1], // black background. 129 | canvas: document.getElementById('game'), // which canvas to render in. 130 | width: 180, // width of the canvas. 131 | height: 180 // height of the canvas. 132 | }); 133 | ``` 134 | 135 | Each screen in a game is called a "scene" in Kaboom. Our game has three sorts of scene: 136 | 137 | 1. The `start` scene: this is shown when the game is first loaded, and encourages the player to press space to start a new game. 138 | 2. The `play` scene: used to render the room that the player is currently in, and handle player movement and collisions with other game objects (doors, flags, walls, keys). 139 | 3. The `winner` scene: used to dispay game stats once the player has defeated the game by collecting three keys and exiting through the locked door in room 0. 140 | 141 | The `start` and `winner` scenes have static text based layout. Kaboom provides a `scene` function to define a new scene, and other utility functions. Here's the complete definition of the `start` scene, which displays some centered text and waits for the space button to be pressed: 142 | 143 | ```javascript 144 | scene('start', () => { 145 | keysHeld = []; 146 | 147 | add([ 148 | text('press space to begin!', 6), // 6 = font size. 149 | pos(width() / 2, height() / 2), 150 | origin('center'), 151 | ]); 152 | 153 | keyPress('space', () => { 154 | newGame(); // a function run when space is pressed. 155 | }); 156 | }); 157 | ``` 158 | 159 | To start a game, we use the provided `start` function, passing it the name of a scene: 160 | 161 | ```javascript 162 | start('start'); 163 | ``` 164 | 165 | The `start` scene is then rendered. To change to another scene, we add logic that calls the `go` function, providing the name of the next scene: 166 | 167 | ```javascript 168 | scene('start', () => { 169 | ... 170 | 171 | keyPress('space', () => { 172 | go('play', 0); // Go the the 'play' scene, passing room ID 0 as a parameter. 173 | }); 174 | }); 175 | ``` 176 | 177 | There are 31 rooms in the maze for our game, and all of them are rendered using a single scene named `play`. This takes a room ID as a parameter, and uses that to retrieve the room's map from the back end which in turn gets it from Redis. Let's look at features of Kaboom that help enable this... 178 | 179 | First, a scene definition takes a function as its parameter. This defines what's in the scene, plus any logic. Here, our `play` scene has an `async` function parameter and the first thing it does it makes a request to the back end to get the map for the room it's been asked to render: 180 | 181 | ```javascript 182 | scene('play', async (roomNumber) => { 183 | // Get the room details from the server. 184 | const res = await fetch(`/api/room/${gameId}/${roomNumber}`); 185 | const roomDetails = await res.json(); 186 | ``` 187 | 188 | Each room's details contain an encoded tile map where different characters represent different graphics in the room. Kaboom provides sprite loading functionality, allowing us to use images as sprites and lay them out like tiles. Here, I'm loading some images: 189 | 190 | ```javascript 191 | loadSprite('player', 'sprites/player.png'); 192 | loadSprite('wall', 'sprites/wall.png'); 193 | loadSprite('key', 'sprites/key.png'); 194 | loadSprite('flag', 'sprites/flag.png'); 195 | loadSprite('door', 'sprites/door.png'); 196 | loadSprite('lockeddoor', 'sprites/lockeddoor.png'); 197 | ``` 198 | 199 | Kaboom's [`addLevel` function](https://kaboomjs.com/#addLevel) takes the room layout expressed as an array of characters (see the "Using Redis as a Data Store" section for details) and a series of objects describing which sprite to use for each character and any additional properties. It then renders this layout into the canvas and assigns each tile the appropriate properties. For example: 200 | 201 | ```javascript 202 | const roomConf = { 203 | '@': [ 204 | sprite('player'), 205 | 'player' 206 | ], 207 | '=': [ 208 | sprite('wall'), 209 | solid() 210 | ], 211 | 'k': [ 212 | sprite('key'), 213 | 'key', 214 | solid() 215 | ], 216 | 'f': [ 217 | sprite('flag'), 218 | 'flag', 219 | solid() 220 | ] 221 | ``` 222 | 223 | Now, each `@` character in the room layout becomes the player's sprite, each `=` a solid wall and so on. For our game, I chose to represent doors as the numbers 1..9 and give them extra properties such as whether they are locked or not. These are added to the `roomConf` array dynamically: 224 | 225 | ```javascript 226 | [ 227 | // Use the 'lockeddoor' sprite if the door's locked. 228 | sprite(door.keysRequired > 0 ? 'lockeddoor' : 'door'), 229 | 'door', 230 | // Extra properties to store about this door - need 231 | // these when the player touches it to determine what 232 | // to do then. 233 | { 234 | leadsTo: door.leadsTo, 235 | keysRequired: door.keysRequired, 236 | isEnd: door.isEnd || false 237 | }, 238 | solid() 239 | ] 240 | ``` 241 | 242 | Player movement is handled by describing how the player should move (x, y) when each of the cursor keys is pressed: 243 | 244 | ```javascript 245 | const directions = { 246 | 'left': vec2(-1, 0), 247 | 'right': vec2(1, 0), 248 | 'up': vec2(0, -1), 249 | 'down': vec2(0, 1) 250 | }; 251 | ``` 252 | 253 | `vec2` is a Kaboom function. Each direction is then associated with a `keyDown` event handler which calls Kaboom's `move` function on the player's sprite object: 254 | 255 | ``` 256 | for (const direction in directions) { 257 | keyDown(direction, () => { 258 | // Move the player. 259 | player.move(directions[direction].scale(60)); 260 | }); 261 | } 262 | ``` 263 | 264 | The Kaboom `scale` function adjusts the speed at which the movement happens. The real game code also handles `keyPress` events for each cursor - the callback for these deals with tidying up any transient on screen messaging e.g. as the player moves away from a locked door having been told they don't yet hold enough keys. 265 | 266 | We also need to detect collisions between the player and other game objects (keys, flags, doors). Kaboom provides a simple API for this. For example, when the player touches a flag, we provide a function containing the logic describing what to do: 267 | 268 | ```javascript 269 | player.overlaps('flag', async () => { 270 | // Go to a random room number. 271 | const res = await fetch('/api/randomroom'); 272 | const roomDetails = await res.json(); 273 | go('play', roomDetails.room); 274 | }); 275 | ``` 276 | 277 | The code above asks the back end application for a random room number and receives a response that looks like `{room: 22}`. It then tells Kaboom to move to the `play` scene for that room. So, when the player touches a flag... they're teleported to another room (or maybe back to the one they're already in). The game code for this has additional logic that creates a camera spin effect too. 278 | 279 | There's no need to provide code for when the player collides with a wall, as we don't need to take any specific action. Kaboom knows that players can't walk over or through walls as we declared them `solid` and that's all we need to say about them. 280 | 281 | The game tracks keys that the player has found using a global `keysHeld` array, containing the room ID(s) that the key(s) were found in. When a player touches a door, we can then figure out if they have enough keys to open it or not: 282 | 283 | ```javascript 284 | player.overlaps('door', (d) => { 285 | // Wait a short time before revealing what is going to happen. 286 | wait(0.3, ()=> { 287 | // Does opening this door require more keys than the player holds? 288 | if (d.keysRequired && d.keysRequired > keysHeld.length) { 289 | showMsg(`You need ${d.keysRequired - keysHeld.length} more keys!`); 290 | camShake(10); 291 | } else { 292 | // Does this door lead to the end state, or another room? 293 | if (d.isEnd) { 294 | go('winner'); 295 | } else { 296 | go('play', d.leadsTo); 297 | } 298 | } 299 | }); 300 | }); 301 | ``` 302 | 303 | If the player doesn't hold sufficient keys to open the door, Kaboom's `camShake` function is used to shake the camera and provide visual feedback that the door can't be opened. If they do have enough keys, they'll be taken to the next room or the `winner` scene if this door is the end of the maze. Kaboom's `wait` function is equivalent to a `setTimeout` in JavaScript, and is used to provide a small dramatic pause. 304 | 305 | These are the main things you need to know to build a game with Kaboom. The code for the game is in `static/js/game.js` and contains a few more nuances than we covered here.. 306 | 307 | ### Using Redis as a Data Store 308 | 309 | This game uses the following Redis data types and features: 310 | 311 | * **JSON (using Redis JSON)**: The tile map for each level (describing where the walls, doors, keys, flags and player's initial position are) is stored in Redis in a single key using Redis JSON. The data loader uses the `JSON.SET` command to store the data in a Redis key named `kaboom:rooms`. The Node.js back end retrieves the map for a given room with the `JSON.GET` command. Room data is stored as a JSON array in Redis. Each room's data is an object in the array: room 0 is the 0th array element, room 1 the first and so on. We use the `JSON.ARRLEN` command whenever we need to know how many rooms are in the map (for example when choosing a random room to teleport the user to when they touch a flag). Each room's data looks like this: 312 | 313 | ```json 314 | { 315 | "layout": [ 316 | "============", 317 | "= =", 318 | "= =", 319 | "= k =", 320 | "= == =", 321 | "1 f @= 2", 322 | "= == =", 323 | "= =", 324 | "= =", 325 | "= =", 326 | "============" 327 | ], 328 | "doors": { 329 | "1": { 330 | "leadsTo": 5 331 | }, 332 | "2": { 333 | "leadsTo": 3, 334 | "keysRequired": 3, 335 | "isEnd": true 336 | } 337 | } 338 | } 339 | ``` 340 | * The `layout` array contains the tilemap for the room, which Kaboom uses in the front end to render the appropriate tiles. `=` is a solid wall, `@` is the position that the player starts in when they enter the room, `f` is a teleporter flag, and numeric characters are doors to other rooms. 341 | * Each door is further described in the `doors` object. In the example above, door 1 leads to room 5, and door 2 to room 3. Door 2 is locked and the player requires 3 keys to pass through it. Door 2 is also the special `isEnd` door, which represents the escape point from the maze. 342 | * **Streams**: Each new game gets its own Stream. We use the timestamp when the game began as part of the key name, for example `kaboom:moves:1625561580120`. Each time the player enters a new room an entry is written to the Stream (using the `XADD` command). At the end of the game, data from the Stream is used to determine: 343 | * How many times the player entered a room (using the `XLEN` command). 344 | * How long the player took to complete the game (each Stream entry is timestamped, so we can calculate the game duration as the difference between the timestamps of the first and last entries). For this we use the `XRANGE` and `XREVRANGE` commands. 345 | * Each Stream entry looks like this: 346 | 347 | ```bash 348 | 127.0.0.1:6379> xrevrange kaboom:moves:1625561580120 + - count 1 349 | 1) 1) "1625561643258-0" 350 | 2) 1) "roomEntry" 351 | 2) "1" 352 | ``` 353 | * Additionally, we write a "start" event to the Stream when the game starts, so that we get the timestamp of the start of the game in the Stream, rather than waiting until the player first moves to another room to start the game clock. The "start" event will always be the first entry in the Stream, and looks like this: 354 | 355 | ```bash 356 | 127.0.0.1:6379> xrange kaboom:moves:1625561580120 - + count 1 357 | 1) 1) "1625561580122-0" 358 | 2) 1) "event" 359 | 2) "start" 360 | ``` 361 | * **Sets**: Every time the player begins a new game, the code "hides" the keys that the player needs to find to escape. It does this by putting random room numbers into a Redis Set until there are 3 members there, using the `SADD` and `SCARD` commands for this. There needs to be one set per running game, so we use the timestamp that the game was started as part of the key, for example `kaboom:keylocations:1625561580120`. The `SISMEMBER` commmand is used to check if a room number should have a key in it when sending the room map to the front end. If it should, then the `k` (key sprite) character is left in the room map, otherwise it's removed before sending the map to the front end. Each new game has its own set, containing three room numbers like so: 362 | 363 | ```bash 364 | 127.0.0.1:6379> smembers kaboom:keylocations:1625561580120 365 | 1) "5" 366 | 2) "17" 367 | 3) "29" 368 | ``` 369 | * **Key Expiry**: We use the `EXPIRE` command to ensure that keys associated with each game are removed from Redis after a day. This ensures that we don't have uncontrolled data growth in Redis, for example because the player abandons the game. When the player wins the game, keys are tidied up immediately using the `DEL` command. Here, I'm looking at the time to live for a game's Stream key with the `TTL` command: 370 | 371 | ```bash 372 | 127.0.0.1:6379> ttl kaboom:moves:1625561580120 373 | (integer) 86210 374 | ``` 375 | -------------------------------------------------------------------------------- /game_map.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "layout": [ 4 | "============", 5 | "= =", 6 | "= =", 7 | "= 1", 8 | "= =", 9 | "2 k @ =", 10 | "= =", 11 | "= =", 12 | "= f =", 13 | "= =", 14 | "======3=====" 15 | ], 16 | "doors": { 17 | "1": { 18 | "isEnd": true, 19 | "keysRequired": 3, 20 | "leadsTo": 0 21 | }, 22 | "2": { 23 | "leadsTo": 1 24 | }, 25 | "3": { 26 | "leadsTo": 28 27 | } 28 | } 29 | }, 30 | { 31 | "layout": [ 32 | "======1=====", 33 | "= =", 34 | "= =", 35 | "= =", 36 | "= === 2", 37 | "= = @ =", 38 | "3 = =", 39 | "= =", 40 | "= =", 41 | "= k =", 42 | "===4========" 43 | ], 44 | "doors": { 45 | "1": { 46 | "leadsTo": 2, 47 | "keysRequired": 1 48 | }, 49 | "2": { 50 | "leadsTo": 0 51 | }, 52 | "3": { 53 | "leadsTo": 6 54 | }, 55 | "4": { 56 | "leadsTo": 3 57 | } 58 | } 59 | }, 60 | { 61 | "layout": [ 62 | "============", 63 | "= k =", 64 | "3 = 2", 65 | "= f f f f =", 66 | "= =====", 67 | "= @= =", 68 | "= = ===", 69 | "= =", 70 | "= = =", 71 | "= =", 72 | "=====1======" 73 | ], 74 | "doors": { 75 | "1": { 76 | "leadsTo": 1 77 | }, 78 | "2": { 79 | "leadsTo": 33 80 | }, 81 | "3": { 82 | "leadsTo": 25, 83 | "keysRequired": 1 84 | } 85 | } 86 | }, 87 | { 88 | "layout": [ 89 | "=====1======", 90 | "= =", 91 | "= =", 92 | "= =", 93 | "= =", 94 | "= @ =", 95 | "= =", 96 | "= =", 97 | "= ===== =", 98 | "= k = =", 99 | "======2=====" 100 | ], 101 | "doors": { 102 | "1": { 103 | "leadsTo": 1 104 | }, 105 | "2": { 106 | "leadsTo": 4 107 | } 108 | } 109 | }, 110 | { 111 | "layout": [ 112 | "======1=====", 113 | "= =", 114 | "= =", 115 | "= k =", 116 | "= == =", 117 | "3 f @= =", 118 | "= == =", 119 | "= =", 120 | "= =", 121 | "= =", 122 | "====2=======" 123 | ], 124 | "doors": { 125 | "1": { 126 | "leadsTo": 3 127 | }, 128 | "2": { 129 | "leadsTo": 5 130 | }, 131 | "3": { 132 | "leadsTo": 11 133 | } 134 | } 135 | }, 136 | { 137 | "layout": [ 138 | "======1=====", 139 | "= =", 140 | "= =", 141 | "= =", 142 | "= =", 143 | "= @ =", 144 | "= =", 145 | "= =", 146 | "======== =", 147 | "= k =", 148 | "======2=====" 149 | ], 150 | "doors": { 151 | "1": { 152 | "leadsTo": 4 153 | }, 154 | "2": { 155 | "leadsTo": 38 156 | } 157 | } 158 | }, 159 | { 160 | "layout": [ 161 | "=====1======", 162 | "= =", 163 | "= = =", 164 | "= =", 165 | "= =", 166 | "3 @ 2", 167 | "= == =", 168 | "= k =", 169 | "= = = =", 170 | "= =", 171 | "============" 172 | ], 173 | "doors": { 174 | "1": { 175 | "leadsTo": 22 176 | }, 177 | "2": { 178 | "leadsTo": 1 179 | }, 180 | "3": { 181 | "leadsTo": 7 182 | } 183 | } 184 | }, 185 | { 186 | "layout": [ 187 | "============", 188 | "= =", 189 | "= =", 190 | "= =", 191 | "= =", 192 | "3 @ 2", 193 | "= =", 194 | "= =", 195 | "= ====k =", 196 | "= =", 197 | "======1=====" 198 | ], 199 | "doors": { 200 | "1": { 201 | "leadsTo": 9 202 | }, 203 | "2": { 204 | "leadsTo": 6 205 | }, 206 | "3": { 207 | "leadsTo": 8 208 | } 209 | } 210 | }, 211 | { 212 | "layout": [ 213 | "============", 214 | "= =", 215 | "= = =", 216 | "= =", 217 | "= = =", 218 | "2 @ 1", 219 | "= =", 220 | "= k= =", 221 | "= = =", 222 | "= =", 223 | "============" 224 | ], 225 | "doors": { 226 | "1": { 227 | "leadsTo": 7 228 | }, 229 | "2": { 230 | "leadsTo": 14 231 | } 232 | } 233 | }, 234 | { 235 | "layout": [ 236 | "======1=====", 237 | "= k =", 238 | "= = = =", 239 | "= = =", 240 | "= =", 241 | "= @ =", 242 | "= =", 243 | "= =", 244 | "= =", 245 | "= =", 246 | "======2=====" 247 | ], 248 | "doors": { 249 | "1": { 250 | "leadsTo": 7 251 | }, 252 | "2": { 253 | "leadsTo": 10 254 | } 255 | } 256 | }, 257 | { 258 | "layout": [ 259 | "======1=====", 260 | "= =", 261 | "= =", 262 | "= =", 263 | "= =", 264 | "= @ =", 265 | "= =", 266 | "= =", 267 | "= === =", 268 | "= k =", 269 | "============" 270 | ], 271 | "doors": { 272 | "1": { 273 | "leadsTo": 9 274 | } 275 | } 276 | }, 277 | { 278 | "layout": [ 279 | "============", 280 | "= =", 281 | "= =", 282 | "= =", 283 | "= =", 284 | "= @ 1", 285 | "= =", 286 | "= = = =", 287 | "= = = =", 288 | "= = = k =", 289 | "======2=====" 290 | ], 291 | "doors": { 292 | "1": { 293 | "leadsTo": 4 294 | }, 295 | "2": { 296 | "leadsTo": 12 297 | } 298 | } 299 | }, 300 | { 301 | "layout": [ 302 | "=====1======", 303 | "= k =", 304 | "= =", 305 | "= =", 306 | "= =", 307 | "2 @ =", 308 | "= =", 309 | "= =", 310 | "= =======", 311 | "= =", 312 | "============" 313 | ], 314 | "doors": { 315 | "1": { 316 | "leadsTo": 11 317 | }, 318 | "2": { 319 | "leadsTo": 13 320 | } 321 | } 322 | }, 323 | { 324 | "layout": [ 325 | "============", 326 | "= =", 327 | "= =", 328 | "= === =", 329 | "= =", 330 | "2 @ 1", 331 | "= =", 332 | "= =", 333 | "= f k =", 334 | "= =", 335 | "============" 336 | ], 337 | "doors": { 338 | "1": { 339 | "leadsTo": 12 340 | }, 341 | "2": { 342 | "leadsTo": 26 343 | } 344 | } 345 | }, 346 | { 347 | "layout": [ 348 | "======3=====", 349 | "= =", 350 | "= ==== =", 351 | "= =", 352 | "= =", 353 | "= = @ = 1", 354 | "= =", 355 | "= =", 356 | "= == =", 357 | "= k =", 358 | "======2=====" 359 | ], 360 | "doors": { 361 | "1": { 362 | "leadsTo": 8 363 | }, 364 | "2": { 365 | "leadsTo": 18 366 | }, 367 | "3": { 368 | "leadsTo": 15 369 | } 370 | } 371 | }, 372 | { 373 | "layout": [ 374 | "======1=====", 375 | "= = =", 376 | "= = =", 377 | "= = =", 378 | "= = =", 379 | "3 @ =", 380 | "= =", 381 | "= = =", 382 | "= k = =", 383 | "= = =", 384 | "=====2======" 385 | ], 386 | "doors": { 387 | "1": { 388 | "leadsTo": 16 389 | }, 390 | "2": { 391 | "leadsTo": 14 392 | }, 393 | "3": { 394 | "leadsTo": 21, 395 | "keysRequired": 2 396 | } 397 | } 398 | }, 399 | { 400 | "layout": [ 401 | "============", 402 | "= =", 403 | "= =", 404 | "= =", 405 | "= =", 406 | "= = @ 1", 407 | "= = =", 408 | "= = =", 409 | "= k= =", 410 | "= =", 411 | "======2=====" 412 | ], 413 | "doors": { 414 | "1": { 415 | "leadsTo": 17 416 | }, 417 | "2": { 418 | "leadsTo": 15 419 | } 420 | } 421 | }, 422 | { 423 | "layout": [ 424 | "============", 425 | "= =", 426 | "= =", 427 | "= k =", 428 | "= = = = =", 429 | "2 = @ = 1", 430 | "= = = =", 431 | "= = = =", 432 | "= =", 433 | "= =", 434 | "============" 435 | ], 436 | "doors": { 437 | "1": { 438 | "leadsTo": 24 439 | }, 440 | "2": { 441 | "leadsTo": 16 442 | } 443 | } 444 | }, 445 | { 446 | "layout": [ 447 | "======1=====", 448 | "= =", 449 | "= =", 450 | "= =", 451 | "= =", 452 | "= @= =", 453 | "= =", 454 | "= =", 455 | "= ===k =", 456 | "= =", 457 | "=====2======" 458 | ], 459 | "doors": { 460 | "1": { 461 | "leadsTo": 14 462 | }, 463 | "2": { 464 | "leadsTo": 19 465 | } 466 | } 467 | }, 468 | { 469 | "layout": [ 470 | "=====1======", 471 | "= = =", 472 | "= = =", 473 | "= f =", 474 | "= =", 475 | "= @ =", 476 | "= =", 477 | "= ====== = =", 478 | "= k =", 479 | "= =", 480 | "=====2======" 481 | ], 482 | "doors": { 483 | "1": { 484 | "leadsTo": 18 485 | }, 486 | "2": { 487 | "leadsTo": 20 488 | } 489 | } 490 | }, 491 | { 492 | "layout": [ 493 | "=====1======", 494 | "= =", 495 | "= ==== =", 496 | "= =", 497 | "= =", 498 | "= @ 2", 499 | "= =", 500 | "= =", 501 | "= ==== =", 502 | "= k=", 503 | "============" 504 | ], 505 | "doors": { 506 | "1": { 507 | "leadsTo": 19 508 | }, 509 | "2": { 510 | "leadsTo": 26 511 | } 512 | } 513 | }, 514 | { 515 | "layout": [ 516 | "============", 517 | "= =", 518 | "= ==== =", 519 | "= =", 520 | "= =", 521 | "= @ 1", 522 | "= =", 523 | "= =", 524 | "= ==== k=", 525 | "= =", 526 | "======2=====" 527 | ], 528 | "doors": { 529 | "1": { 530 | "leadsTo": 15, 531 | "keysRequired": 2 532 | }, 533 | "2": { 534 | "leadsTo": 39 535 | } 536 | } 537 | }, 538 | { 539 | "layout": [ 540 | "============", 541 | "= =", 542 | "= =", 543 | "= k =", 544 | "= =", 545 | "2 =@= =", 546 | "= = =", 547 | "= =", 548 | "= =", 549 | "= =", 550 | "======1=====" 551 | ], 552 | "doors": { 553 | "1": { 554 | "leadsTo": 6 555 | }, 556 | "2": { 557 | "leadsTo": 23 558 | } 559 | } 560 | }, 561 | { 562 | "layout": [ 563 | "=====1======", 564 | "= =", 565 | "= =", 566 | "== =", 567 | "= =", 568 | "= @ 2", 569 | "= =", 570 | "= =", 571 | "= = =", 572 | "= k =", 573 | "============" 574 | ], 575 | "doors": { 576 | "1": { 577 | "leadsTo": 24 578 | }, 579 | "2": { 580 | "leadsTo": 22 581 | }, 582 | "3": { 583 | "leadsTo": 41 584 | }, 585 | "4": { 586 | "leadsTo": 39 587 | } 588 | } 589 | }, 590 | { 591 | "layout": [ 592 | "============", 593 | "= =", 594 | "= =", 595 | "= f =", 596 | "= fffff =", 597 | "3 @ f 1", 598 | "= f fff =", 599 | "= f k =", 600 | "= f =", 601 | "= =", 602 | "=====2======" 603 | ], 604 | "doors": { 605 | "1": { 606 | "leadsTo": 25 607 | }, 608 | "2": { 609 | "leadsTo": 23 610 | }, 611 | "3": { 612 | "leadsTo": 17 613 | } 614 | } 615 | }, 616 | { 617 | "layout": [ 618 | "============", 619 | "= k =", 620 | "= = =", 621 | "= = =", 622 | "= = =", 623 | "2 = @ 1", 624 | "= = =", 625 | "= = =", 626 | "= = 3", 627 | "= =", 628 | "============" 629 | ], 630 | "doors": { 631 | "1": { 632 | "leadsTo": 33 633 | }, 634 | "2": { 635 | "leadsTo": 24 636 | }, 637 | "3": { 638 | "leadsTo": 2, 639 | "keysRequired": 1 640 | } 641 | } 642 | }, 643 | { 644 | "layout": [ 645 | "======3=====", 646 | "= =", 647 | "= == =", 648 | "= f =", 649 | "= = =", 650 | "2 @ 1", 651 | "= =", 652 | "= =", 653 | "= = =", 654 | "= k ==== =", 655 | "============" 656 | ], 657 | "doors": { 658 | "1": { 659 | "leadsTo": 13 660 | }, 661 | "2": { 662 | "leadsTo": 20 663 | }, 664 | "3": { 665 | "leadsTo": 27, 666 | "keysRequired": 1 667 | } 668 | } 669 | }, 670 | { 671 | "layout": [ 672 | "============", 673 | "= =", 674 | "= =", 675 | "= =", 676 | "= = =", 677 | "= =k @ =", 678 | "= = = =", 679 | "= =", 680 | "= =", 681 | "= =", 682 | "======1=====" 683 | ], 684 | "doors": { 685 | "1": { 686 | "leadsTo": 26, 687 | "keysRequired": 1 688 | } 689 | } 690 | }, 691 | { 692 | "layout": [ 693 | "=====1======", 694 | "= = =", 695 | "= = =", 696 | "= k =", 697 | "= = =", 698 | "= @ 2", 699 | "= = =", 700 | "= =", 701 | "= = =", 702 | "= =", 703 | "============" 704 | ], 705 | "doors": { 706 | "1": { 707 | "leadsTo": 0 708 | }, 709 | "2": { 710 | "leadsTo": 29 711 | } 712 | } 713 | }, 714 | { 715 | "layout": [ 716 | "============", 717 | "= =", 718 | "= = =", 719 | "= = =", 720 | "= =", 721 | "2 @ 1", 722 | "= =", 723 | "= = =", 724 | "= = k=", 725 | "= f =", 726 | "============" 727 | ], 728 | "doors": { 729 | "1": { 730 | "leadsTo": 30 731 | }, 732 | "2": { 733 | "leadsTo": 28 734 | } 735 | } 736 | }, 737 | { 738 | "layout": [ 739 | "=====1======", 740 | "= =", 741 | "= =", 742 | "=k==========", 743 | "= =", 744 | "3 @ =", 745 | "= =", 746 | "= =", 747 | "= =", 748 | "= =", 749 | "=====2======" 750 | ], 751 | "doors": { 752 | "1": { 753 | "leadsTo": 31 754 | }, 755 | "2": { 756 | "leadsTo": 34 757 | }, 758 | "3": { 759 | "leadsTo": 29 760 | } 761 | } 762 | }, 763 | { 764 | "layout": [ 765 | "=====1======", 766 | "= = = k =", 767 | "= = = =", 768 | "= == =", 769 | "= =", 770 | "= = @ =", 771 | "= =", 772 | "= = =", 773 | "= = = =", 774 | "= =", 775 | "=====2======" 776 | ], 777 | "doors": { 778 | "1": { 779 | "leadsTo": 32 780 | }, 781 | "2": { 782 | "leadsTo": 30 783 | } 784 | } 785 | }, 786 | { 787 | "layout": [ 788 | "======2=====", 789 | "= k =", 790 | "= = = =", 791 | "= = = =", 792 | "= = =", 793 | "= = @ = =", 794 | "= = = =", 795 | "= = =", 796 | "= = = =", 797 | "= =", 798 | "=====1======" 799 | ], 800 | "doors": { 801 | "1": { 802 | "leadsTo": 31 803 | }, 804 | "2": { 805 | "leadsTo": 33 806 | } 807 | } 808 | }, 809 | { 810 | "layout": [ 811 | "============", 812 | "3 =", 813 | "= ==========", 814 | "2 =", 815 | "========== =", 816 | "= @ =", 817 | "= ==========", 818 | "= k =", 819 | "========== =", 820 | "= =", 821 | "=====1======" 822 | ], 823 | "doors": { 824 | "1": { 825 | "leadsTo": 32 826 | }, 827 | "2": { 828 | "leadsTo": 2, 829 | "keysRequired": 1 830 | }, 831 | "3": { 832 | "leadsTo": 25 833 | } 834 | } 835 | }, 836 | { 837 | "layout": [ 838 | "=====1======", 839 | "= =", 840 | "= = = =", 841 | "= = f =", 842 | "= = = =", 843 | "= @ =", 844 | "= =", 845 | "= = = =", 846 | "= = =", 847 | "= = = k =", 848 | "=======2====" 849 | ], 850 | "doors": { 851 | "1": { 852 | "leadsTo": 30 853 | }, 854 | "2": { 855 | "leadsTo": 35 856 | } 857 | } 858 | }, 859 | { 860 | "layout": [ 861 | "======1=====", 862 | "= = =", 863 | "= = =", 864 | "= k = =", 865 | "= = =", 866 | "= = @ =", 867 | "= = =", 868 | "= = =", 869 | "= = = =", 870 | "= = =", 871 | "====2=======" 872 | ], 873 | "doors": { 874 | "1": { 875 | "leadsTo": 34 876 | }, 877 | "2": { 878 | "leadsTo": 36 879 | } 880 | } 881 | }, 882 | { 883 | "layout": [ 884 | "=====1======", 885 | "= =", 886 | "= = = =", 887 | "= = = =", 888 | "= == = =", 889 | "= =f@ = =", 890 | "= ===== =", 891 | "= =", 892 | "= k =", 893 | "= =", 894 | "======2=====" 895 | ], 896 | "doors": { 897 | "1": { 898 | "leadsTo": 35 899 | }, 900 | "2": { 901 | "leadsTo": 37 902 | } 903 | } 904 | }, 905 | { 906 | "layout": [ 907 | "======1=====", 908 | "= =", 909 | "= =", 910 | "= = =", 911 | "= =", 912 | "2 = @ = =", 913 | "= =", 914 | "= = =", 915 | "= f k =", 916 | "= =", 917 | "============" 918 | ], 919 | "doors": { 920 | "1": { 921 | "leadsTo": 36 922 | }, 923 | "2": { 924 | "leadsTo": 38 925 | } 926 | } 927 | }, 928 | { 929 | "layout": [ 930 | "=====2======", 931 | "= =", 932 | "= = = =", 933 | "= =", 934 | "= =", 935 | "3 @ f 1", 936 | "= =", 937 | "= k =", 938 | "= = = =", 939 | "= =", 940 | "============" 941 | ], 942 | "doors": { 943 | "1": { 944 | "leadsTo": 37 945 | }, 946 | "2": { 947 | "leadsTo": 5 948 | }, 949 | "3": { 950 | "leadsTo": 39 951 | } 952 | } 953 | }, 954 | { 955 | "layout": [ 956 | "=====2======", 957 | "= =", 958 | "= = = =", 959 | "= =", 960 | "= =", 961 | "= @ f 1", 962 | "= =", 963 | "= k =", 964 | "= = = =", 965 | "= =", 966 | "============" 967 | ], 968 | "doors": { 969 | "1": { 970 | "leadsTo": 38 971 | }, 972 | "2": { 973 | "leadsTo": 21 974 | } 975 | } 976 | } 977 | ] 978 | -------------------------------------------------------------------------------- /game_map2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "layout": [ 4 | "=====1======", 5 | "= =", 6 | "= k =", 7 | "= =", 8 | "= =", 9 | "= @ 2", 10 | "= =", 11 | "= =", 12 | "= f =", 13 | "= =", 14 | "======3=====" 15 | ], 16 | "doors": { 17 | "1": { 18 | "leadsTo": 1 19 | }, 20 | "2": { 21 | "leadsTo": 0, 22 | "isEnd": true, 23 | "keysRequired": 3 24 | }, 25 | "3": { 26 | "leadsTo": 31 27 | } 28 | } 29 | }, 30 | { 31 | "layout": [ 32 | "============", 33 | "= =", 34 | "= =", 35 | "= =", 36 | "= === =", 37 | "1 = @ =", 38 | "= = =", 39 | "= =", 40 | "= =", 41 | "= k =", 42 | "======2=====" 43 | ], 44 | "doors": { 45 | "1": { 46 | "leadsTo": 2 47 | }, 48 | "2": { 49 | "leadsTo": 0 50 | } 51 | } 52 | }, 53 | { 54 | "layout": [ 55 | "============", 56 | "= k =", 57 | "= = =", 58 | "= =", 59 | "= =====", 60 | "1 @= 2", 61 | "= = ===", 62 | "= =", 63 | "= = =", 64 | "= =", 65 | "============" 66 | ], 67 | "doors": { 68 | "1": { 69 | "leadsTo": 3 70 | }, 71 | "2": { 72 | "leadsTo": 1 73 | } 74 | } 75 | }, 76 | { 77 | "layout": [ 78 | "=====1======", 79 | "= =", 80 | "= =", 81 | "= =", 82 | "= =", 83 | "2 @ 3", 84 | "= =", 85 | "= =", 86 | "= ===== =", 87 | "= k = =", 88 | "======4=====" 89 | ], 90 | "doors": { 91 | "1": { 92 | "leadsTo": 6 93 | }, 94 | "2": { 95 | "leadsTo": 4 96 | }, 97 | "3": { 98 | "leadsTo": 2 99 | }, 100 | "4": { 101 | "leadsTo": 28, 102 | "keysRequired": 1 103 | } 104 | } 105 | }, 106 | { 107 | "layout": [ 108 | "============", 109 | "= =", 110 | "= =", 111 | "= k =", 112 | "= == =", 113 | "1 f @= 2", 114 | "= == =", 115 | "= =", 116 | "= =", 117 | "= =", 118 | "============" 119 | ], 120 | "doors": { 121 | "1": { 122 | "leadsTo": 5 123 | }, 124 | "2": { 125 | "leadsTo": 3 126 | } 127 | } 128 | }, 129 | { 130 | "layout": [ 131 | "============", 132 | "= =", 133 | "= =", 134 | "= =", 135 | "= =", 136 | "= @ 1", 137 | "= =", 138 | "= =", 139 | "======== =", 140 | "= k =", 141 | "============" 142 | ], 143 | "doors": { 144 | "1": { 145 | "leadsTo": 4 146 | } 147 | } 148 | }, 149 | { 150 | "layout": [ 151 | "=====1======", 152 | "= =", 153 | "= = =", 154 | "= =", 155 | "= =", 156 | "= @ =", 157 | "= == =", 158 | "= k =", 159 | "= = = =", 160 | "= =", 161 | "======2=====" 162 | ], 163 | "doors": { 164 | "1": { 165 | "leadsTo": 7 166 | }, 167 | "2": { 168 | "leadsTo": 3 169 | } 170 | } 171 | }, 172 | { 173 | "layout": [ 174 | "=====1======", 175 | "= =", 176 | "= =", 177 | "= =", 178 | "= =", 179 | "2 @ 4", 180 | "= =", 181 | "= =", 182 | "= ====k =", 183 | "= =", 184 | "======3=====" 185 | ], 186 | "doors": { 187 | "1": { 188 | "leadsTo": 8 189 | }, 190 | "2": { 191 | "leadsTo": 14 192 | }, 193 | "3": { 194 | "leadsTo": 6 195 | }, 196 | "4": { 197 | "leadsTo": 37 198 | } 199 | } 200 | }, 201 | { 202 | "layout": [ 203 | "=====1======", 204 | "= =", 205 | "= = =", 206 | "= =", 207 | "= = =", 208 | "= @ =", 209 | "= =", 210 | "= k= =", 211 | "= = =", 212 | "= =", 213 | "======2=====" 214 | ], 215 | "doors": { 216 | "1": { 217 | "leadsTo": 9 218 | }, 219 | "2": { 220 | "leadsTo": 7 221 | } 222 | } 223 | }, 224 | { 225 | "layout": [ 226 | "============", 227 | "= k = =", 228 | "= = = =", 229 | "= =", 230 | "= =", 231 | "1 @ =", 232 | "= =", 233 | "= =", 234 | "= =", 235 | "= =", 236 | "======2=====" 237 | ], 238 | "doors": { 239 | "1": { 240 | "leadsTo": 10 241 | }, 242 | "2": { 243 | "leadsTo": 8 244 | } 245 | } 246 | }, 247 | { 248 | "layout": [ 249 | "============", 250 | "= =", 251 | "= =", 252 | "= =", 253 | "= =", 254 | "1 @ 2", 255 | "= =", 256 | "= =", 257 | "= === =", 258 | "= k =", 259 | "============" 260 | ], 261 | "doors": { 262 | "1": { 263 | "leadsTo": 11 264 | }, 265 | "2": { 266 | "leadsTo": 9 267 | } 268 | } 269 | }, 270 | { 271 | "layout": [ 272 | "============", 273 | "= =", 274 | "= =", 275 | "= =", 276 | "= =", 277 | "= @ 1", 278 | "= =", 279 | "= = = =", 280 | "= = = =", 281 | "= = = k =", 282 | "======2=====" 283 | ], 284 | "doors": { 285 | "1": { 286 | "leadsTo": 10 287 | }, 288 | "2": { 289 | "leadsTo": 12 290 | } 291 | } 292 | }, 293 | { 294 | "layout": [ 295 | "=====1======", 296 | "= k =", 297 | "= =", 298 | "= =", 299 | "= =", 300 | "= @ =", 301 | "= =", 302 | "= =", 303 | "= =======", 304 | "= =", 305 | "======2=====" 306 | ], 307 | "doors": { 308 | "1": { 309 | "leadsTo": 11 310 | }, 311 | "2": { 312 | "leadsTo": 13 313 | } 314 | } 315 | }, 316 | { 317 | "layout": [ 318 | "=====1======", 319 | "= =", 320 | "= =", 321 | "= === =", 322 | "= =", 323 | "2 @ 3", 324 | "= =", 325 | "= =", 326 | "= fk =", 327 | "= =", 328 | "============" 329 | ], 330 | "doors": { 331 | "1": { 332 | "leadsTo": 12 333 | }, 334 | "2": { 335 | "leadsTo": 15 336 | }, 337 | "3": { 338 | "leadsTo": 14 339 | } 340 | } 341 | }, 342 | { 343 | "layout": [ 344 | "============", 345 | "= =", 346 | "= ==== =", 347 | "= =", 348 | "= =", 349 | "1 = @ = 2", 350 | "= =", 351 | "= =", 352 | "= == =", 353 | "= k =", 354 | "============" 355 | ], 356 | "doors": { 357 | "1": { 358 | "leadsTo": 13 359 | }, 360 | "2": { 361 | "leadsTo": 7 362 | } 363 | } 364 | }, 365 | { 366 | "layout": [ 367 | "============", 368 | "= = =", 369 | "= = =", 370 | "= = =", 371 | "= = =", 372 | "1 @ 2", 373 | "= =", 374 | "= = =", 375 | "= k = =", 376 | "= = =", 377 | "============" 378 | ], 379 | "doors": { 380 | "1": { 381 | "leadsTo": 16 382 | }, 383 | "2": { 384 | "leadsTo": 13 385 | } 386 | } 387 | }, 388 | { 389 | "layout": [ 390 | "============", 391 | "= =", 392 | "= =", 393 | "= =", 394 | "= =", 395 | "1 = @ 2", 396 | "= = =", 397 | "= = =", 398 | "= k= =", 399 | "= =", 400 | "=====3======" 401 | ], 402 | "doors": { 403 | "1": { 404 | "leadsTo": 17 405 | }, 406 | "2": { 407 | "leadsTo": 15 408 | }, 409 | "3": { 410 | "leadsTo": 20 411 | } 412 | } 413 | }, 414 | { 415 | "layout": [ 416 | "============", 417 | "= =", 418 | "= =", 419 | "= k =", 420 | "= = = = =", 421 | "1 = @ = 2", 422 | "= = = =", 423 | "= = = =", 424 | "= =", 425 | "= =", 426 | "============" 427 | ], 428 | "doors": { 429 | "1": { 430 | "leadsTo": 18 431 | }, 432 | "2": { 433 | "leadsTo": 16 434 | } 435 | } 436 | }, 437 | { 438 | "layout": [ 439 | "============", 440 | "= =", 441 | "= =", 442 | "= =", 443 | "= =", 444 | "= @= 1", 445 | "= =", 446 | "= =", 447 | "= ===k =", 448 | "= =", 449 | "=====2======" 450 | ], 451 | "doors": { 452 | "1": { 453 | "leadsTo": 17 454 | }, 455 | "2": { 456 | "leadsTo": 19, 457 | "keysRequired": 1 458 | } 459 | } 460 | }, 461 | { 462 | "layout": [ 463 | "=====1======", 464 | "= = =", 465 | "= = =", 466 | "= f =", 467 | "= =", 468 | "= @ =", 469 | "= =", 470 | "= ====== = =", 471 | "= k =", 472 | "= =", 473 | "============" 474 | ], 475 | "doors": { 476 | "1": { 477 | "leadsTo": 18 478 | } 479 | } 480 | }, 481 | { 482 | "layout": [ 483 | "=====1======", 484 | "= =", 485 | "= ==== =", 486 | "= =", 487 | "= =", 488 | "= @ =", 489 | "= =", 490 | "= =", 491 | "= ==== =", 492 | "= k=", 493 | "=====2======" 494 | ], 495 | "doors": { 496 | "1": { 497 | "leadsTo": 16 498 | }, 499 | "2": { 500 | "leadsTo": 21 501 | } 502 | } 503 | }, 504 | { 505 | "layout": [ 506 | "=====1======", 507 | "= =", 508 | "= ==== =", 509 | "= =", 510 | "= =", 511 | "= @ =", 512 | "= =", 513 | "= =", 514 | "= ==== k=", 515 | "= =", 516 | "======2=====" 517 | ], 518 | "doors": { 519 | "1": { 520 | "leadsTo": 20 521 | }, 522 | "2": { 523 | "leadsTo": 22 524 | } 525 | } 526 | }, 527 | { 528 | "layout": [ 529 | "=====1======", 530 | "= =", 531 | "= =", 532 | "= k =", 533 | "= =", 534 | "= =@= =", 535 | "= = =", 536 | "= =", 537 | "= =", 538 | "= =", 539 | "======2=====" 540 | ], 541 | "doors": { 542 | "1": { 543 | "leadsTo": 21 544 | }, 545 | "2": { 546 | "leadsTo": 23 547 | } 548 | } 549 | }, 550 | { 551 | "layout": [ 552 | "=====1======", 553 | "= =", 554 | "= =", 555 | "== =", 556 | "= =", 557 | "4 @ 2", 558 | "= =", 559 | "= =", 560 | "= = =", 561 | "= k =", 562 | "=====3======" 563 | ], 564 | "doors": { 565 | "1": { 566 | "leadsTo": 22 567 | }, 568 | "2": { 569 | "leadsTo": 24 570 | }, 571 | "3": { 572 | "leadsTo": 41 573 | }, 574 | "4": { 575 | "leadsTo": 39 576 | } 577 | } 578 | }, 579 | { 580 | "layout": [ 581 | "============", 582 | "= =", 583 | "= =", 584 | "= f =", 585 | "= fffff =", 586 | "1 @ f 2", 587 | "= f fff =", 588 | "= f k =", 589 | "= f =", 590 | "= =", 591 | "============" 592 | ], 593 | "doors": { 594 | "1": { 595 | "leadsTo": 23 596 | }, 597 | "2": { 598 | "leadsTo": 25 599 | } 600 | } 601 | }, 602 | { 603 | "layout": [ 604 | "============", 605 | "= k =", 606 | "= = =", 607 | "= = =", 608 | "= = =", 609 | "1 = @ 2", 610 | "= = =", 611 | "= = =", 612 | "= = =", 613 | "= =", 614 | "=====3======" 615 | ], 616 | "doors": { 617 | "1": { 618 | "leadsTo": 24 619 | }, 620 | "2": { 621 | "leadsTo": 26 622 | }, 623 | "3": { 624 | "leadsTo": 42 625 | } 626 | } 627 | }, 628 | { 629 | "layout": [ 630 | "============", 631 | "= =", 632 | "= == =", 633 | "= f =", 634 | "= = =", 635 | "1 @ 2", 636 | "= =", 637 | "= =", 638 | "= = =", 639 | "= k ==== =", 640 | "============" 641 | ], 642 | "doors": { 643 | "1": { 644 | "leadsTo": 25 645 | }, 646 | "2": { 647 | "leadsTo": 27 648 | } 649 | } 650 | }, 651 | { 652 | "layout": [ 653 | "=====1======", 654 | "= =", 655 | "= =", 656 | "= =", 657 | "= = =", 658 | "2 =k @ 3", 659 | "= = = =", 660 | "= =", 661 | "= =", 662 | "= =", 663 | "============" 664 | ], 665 | "doors": { 666 | "1": { 667 | "leadsTo": 28, 668 | "keysRequired": 2 669 | }, 670 | "2": { 671 | "leadsTo": 26 672 | }, 673 | "3": { 674 | "leadsTo": 29 675 | } 676 | } 677 | }, 678 | { 679 | "layout": [ 680 | "=====1======", 681 | "= = =", 682 | "= = =", 683 | "= k =", 684 | "= = =", 685 | "= @ =", 686 | "= = =", 687 | "= =", 688 | "= = =", 689 | "= =", 690 | "======2=====" 691 | ], 692 | "doors": { 693 | "1": { 694 | "leadsTo": 3, 695 | "keysRequired": 1 696 | }, 697 | "2": { 698 | "leadsTo": 27, 699 | "keysRequired": 2 700 | } 701 | } 702 | }, 703 | { 704 | "layout": [ 705 | "============", 706 | "= =", 707 | "= = =", 708 | "= = =", 709 | "= =", 710 | "1 @ 2", 711 | "= =", 712 | "= = =", 713 | "= = k=", 714 | "= f =", 715 | "=====3======" 716 | ], 717 | "doors": { 718 | "1": { 719 | "leadsTo": 27 720 | }, 721 | "2": { 722 | "leadsTo": 31 723 | }, 724 | "3": { 725 | "leadsTo": 30 726 | } 727 | } 728 | }, 729 | { 730 | "layout": [ 731 | "=====1======", 732 | "= =", 733 | "= =", 734 | "=k==========", 735 | "= =", 736 | "= @ =", 737 | "= =", 738 | "= =", 739 | "= =", 740 | "= =", 741 | "=====2======" 742 | ], 743 | "doors": { 744 | "1": { 745 | "leadsTo": 29 746 | }, 747 | "2": { 748 | "leadsTo": 36 749 | } 750 | } 751 | }, 752 | { 753 | "layout": [ 754 | "=====1======", 755 | "= = = k =", 756 | "= = = =", 757 | "= == =", 758 | "= =", 759 | "2 = @ 3", 760 | "= =", 761 | "= = =", 762 | "= = =", 763 | "= = =", 764 | "============" 765 | ], 766 | "doors": { 767 | "1": { 768 | "leadsTo": 0 769 | }, 770 | "2": { 771 | "leadsTo": 29 772 | }, 773 | "3": { 774 | "leadsTo": 32 775 | } 776 | } 777 | }, 778 | { 779 | "layout": [ 780 | "============", 781 | "= = k =", 782 | "= = = =", 783 | "= = = =", 784 | "= = =", 785 | "1 = @ = =", 786 | "= = = =", 787 | "= = =", 788 | "= = = =", 789 | "= =", 790 | "=====2======" 791 | ], 792 | "doors": { 793 | "1": { 794 | "leadsTo": 31 795 | }, 796 | "2": { 797 | "leadsTo": 33 798 | } 799 | } 800 | }, 801 | { 802 | "layout": [ 803 | "=====1======", 804 | "= =", 805 | "= ==========", 806 | "= =", 807 | "========== =", 808 | "= @ =", 809 | "= ==========", 810 | "= k =", 811 | "========== =", 812 | "= =", 813 | "=====2======" 814 | ], 815 | "doors": { 816 | "1": { 817 | "leadsTo": 32 818 | }, 819 | "2": { 820 | "leadsTo": 34 821 | } 822 | } 823 | }, 824 | { 825 | "layout": [ 826 | "=====1======", 827 | "= =", 828 | "= = = =", 829 | "= = f =", 830 | "= = = =", 831 | "2 @ =", 832 | "= =", 833 | "= = = =", 834 | "= = =", 835 | "= = = k =", 836 | "============" 837 | ], 838 | "doors": { 839 | "1": { 840 | "leadsTo": 33, 841 | "keysRequired": 1 842 | }, 843 | "2": { 844 | "leadsTo": 35 845 | } 846 | } 847 | }, 848 | { 849 | "layout": [ 850 | "============", 851 | "= = =", 852 | "= = =", 853 | "= k = =", 854 | "= = =", 855 | "1 = @ 2", 856 | "= = =", 857 | "= = =", 858 | "= = = =", 859 | "= = =", 860 | "============" 861 | ], 862 | "doors": { 863 | "1": { 864 | "leadsTo": 36 865 | }, 866 | "2": { 867 | "leadsTo": 34 868 | } 869 | } 870 | }, 871 | { 872 | "layout": [ 873 | "=====1======", 874 | "= =", 875 | "= = = =", 876 | "= = = =", 877 | "= == = =", 878 | "= =f@ = 2", 879 | "= ===== =", 880 | "= =", 881 | "= k =", 882 | "= =", 883 | "============" 884 | ], 885 | "doors": { 886 | "1": { 887 | "leadsTo": 30 888 | }, 889 | "2": { 890 | "leadsTo": 35 891 | } 892 | } 893 | }, 894 | { 895 | "layout": [ 896 | "============", 897 | "= =", 898 | "= =", 899 | "= = =", 900 | "= =", 901 | "1 = @ = 2", 902 | "= =", 903 | "= = =", 904 | "= f k =", 905 | "= =", 906 | "============" 907 | ], 908 | "doors": { 909 | "1": { 910 | "leadsTo": 7 911 | }, 912 | "2": { 913 | "leadsTo": 38, 914 | "keysRequired": 1 915 | } 916 | } 917 | }, 918 | { 919 | "layout": [ 920 | "============", 921 | "= =", 922 | "= = = =", 923 | "= =", 924 | "= =", 925 | "1 @ f =", 926 | "= =", 927 | "= k =", 928 | "= = = =", 929 | "= =", 930 | "============" 931 | ], 932 | "doors": { 933 | "1": { 934 | "leadsTo": 37 935 | } 936 | } 937 | }, 938 | { 939 | "layout": [ 940 | "============", 941 | "= =", 942 | "= = =", 943 | "= =", 944 | "= = =", 945 | "= @ k 1", 946 | "= = =", 947 | "= =", 948 | "= = =", 949 | "= =", 950 | "=====2======" 951 | ], 952 | "doors": { 953 | "1": { 954 | "leadsTo": 23, 955 | "keysRequired": 2 956 | }, 957 | "2": { 958 | "leadsTo": 40 959 | } 960 | } 961 | }, 962 | { 963 | "layout": [ 964 | "=====1======", 965 | "= =", 966 | "= k = =", 967 | "= =", 968 | "= = =", 969 | "= = @ 2", 970 | "= =", 971 | "= = =", 972 | "= =", 973 | "= = =", 974 | "============" 975 | ], 976 | "doors": { 977 | "1": { 978 | "leadsTo": 39 979 | }, 980 | "2": { 981 | "leadsTo": 41 982 | } 983 | } 984 | }, 985 | { 986 | "layout": [ 987 | "=====2======", 988 | "= =", 989 | "= f =", 990 | "= =", 991 | "= =", 992 | "1 @ =", 993 | "= =", 994 | "= =", 995 | "= k =", 996 | "= =", 997 | "============" 998 | ], 999 | "doors": { 1000 | "1": { 1001 | "leadsTo": 40 1002 | }, 1003 | "2": { 1004 | "leadsTo": 23 1005 | } 1006 | } 1007 | }, 1008 | { 1009 | "layout": [ 1010 | "======1=====", 1011 | "= =", 1012 | "= = = =", 1013 | "= = = =", 1014 | "= = = =", 1015 | "= @ =", 1016 | "= =", 1017 | "= =", 1018 | "= k =", 1019 | "= =", 1020 | "============" 1021 | ], 1022 | "doors": { 1023 | "1": { 1024 | "leadsTo": 25 1025 | } 1026 | } 1027 | } 1028 | ] 1029 | --------------------------------------------------------------------------------