436 lines
17 KiB
C++
436 lines
17 KiB
C++
#include "Maze.h"
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <set>
|
|
#include <tuple>
|
|
#include <map>
|
|
#include <random>
|
|
#include <time.h>
|
|
#include <numeric>
|
|
|
|
void ext::Maze::initialize(int columns, int rows, int floors, double horizontal_bias, double vertical_bias) {
|
|
/* The Maze class contructor
|
|
* Input:
|
|
* int columns - Set the number of columns in the maze. Correspond to the X axis (length).
|
|
* int rows - Set the number of rows in the maze. Correspond to the Y axis (width).
|
|
* int floors - Set the number of floors in the maze. Correspond to the Z axis (height).
|
|
*
|
|
* All three must be larger than 1.
|
|
*
|
|
* double horizontal_bias - Sets the likelihood of a passage between rooms on the same row.
|
|
* double vertical_bias - Sets the likelihood of a passage between rooms in the same column.
|
|
*
|
|
* Both must be between 0 and 1 exclusive.
|
|
* Higher number gives higher likelihood of a passage.
|
|
*
|
|
* Output:
|
|
* Maze object
|
|
*/
|
|
if (rows < 1 || columns < 1 || floors < 1){
|
|
std::cout << "A maze must have dimensions greater than zero.\n";
|
|
throw std::invalid_argument("A maze must have dimensions greater than zero.\n");
|
|
}
|
|
LENGTH = columns;
|
|
WIDTH = rows;
|
|
HEIGHT = floors;
|
|
|
|
if (horizontal_bias <= 0 || horizontal_bias >= 1 || vertical_bias <= 0 || vertical_bias >= 1){
|
|
std::cout << "Biases must be between 0 and 1 exclusive.\n";
|
|
throw std::invalid_argument("Biases must be between 0 and 1 exclusive.\n");
|
|
}
|
|
EAST_WALL_THRESHOLD = horizontal_bias;
|
|
SOUTH_WALL_THRESHOLD = vertical_bias;
|
|
|
|
//Creating a vector of all rooms.
|
|
cells.assign(LENGTH*WIDTH*HEIGHT, -1);
|
|
|
|
// seed
|
|
seed = time(NULL);
|
|
|
|
// Give each room a different set number (ascending).
|
|
// (Which is used during maze generation)
|
|
std::iota(std::begin(cells), std::end(cells), 0);
|
|
};
|
|
|
|
int ext::Maze::get(int i, int col, int floor){
|
|
// Gets the value stored at row i, column j, level k, in cells;
|
|
return cells.at(col + LENGTH*i + LENGTH*WIDTH*floor);
|
|
}
|
|
|
|
void ext::Maze::set(int row, int col, int floor, int val){
|
|
// Set the value of cell at (row, col, floor) to val
|
|
cells[col + LENGTH*row + LENGTH*WIDTH*floor] = val;
|
|
return;
|
|
}
|
|
|
|
void ext::Maze::build(){
|
|
/* This function generates a maze.
|
|
*
|
|
* It uses sets to keep track of areas created by removing walls.
|
|
* Initially each room has it's own set, known by it's room value.
|
|
* The room sets grows as walls are removed and sets joined.
|
|
* To keep track, each room value is mapped to itself, or the room value it has joined.
|
|
* The mapping and sets are reset at each floor.
|
|
*
|
|
* Summary:
|
|
* It goes through each cell, removing the walls randomly.
|
|
* (First WEST to EAST, then NORTH to SOUTH, then DOWN to UP.
|
|
* Just as one write in English, putting each new page on top of the other)
|
|
* If a wall is between two rooms in the same set, it is not removed.
|
|
*
|
|
* Each room value denote which set it belongs to
|
|
*
|
|
* Each wall removed is stored in as a passage between the two rooms.
|
|
* (The rooms position is stored in ext::Maze::passages)
|
|
*
|
|
* If the current room value is not mapped, then map it to itself.
|
|
* Set current room value to it's mapped value.
|
|
*
|
|
* Add the room to the set given by room value
|
|
*
|
|
* If a EASTERN wall is removed, then:
|
|
* - The set of the connected room is added to the set of this room.
|
|
* - The set of the connected room is deleted.
|
|
* - Map the connected room value to the current room value.
|
|
* - Change the connected room value to the current room value.
|
|
* - Add connected room to set.
|
|
*
|
|
* If a SOUTHERN wall is removed, then:
|
|
* - Change the connected room value to the current room value.
|
|
* - Add the connected room to set.
|
|
*
|
|
* After all the rooms of the current floor has been visited:
|
|
* - Go through each set, pick a random room, and make a passage up.
|
|
* - Clear the sets, and mapping.
|
|
*
|
|
* On the last floor, each set on each row must have a passage south.
|
|
*
|
|
* On the last row on the last floor, passages between unconnected sets are removed.
|
|
*
|
|
* Finally each room value is set according to which walls remain. (Including floor and ceiling)
|
|
*/
|
|
|
|
|
|
// The room sets
|
|
std::map<std::size_t, std::vector<pos>> room_set;
|
|
|
|
// The mapping of connected rooms
|
|
std::map<int, int> merged_room_sets;
|
|
|
|
|
|
int room_value;
|
|
int east_room_value;
|
|
int south_room_value;
|
|
|
|
// To get different mazes each time
|
|
srand(seed);
|
|
|
|
for (int floor = 0; floor < HEIGHT - 1; floor++){ // Go through each floor but the last.
|
|
|
|
for (int row = 0; row < WIDTH; row++){ // Go through each row
|
|
|
|
for(int col = 0; col < LENGTH; col++){ // Go through each element in row (Each column)
|
|
|
|
room_value = get(row, col, floor);
|
|
|
|
// If this room value is mapped to another, get that value: else map value to itself
|
|
if(merged_room_sets.find(room_value) != merged_room_sets.end()){
|
|
room_value = merged_room_sets[room_value];
|
|
} else {
|
|
merged_room_sets[room_value] = room_value;
|
|
}
|
|
|
|
// If not the last cell in row, store eastern room value
|
|
if (col<LENGTH-1){
|
|
//Is the eastern room value mapped?
|
|
east_room_value = (merged_room_sets.find(get(row, col+1, floor))!=merged_room_sets.end()) ?
|
|
// Yes: Use mapped value
|
|
merged_room_sets[get(row, col+1, floor)] :
|
|
// No: Use stored value
|
|
get(row, col+1, floor);
|
|
}
|
|
|
|
// If not the last row, store southern room value
|
|
if (row < WIDTH-1){
|
|
//Is the southern room value mapped?
|
|
south_room_value = (merged_room_sets.find(get(row + 1, col, floor))!=merged_room_sets.end()) ?
|
|
|
|
// Yes: Use mapped value
|
|
merged_room_sets[get(row + 1, col, floor)] :
|
|
|
|
// No: Use stored value
|
|
get(row + 1, col, floor);
|
|
}
|
|
|
|
// Add this room to the set of room value
|
|
room_set[room_value].push_back(std::make_tuple(row, col, floor));
|
|
|
|
//If last room in row, skip (don't remove eastern boundary wall)
|
|
if(col == LENGTH - 1){}
|
|
|
|
// If this and eastern room not in same set, maybe remove wall
|
|
else if((rand() < EAST_WALL_THRESHOLD * RAND_MAX) && (room_value != east_room_value)){
|
|
// EASTERN WALL REMOVED
|
|
|
|
// Get set of eastern room
|
|
std::vector<pos> next_room_position_set = room_set[get(row, col+1, floor)];
|
|
|
|
// Add set of eastern room to this set.
|
|
room_set[room_value].insert(room_set[room_value].end(), next_room_position_set.begin(), next_room_position_set.end() );
|
|
|
|
// Remove eastern room set
|
|
room_set.erase(get(row, col+1, floor));
|
|
|
|
// Map eastern room value to this room value.
|
|
merged_room_sets[get(row,col+1,floor)] = room_value;
|
|
|
|
// Set eastern room value to this
|
|
set(row, col+1, floor, room_value);
|
|
|
|
// Add the new horizontal passage created.
|
|
passages.push_back(std::make_tuple(row, col, floor, row, col+1, floor));
|
|
|
|
} // Else the eastern wall remain
|
|
|
|
// Don't remove southern boundary wall.
|
|
if(row == WIDTH - 1){}
|
|
|
|
// If current and southern room is not in same set, then maybe remove wall
|
|
else if(rand() < SOUTH_WALL_THRESHOLD * RAND_MAX && room_value != south_room_value){
|
|
|
|
// Set southern room value to this one
|
|
set(row+1, col, floor, room_value);
|
|
|
|
// Add the newly created vertical passage.
|
|
passages.push_back(std::make_tuple(row, col, floor, row+1, col, floor));
|
|
} //Else wall down remain
|
|
|
|
} // All rooms in this row has been visited
|
|
|
|
} // All rows on this floor has been visited
|
|
|
|
// Clear room set mapping
|
|
merged_room_sets.clear();
|
|
|
|
// Go through all sets this floor
|
|
for(auto entry : room_set)
|
|
{
|
|
auto group = entry.second;
|
|
int x, y, z;
|
|
|
|
// Picking out a random room in each group
|
|
// Room position saved in matrix notation (i, j, k = y, x, z)
|
|
std::tie(y, x, z) = group.at(rand() % group.size());
|
|
|
|
// Only one passage up for each group. To avoid graph cycles
|
|
passages.push_back(std::make_tuple(y, x, z, y, x, z+1));
|
|
}
|
|
// Passages up have been set. Clearing room groups for next floor.
|
|
room_set.clear();
|
|
|
|
}// Finished with all but last floor
|
|
|
|
|
|
int floor = HEIGHT - 1;
|
|
|
|
std::set<int> can_go_south;
|
|
|
|
//deal with last floor, but leave the last row
|
|
for(int row = 0; row < WIDTH - 1; row++){
|
|
|
|
for(int col = 0; col<LENGTH; col++){
|
|
room_value = get(row, col, floor);
|
|
|
|
// If this room value is mapped to another, get that value: else map value to itself
|
|
if(merged_room_sets.find(room_value) != merged_room_sets.end()){
|
|
room_value = merged_room_sets[room_value];
|
|
} else {
|
|
merged_room_sets[room_value] = room_value;
|
|
}
|
|
|
|
// If not the last cell in row, get eastern room value
|
|
if (col<LENGTH-1){
|
|
east_room_value = (merged_room_sets.find(get(row, col+1, floor)) != merged_room_sets.end()) ?
|
|
merged_room_sets[get(row, col+1, floor)] : get(row, col+1, floor);
|
|
}
|
|
// Get southern room value
|
|
south_room_value = (merged_room_sets.find(get(row + 1, col, floor)) != merged_room_sets.end()) ?
|
|
merged_room_sets[get(row + 1, col, floor)] : get(row + 1, col, floor);
|
|
|
|
// Try and make passage east
|
|
if(col == LENGTH -1){}
|
|
else if((rand() < EAST_WALL_THRESHOLD * RAND_MAX) && (room_value != east_room_value)){
|
|
|
|
passages.push_back(std::make_tuple(row, col, floor, row, col+1, floor));
|
|
merged_room_sets[east_room_value] = room_value;
|
|
set(row, col+1, floor, room_value);
|
|
}
|
|
|
|
// Try and make passage south
|
|
if( ( (rand() < SOUTH_WALL_THRESHOLD * RAND_MAX)
|
|
|| (can_go_south.find(room_value) == can_go_south.end()) ) // Make sure all sets this row have a passage down
|
|
&& (room_value != south_room_value))
|
|
{
|
|
passages.push_back(std::make_tuple(row, col, floor, row+1, col, floor));
|
|
merged_room_sets[south_room_value] = room_value;
|
|
set(row+1, col, floor, room_value);
|
|
can_go_south.insert(room_value);
|
|
}
|
|
}
|
|
|
|
can_go_south.clear();
|
|
}
|
|
|
|
// Go through the last row on the last floor
|
|
int row = WIDTH - 1;
|
|
for (int col = 0; col < LENGTH - 1; col++)
|
|
{
|
|
// Get room values
|
|
room_value = get(row, col, floor);
|
|
|
|
if(merged_room_sets.find(room_value) != merged_room_sets.end()){
|
|
room_value = merged_room_sets[room_value];
|
|
} else {
|
|
merged_room_sets[room_value] = room_value;
|
|
}
|
|
|
|
// Get eastern room value
|
|
east_room_value = (merged_room_sets.find(get(row, col+1, floor))!=merged_room_sets.end()) ?
|
|
merged_room_sets[get(row, col+1, floor)] : get(row, col+1, floor);
|
|
|
|
// If this and eastern room is in different sets
|
|
if(room_value != east_room_value)
|
|
{
|
|
// Make passage and merge sets
|
|
passages.push_back(std::make_tuple(row, col, floor, row, col+1, floor));
|
|
merged_room_sets[east_room_value] = room_value;
|
|
set(row, col+1, floor, room_value);
|
|
}
|
|
}
|
|
|
|
// Calculate the room values based on remaining walls (and floors & ceilings)
|
|
ext::Maze::calculate();
|
|
};
|
|
|
|
void ext::Maze::calculate(){
|
|
/* This function set each room value according to the remaining walls in that room.
|
|
* The walls are represented as bits of a 8-bit int.
|
|
* Floor = 0000 0001
|
|
* Eastern wall = 0000 0010
|
|
* Northern wall = 0000 0100
|
|
* Western wall = 0000 1000
|
|
* Southern wall = 0001 0000
|
|
* Ceiling = 0010 0000
|
|
*/
|
|
|
|
cells.clear();
|
|
|
|
// 63 = 0011 1111 = all walls, floor, and ceiling
|
|
cells.assign(LENGTH*WIDTH*HEIGHT, 63);
|
|
|
|
int row_from, row_to,
|
|
col_from, col_to,
|
|
lvl_from, lvl_to;
|
|
|
|
uint8_t cell_num_to;
|
|
uint8_t cell_num_from;
|
|
|
|
// Go through all passages
|
|
for(auto pass : passages){
|
|
std::tie(row_from, col_from, lvl_from,
|
|
row_to, col_to, lvl_to ) = pass;
|
|
|
|
cell_num_from = get(row_from, col_from, lvl_from);
|
|
cell_num_to = get(row_to, col_to, lvl_to);
|
|
|
|
// North to South passage
|
|
if(row_from != row_to){
|
|
set(row_from, col_from, lvl_from, cell_num_from & ~(SOUTH));
|
|
set(row_to, col_to, lvl_to, cell_num_to & ~(NORTH));
|
|
|
|
// West to East passage
|
|
} else if (col_from != col_to){
|
|
set(row_from, col_from, lvl_from, cell_num_from & ~(EAST));
|
|
set(row_to, col_to, lvl_to, cell_num_to & ~(WEST));
|
|
|
|
// Passage Up
|
|
} else if(lvl_from != lvl_to){
|
|
set(row_from, col_from, lvl_from, cell_num_from & ~(CEIL));
|
|
set(row_to, col_to, lvl_to, cell_num_to & ~(FLOOR));
|
|
}
|
|
// Else the passage is in several directions and in this case discarded
|
|
}
|
|
}
|
|
|
|
void ext::Maze::print(){
|
|
/* Print the maze to std::cout
|
|
*
|
|
* Prints floor in ascending order (top floor last)
|
|
* | - represent vertical and horizontal walls respectively
|
|
* + is the room corners
|
|
* U D B represent stairs up, down, and both, respectively
|
|
*
|
|
* ext::Maze::passages s used, so remember to run ext::Maze::build first.
|
|
*/
|
|
|
|
// Maze map canvas
|
|
std::string map_drawing[2 * WIDTH + 1][HEIGHT];
|
|
|
|
std::string wall_row = "+";
|
|
std::string room_row = "|";
|
|
|
|
// Make a row of walls and rooms
|
|
for (int col = 0; col < LENGTH; col++){
|
|
wall_row.append("-+");
|
|
room_row.append(" |");
|
|
}
|
|
// Drawing initial map
|
|
for (int floor = 0; floor < HEIGHT; floor++){
|
|
map_drawing[2*WIDTH][floor] = wall_row;
|
|
|
|
for (int row = 0; row < WIDTH; row++){
|
|
map_drawing[2*row][floor] = wall_row;
|
|
map_drawing[2*row + 1][floor] = room_row;
|
|
}
|
|
}
|
|
|
|
// Making passages
|
|
for ( auto pass : passages){
|
|
int row_from, row_to,
|
|
col_from, col_to,
|
|
lvl_from, lvl_to;
|
|
|
|
std::tie(row_from, col_from, lvl_from,
|
|
row_to, col_to, lvl_to) = pass;
|
|
|
|
// North - South passage
|
|
if(row_from != row_to){
|
|
map_drawing[2 * row_from + 2][lvl_from].replace(2 * col_from + 1, 1, " ");
|
|
|
|
// West - East passage
|
|
} else if (col_from != col_to){
|
|
map_drawing[2*row_from + 1][lvl_from].replace(2 * col_from + 2, 1, " ");
|
|
|
|
// Up - Down passage
|
|
} else if (lvl_from != lvl_to){
|
|
|
|
// If this room have a passage down, you can go up and down from it
|
|
if (map_drawing[2 * row_from + 1][lvl_from].at(2 * col_from + 1) == 'D'){
|
|
map_drawing[2 * row_from + 1][lvl_from].replace(2 * col_from + 1, 1, "B");
|
|
} else {
|
|
map_drawing[2 * row_from + 1][lvl_from].replace(2 * col_from + 1, 1, "U");
|
|
}
|
|
|
|
// The passages should come in order, so floor above have no passages up
|
|
map_drawing[2 * row_to + 1][lvl_to].replace(2 * col_to + 1, 1, "D");
|
|
}
|
|
}
|
|
|
|
// Printing the maze, one row at a time.
|
|
for (int floor = 0; floor < HEIGHT; floor++){
|
|
for (int row = 0; row < 2*WIDTH+1; row++){
|
|
std::cout << map_drawing[row][floor] << std::endl;
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
} |