This script will generate roguelike maps in the form of a 2D array of objects. They have been built in such a way to easily be used for generating 3D maps with later. Here is an example of a generated map:
Like many ambitious projects, however, it was never completed. Not wanting to let good code go to waste, I figured this project would make a great open-source candidate.
What is Generated
Each time a map is generated, it is called a sector. A sector contains a grid of possible rooms. Rooms can be large or small, taking up different numbers of grid spaces. Rooms are connected to other rooms via doors.
Individual squares in the grid are referred to as zones.
SECTOR_WIDTH: The grid width of the level SECTOR_HEIGHT: The grid height of the level MIN_ZONES_PER_ROOM: The minimum number of blocks a room should contain MAX_ZONES_PER_ROOM: The maximum number of blocks a room should contain MIN_ROOMS_PER_SECTOR: The minimum number of rooms the whole map should have MAX_ROOMS_PER_SECTOR: The maximum number of rooms the whole map should have MIN_SECRET_ROOMS: This feature was never implemented NEW_DOOR_MIN_THRESHOLD: The minimum number of extra doors to add. See "How it Works" ROOM_ID_DIFF_RANDOM_DOOR_THRESHOLD: How far apart the room id's should be. See "How it Works" ROOM_ID_DIFF_RANDOM_DOOR_ODDS: What are the odds that we'll build an extra new door
How it Works
Zones snake out from the center in random directions. Rooms are created which contain several zones. Each time a room is created a door between the new and old is created. When the rooms are done, we randomly add more doors to the level isn't too linear.
The first thing we do is generate our grid of empty zones in the format of a big array of arrays (think of it as a 2D array). A zone is one item in the grid. The size of the grid is based on the configuration settings
Each zone contains some information about it. The attribute
open means that this is an open area, and a player or enemy could exist in this area.
room_id is a unique identifier for the room. This number starts from 0 and increments. When you look at the screenshot, the
render() function begins by making low ID rooms orange, and slowly fades to blue. This is just to make visualisation easier and see how rooms connect (and visualize how far of a traveling path the player has to make). All zones within a room will have the same
is_secret was never implemented, but would specify whether or not the sector is part of a secret 1x1 room.
is_airlock tells of if that room contains an airlock, which is a special door which in theory could link to other sectors.
zone_id is a unique number for each zone, and also increments from 0.
edges attribute contains information about the four edges of the zone, being
w. Each of these edges can be either a
OPEN, or an
AIRLOCK. The door is a method for passing between rooms. A wall will separate this zone from either another room or an area that cannot be occupied. The airlock allows the player to go to another sector. And open means that there is nothing there, e.g., it connects to another zone within the same room.
This function begins the real work for building out the sector.
The first thing we do is create a loop for the number of rooms we're going to build. Then, for each room, we loop for the number of zones we're going to build in this room. Finally we start to snake outwards from the center of the map building these sectors. We randomly snake outward, doing our best to not smack into another open sector. If we paint ourselves into a corner, we start over.
When a room is considered to be finished, we take a sector next to it and start building the next sector. When we do this, we create a door between this room and the next.
Also, once we've completed creating all of the rooms, we then go back through and look at all neighboring zones, where we check if this zone is next to a zone of a different room. If the two
room_id values are different enough, aka at least
ROOM_ID_DIFF_RANDOM_DOOR_THRESHOLD, we will consider building a door there. This value should be at least 2. Since there is always a door between rooms with an id difference of 1 (remember, we build doors when building new rooms), it would be redundant to add another door.
Now, I said we would consider adding a door. We roll the dice and check it against the
ROOM_ID_DIFF_RANDOM_DOOR_ODDS value, and if it passes, we then add the door. If we don't end up adding at least
NEW_DOOR_MIN_THRESHOLD doors using this method, we throw away the map and start over. The reason these special doors need to be added is that if we left them out, the level would be incredibly linear and boring.
Each time we created a new zone, we checked its position in the grid. If it were the most extreme zone in a particular direction, we decided it would become an airlock. After the rooms are done, we take those most extreme zones and make them an airlock in the applicable direction.
This is a simple function to provide a minimum integer and a maximum integer, and get an integer returned within that range, inclusive.
This moves our zone-building cursor for us. It will randomly move the cursor in one of the cardinal directions and return that new position if it is available. If it isn't available, it will try another direction at random. If all four directions are taken, it gives up and the map is built over. If it moves out of bounds, it also gives up.
This checks adjacent zones. If the adjacent zone is another room or nothingness, it builds a wall.
This is used for finding an open area adjacent to a particular zone. It is used when creating new rooms.
This is used for creating doors between two zones of differing rooms. It adds a door edge to the opposite directions of the two zones.
This function randomizes the order of an array.
render() and exporter()
These are used for rendering the map object into a canvas element that can be easily viewed. The exporter function will make a PNG that you can save.
The renderer doesn't create a playable game, so it is just for convenience and visualizing what you've created. For you to use this code to create a playable game, you'll probably throw these two functions away.
Darkness represents areas that can't be occupied. Blue through orange represents areas that can be occupied. Red lines are doors, grey lines are walls, green lines are airlocks.
Sample Wasted Generations
Here's an example of the wasted room generation. When you see
CURSOR STUCK, it means the cursor, which snakes out from the center of the map, snaked into itself. When you see
CURSOR OUT OF BOUNDS, it means the cursor smacked into the edge of the map boundary. When you see
UNMET DOOR THRESHOLD, it means we didn't build enough doors between rooms.
CURSOR STUCK. Rebuild... CURSOR OUT OF BOUNDS. Rebuild... CURSOR STUCK. Rebuild... CURSOR OUT OF BOUNDS. Rebuild... CURSOR OUT OF BOUNDS. Rebuild... CURSOR STUCK. Rebuild... UNMET DOOR THRESHOLD: 0 OF 3. Rebuild... UNMET DOOR THRESHOLD: 0 OF 3. Rebuild... CURSOR STUCK. Rebuild... CURSOR OUT OF BOUNDS. Rebuild... CURSOR STUCK. Rebuild... CURSOR STUCK. Rebuild...
This is the result of the
render() function I have in the code. Other than this function, the code need-not execute in the browser. I actually began this project in Node, but switched to the browser when I needed a good way to visualize the created level.
When creating a project with this, the created level object could be used to place tiles onto a canvas or do some Three.js 3D renderings. The sky is the limit really.