#include "map.hpp" #include "dbc.hpp" #include #include #include #include "rand.hpp" #include using std::vector, std::pair; using namespace fmt; void dump_map(const std::string &msg, Matrix &map) { println("----------------- {}", msg); for(auto row : map) { for(auto col : row) { print("{} ", col); } print("\n"); } } inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) { size_t h = closed.size(); size_t w = closed[0].size(); vector rows{y - 1, y, y + 1}; vector cols{x - 1, x, x + 1}; for(size_t row : rows) { for(size_t col : cols) { if((0 <= row && row < h) && (0 <= col && col < w) && closed[row][col] == 0) { closed[row][col] = 1; neighbors.push_back({.x=col, .y=row}); } } } } /** * This will create an _inverted_ map that you * can run make_rooms and generate on. It will * NOT be valid until you actually run generate. */ Map::Map(size_t width, size_t height) : $limit(1000) { $walls = Matrix(height, MatrixRow(width, INV_WALL)); $input_map = Matrix(height, MatrixRow(width, 1)); } void Map::make_paths() { size_t h = $input_map.size(); size_t w = $input_map[0].size(); // Initialize the new array with every pixel at limit distance // NOTE: this is normally ones() * limit int limit = $limit == 0 ? h * w : $limit; Matrix new_arr = Matrix(h, MatrixRow(w, limit)); Matrix closed = $walls; PointList starting_pixels; PointList open_pixels; // First pass: Add starting pixels and put them in closed for(size_t counter = 0; counter < h * w; counter++) { size_t x = counter % w; size_t y = counter / w; if($input_map[y][x] == 0) { new_arr[y][x] = 0; closed[y][x] = 1; starting_pixels.push_back({.x=x,.y=y}); } } // Second pass: Add border to open for(auto sp : starting_pixels) { add_neighbors(open_pixels, closed, sp.y, sp.x); } // Third pass: Iterate filling in the open list int counter = 1; // leave this here so it's available below for(; counter < limit && !open_pixels.empty(); ++counter) { PointList next_open; for(auto sp : open_pixels) { new_arr[sp.y][sp.x] = counter; add_neighbors(next_open, closed, sp.y, sp.x); } open_pixels = next_open; } // Last pass: flood last pixels for(auto sp : open_pixels) { new_arr[sp.y][sp.x] = counter; } $paths = new_arr; } void Map::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) { dbc::pre("x out of bounds", origin_x < width()); dbc::pre("y out of bounds", origin_y < height()); dbc::pre("w out of bounds", w <= width()); dbc::pre("h out of bounds", h <= height()); for(size_t y = origin_y; y < origin_y + h; ++y) { dbc::check(y < $walls.size(), "y is out of bounds"); for(size_t x = origin_x; x < origin_x + w; ++x) { dbc::check(x < $walls[y].size(), "x is out of bounds"); $walls[y][x] = INV_SPACE; } } } inline int make_split(Room &cur, bool horiz) { size_t dimension = horiz ? cur.height : cur.width; int min = dimension / 4; int max = dimension - min; return Random::uniform(min, max); } void Map::partition_map(Room &cur, int depth) { if(cur.width >= 5 && cur.width <= 10 && cur.height >= 5 && cur.height <= 10) { $rooms.push_back(cur); return; } bool horiz = cur.width > cur.height ? false : true; int split = make_split(cur, horiz); Room left = cur; Room right = cur; if(horiz) { dbc::check(split > 0, "split is not > 0"); dbc::check(split < int(cur.height), "split is too big!"); left.height = size_t(split - 1); right.y = cur.y + split; right.height = size_t(cur.height - split); } else { dbc::check(split > 0, "split is not > 0"); dbc::check(split < int(cur.width), "split is too big!"); left.width = size_t(split-1); right.x = cur.x + split, right.width = size_t(cur.width - split); } if(depth > 0 && left.width > 5 && left.height > 5) { partition_map(left, depth-1); } if(depth > 0 && right.width > 5 && right.height > 5) { partition_map(right, depth-1); } } void Map::place_rooms(Room &cur) { for(auto &cur : $rooms) { cur.x += 2; cur.y += 2; cur.width -= 4; cur.height -= 4; add_door(cur); set_door(cur, INV_SPACE); make_room(cur.x, cur.y, cur.width, cur.height); } } bool Map::neighbors(Point &out, bool greater) { std::array dirs{{ {out.x,out.y-1}, {out.x+1,out.y}, {out.x,out.y+1}, {out.x-1,out.y} }}; int zero_i = -1; int cur = $paths[out.y][out.x]; // BUG: sometimes cur is in a wall so finding neighbors fails for(int i = 0; i < 4; ++i) { Point dir = dirs[i]; int diff = inmap(dir.x, dir.y) ? cur - $paths[dir.y][dir.x] : -1000; if(diff == 1) { out = {.x=dir.x, .y=dir.y}; return true; } else if(diff == 0) { zero_i = i; } } if(zero_i != -1) { out = {.x=dirs[zero_i].x, .y=dirs[zero_i].y}; return true; } else { return false; } } bool Map::inmap(size_t x, size_t y) { return x < width() && y < height(); } void Map::set_door(Room &room, int value) { $walls[room.entry.y][room.entry.x] = value; $walls[room.exit.y][room.exit.x] = value; } void rand_side(Room &room, Point &door) { int rand_x = Random::uniform(0, room.width - 1); int rand_y = Random::uniform(0, room.height - 1); switch(Random::uniform(0,3)) { case 0: // north door.x = room.x + rand_x; door.y = room.y-1; break; case 1: // south door.x = room.x + rand_x; door.y = room.y + room.height; break; case 2: // east door.x = room.x + room.width; door.y = room.y + rand_y; break; case 3: // west door.x = room.x - 1; door.y = room.y + rand_y; break; default: dbc::sentinel("impossible side"); } } void Map::add_door(Room &room) { rand_side(room, room.entry); rand_side(room, room.exit); } bool Map::walk(Point &src, Point &target) { // this sets the target for the path dbc::check($input_map[target.y][target.x] == 0, "target point not set to 0"); $walls[src.y][src.x] = INV_WALL; $walls[target.y][target.x] = INV_WALL; // for the walk this needs to be walls since it's inverted? dbc::check($walls[src.y][src.x] == INV_WALL, "src room has a wall at exit door"); dbc::check($walls[target.y][target.x] == INV_WALL, "target room has a wall at entry door"); make_paths(); bool found = false; Point out{src.x, src.y}; int count = 0; do { $walls[out.y][out.x] = INV_SPACE; found = neighbors(out, true); if($paths[out.y][out.x] == 0) { $walls[out.y][out.x] = INV_SPACE; return true; } } while(found && out.x > 0 && out.y > 0 && ++count < 100); return false; } void Map::set_target(Point &at, int value) { $input_map[at.y][at.x] = 0; } void Map::clear_target(Point &at) { $input_map[at.y][at.x] = 1; } Point Map::place_entity(size_t room_index) { dbc::check(room_index < $rooms.size(), "room_index is out of bounds, not enough rooms"); Room &start = $rooms[room_index]; return {start.x+1, start.y+1}; } void Map::generate() { Room root{ .x = 0, .y = 0, .width = width(), .height = height() }; partition_map(root, 10); place_rooms(root); for(size_t i = 0; i < $rooms.size() - 1; i++) { Room &src = $rooms[i]; Room &target = $rooms[i+1]; set_target(target.entry); bool found = walk(src.exit, target.entry); if(!found) { println("ROOM NOT FOUND!"); } clear_target(target.entry); } Room &src = $rooms.back(); Room &target = $rooms.front(); set_target(target.entry); walk(src.exit, target.entry); clear_target(target.entry); for(size_t y = 0; y < height(); ++y) { for(size_t x = 0; x < width(); ++x) { $walls[y][x] = !$walls[y][x]; } } } bool Map::iswall(size_t x, size_t y) { return $walls[y][x] == WALL_VALUE; } void Map::dump() { dump_map("PATHS", $paths); dump_map("WALLS", $walls); dump_map("INPUT", $input_map); }