engine/ext/scenes/worldscape/terrain/maze.cpp
2020-09-26 00:00:00 -05:00

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;
}
}