├── include ├── Vector2.h ├── Domain.h ├── RandomGenerator.h ├── CornerConstraint.h ├── Direction.h ├── Constraint.h ├── DistanceToAnyWallConstraint.h ├── NextToDoorConstraint.h ├── DistanceToBorderConstraint.h ├── DistanceToWallConstraint.h ├── Variable.h ├── Box.h ├── Solver.h ├── Array2.h └── Grid.h ├── examples ├── CMakeLists.txt └── main.cpp ├── src ├── CornerConstraint.cpp ├── DistanceToAnyWallConstraint.cpp ├── DistanceToBorderConstraint.cpp ├── DistanceToWallConstraint.cpp ├── NextToDoorConstraint.cpp ├── Solver.cpp └── Grid.cpp ├── README.md ├── LICENSE └── CMakeLists.txt /include/Vector2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace room 4 | { 5 | 6 | struct Vector2i 7 | { 8 | int x = 0; 9 | int y = 0; 10 | }; 11 | 12 | } -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(example main.cpp) 2 | target_link_libraries(example PRIVATE room_generator) 3 | setWarnings(example) 4 | setStandard(example) -------------------------------------------------------------------------------- /include/Domain.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Vector2.h" 5 | 6 | namespace room 7 | { 8 | 9 | using Domain = std::vector; 10 | 11 | } -------------------------------------------------------------------------------- /include/RandomGenerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace room 6 | { 7 | 8 | // Use a better random generator in production 9 | using RandomGenerator = std::mt19937; 10 | 11 | } -------------------------------------------------------------------------------- /include/CornerConstraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Constraint.h" 4 | 5 | namespace room 6 | { 7 | 8 | struct CornerConstraint : public Constraint 9 | { 10 | void constrain(const Grid& grid, Variable& variable) final; 11 | }; 12 | 13 | } -------------------------------------------------------------------------------- /include/Direction.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace room 6 | { 7 | 8 | enum class Direction : uint8_t 9 | { 10 | North, 11 | West, 12 | South, 13 | East, 14 | }; 15 | 16 | inline constexpr auto DirectionCount = 4; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /include/Constraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace room 4 | { 5 | 6 | class Grid; 7 | struct Variable; 8 | 9 | struct Constraint 10 | { 11 | virtual ~Constraint() = default; 12 | 13 | virtual void constrain(const Grid& grid, Variable& variable) = 0; 14 | }; 15 | 16 | } -------------------------------------------------------------------------------- /include/DistanceToAnyWallConstraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Constraint.h" 5 | 6 | namespace room 7 | { 8 | 9 | struct DistanceToAnyWallConstraint : public Constraint 10 | { 11 | uint32_t distance = 0; 12 | 13 | void constrain(const Grid& grid, Variable& variable) final; 14 | }; 15 | 16 | } -------------------------------------------------------------------------------- /include/NextToDoorConstraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Constraint.h" 4 | #include "Direction.h" 5 | 6 | namespace room 7 | { 8 | 9 | struct NextToDoorConstraint : public Constraint 10 | { 11 | Direction side = Direction::North; 12 | 13 | void constrain(const Grid& grid, Variable& variable) final; 14 | }; 15 | 16 | } -------------------------------------------------------------------------------- /include/DistanceToBorderConstraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Constraint.h" 4 | #include "Direction.h" 5 | 6 | namespace room 7 | { 8 | 9 | struct DistanceToBorderConstraint : public Constraint 10 | { 11 | Direction direction = Direction::North; 12 | int distance = 0; 13 | 14 | void constrain(const Grid& grid, Variable& variable) final; 15 | }; 16 | 17 | } -------------------------------------------------------------------------------- /include/DistanceToWallConstraint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Constraint.h" 4 | #include "Direction.h" 5 | 6 | namespace room 7 | { 8 | 9 | struct DistanceToWallConstraint : public Constraint 10 | { 11 | Direction direction = Direction::North; 12 | uint32_t distance = 0; 13 | 14 | void constrain(const Grid& grid, Variable& variable) final; 15 | }; 16 | 17 | } -------------------------------------------------------------------------------- /src/CornerConstraint.cpp: -------------------------------------------------------------------------------- 1 | #include "CornerConstraint.h" 2 | #include 3 | #include "Grid.h" 4 | #include "Variable.h" 5 | 6 | namespace room 7 | { 8 | 9 | void CornerConstraint::constrain(const Grid& grid, Variable& variable) 10 | { 11 | auto box = variable.collisionBox.getUnion(variable.marginBox); 12 | std::erase_if(variable.domain, [&grid, &box](const auto& value) 13 | { 14 | return !grid.isInCorner(value.y, value.x, box); 15 | }); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /include/Variable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "Array2.h" 7 | #include "Box.h" 8 | #include "Constraint.h" 9 | #include "Domain.h" 10 | 11 | namespace room 12 | { 13 | 14 | struct Constraint; 15 | 16 | struct Variable 17 | { 18 | Box collisionBox; 19 | Box marginBox; 20 | const std::vector>* constraints = nullptr; 21 | Domain domain = {}; 22 | int priority = 0; 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Room Generator 2 | 3 | Demo code that accompanies my talk "Room Generation using Constraint Satisfaction" at [Roguelike Celebration 2022](https://www.roguelike.club/). 4 | 5 | You can [watch the talk](https://youtu.be/oVhq8V93gHM) on YouTube, [get the slides](https://docs.google.com/presentation/d/1lECom7pLqrKIiVtetD_KEZHAtFeXWwcMqCItoteSEQ4/edit?usp=sharing) or [read the article](https://pvigier.github.io/2022/11/05/room-generation-using-constraint-satisfaction.html) that transcribes the talk on my blog. 6 | 7 | ## License 8 | 9 | Distributed under the MIT License. 10 | -------------------------------------------------------------------------------- /include/Box.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | namespace room 5 | { 6 | 7 | struct Box 8 | { 9 | int left = 0; 10 | int top = 0; 11 | int width = 0; 12 | int height = 0; 13 | 14 | int getRight() const 15 | { 16 | return left + width; 17 | } 18 | 19 | int getBottom() const 20 | { 21 | return top + height; 22 | } 23 | 24 | Box getUnion(const Box& rhs) const 25 | { 26 | auto xMin = std::min(left, rhs.left); 27 | auto yMin = std::min(top, rhs.top); 28 | auto xMax = std::max(getRight(), rhs.getRight()); 29 | auto yMax = std::max(getBottom(), rhs.getBottom()); 30 | return Box{xMin, yMin, xMax - xMin, yMax - yMin}; 31 | } 32 | }; 33 | 34 | } -------------------------------------------------------------------------------- /src/DistanceToAnyWallConstraint.cpp: -------------------------------------------------------------------------------- 1 | #include "DistanceToAnyWallConstraint.h" 2 | #include 3 | #include "Grid.h" 4 | #include "Variable.h" 5 | 6 | namespace room 7 | { 8 | 9 | void DistanceToAnyWallConstraint::constrain(const Grid& grid, Variable& variable) 10 | { 11 | auto box = variable.collisionBox.getUnion(variable.marginBox); 12 | std::erase_if(variable.domain, [this, &grid, &box](const auto& value) 13 | { 14 | return !grid.isDistantToWall(value.y, value.x, box, distance) && 15 | !grid.isDistantToWall(value.y, value.x, box, distance) && 16 | !grid.isDistantToWall(value.y, value.x, box, distance) && 17 | !grid.isDistantToWall(value.y, value.x, box, distance); 18 | }); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/DistanceToBorderConstraint.cpp: -------------------------------------------------------------------------------- 1 | #include "DistanceToBorderConstraint.h" 2 | #include 3 | #include "Grid.h" 4 | #include "Variable.h" 5 | 6 | namespace room 7 | { 8 | 9 | void DistanceToBorderConstraint::constrain(const Grid& grid, Variable& variable) 10 | { 11 | auto box = variable.collisionBox.getUnion(variable.marginBox); 12 | if (direction == Direction::North || direction == Direction::South) 13 | { 14 | auto i = direction == Direction::North ? 15 | distance - box.top : 16 | static_cast(grid.getHeight()) - box.getBottom() - distance; 17 | std::erase_if(variable.domain, [i](const auto& value){ return value.y != i; }); 18 | } 19 | else 20 | { 21 | auto j = direction == Direction::West ? 22 | distance - box.left : 23 | static_cast(grid.getWidth()) - box.getRight() - distance; 24 | std::erase_if(variable.domain, [j](const auto& value){ return value.x != j; }); 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pierre Vigier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /include/Solver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Array2.h" 6 | #include "Grid.h" 7 | #include "RandomGenerator.h" 8 | #include "Variable.h" 9 | 10 | namespace room 11 | { 12 | 13 | class Solver 14 | { 15 | public: 16 | struct Solution 17 | { 18 | std::vector positions; 19 | std::vector> optionalPositions; 20 | Array2 cells; 21 | }; 22 | 23 | Solver(RandomGenerator& generator, std::vector variables, 24 | std::vector optionalVariables, const Array2& cells, 25 | const std::vector& doors, bool connected); 26 | 27 | std::optional solve(); 28 | 29 | private: 30 | RandomGenerator& mGenerator; 31 | std::vector mVariables; 32 | std::vector mOptionalVariables; 33 | Grid mGrid; 34 | std::vector mIndices; 35 | 36 | void computeDomains(); 37 | void computeDomain(Variable& variable); 38 | void orderVariables(); 39 | bool solve(std::size_t depth, std::vector& positions); 40 | void placeOptionals(std::vector>& positions); 41 | }; 42 | 43 | } -------------------------------------------------------------------------------- /src/DistanceToWallConstraint.cpp: -------------------------------------------------------------------------------- 1 | #include "DistanceToWallConstraint.h" 2 | #include 3 | #include "Grid.h" 4 | #include "Variable.h" 5 | 6 | namespace room 7 | { 8 | 9 | void DistanceToWallConstraint::constrain(const Grid& grid, Variable& variable) 10 | { 11 | auto box = variable.collisionBox.getUnion(variable.marginBox); 12 | if (direction == Direction::North) 13 | { 14 | std::erase_if(variable.domain, [this, &grid, &box](const auto& value) 15 | { 16 | return !grid.isDistantToWall(value.y, value.x, box, distance); 17 | }); 18 | } 19 | else if (direction == Direction::West) 20 | { 21 | std::erase_if(variable.domain, [this, &grid, &box](const auto& value) 22 | { 23 | return !grid.isDistantToWall(value.y, value.x, box, distance); 24 | }); 25 | } 26 | else if (direction == Direction::South) 27 | { 28 | std::erase_if(variable.domain, [this, &grid, &box](const auto& value) 29 | { 30 | return !grid.isDistantToWall(value.y, value.x, box, distance); 31 | }); 32 | } 33 | else if (direction == Direction::East) 34 | { 35 | std::erase_if(variable.domain, [this, &grid, &box](const auto& value) 36 | { 37 | return !grid.isDistantToWall(value.y, value.x, box, distance); 38 | }); 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/NextToDoorConstraint.cpp: -------------------------------------------------------------------------------- 1 | #include "NextToDoorConstraint.h" 2 | #include 3 | #include "Grid.h" 4 | #include "Variable.h" 5 | 6 | namespace room 7 | { 8 | 9 | void NextToDoorConstraint::constrain(const Grid& grid, Variable& variable) 10 | { 11 | auto box = variable.collisionBox.getUnion(variable.marginBox); 12 | if (side == Direction::North || side == Direction::South) 13 | { 14 | auto offset = side == Direction::North ? box.top : box.getBottom() - 1; 15 | std::erase_if(variable.domain, [&grid, &variable, offset](const auto& value) 16 | { 17 | auto i = value.y + offset; 18 | auto j1 = value.x + variable.collisionBox.left - 1; 19 | auto j2 = value.x + variable.collisionBox.getRight(); 20 | return (j1 < 0 || !grid.isDoor(i, j1)) && 21 | (static_cast(j2) >= grid.getWidth() || !grid.isDoor(i, j2)); 22 | }); 23 | } 24 | else 25 | { 26 | auto offset = side == Direction::West ? box.left : box.getRight() - 1; 27 | std::erase_if(variable.domain, [&grid, &variable, offset](const auto& value) 28 | { 29 | auto j = value.x + offset; 30 | auto i1 = value.y + variable.collisionBox.top - 1; 31 | auto i2 = value.y + variable.collisionBox.getBottom(); 32 | return (i1 < 0 || !grid.isDoor(i1, j)) && 33 | (static_cast(i2) >= grid.getHeight() || !grid.isDoor(i2, j)); 34 | }); 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /include/Array2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace room 7 | { 8 | 9 | template 10 | class Array2 11 | { 12 | public: 13 | Array2(std::size_t height = 0, std::size_t width = 0) 14 | { 15 | reshape(height, width); 16 | } 17 | 18 | Array2(std::size_t height, std::size_t width, const T& defaultValue) 19 | { 20 | reshape(height, width, defaultValue); 21 | } 22 | 23 | // Using decltype(auto) allows the methods to work equally well with std::vector 24 | decltype(auto) get(std::size_t i, std::size_t j) 25 | { 26 | assert(i < mHeight && j < mWidth); 27 | return mData[i * mWidth + j]; 28 | } 29 | 30 | decltype(auto) get(std::size_t i, std::size_t j) const 31 | { 32 | assert(i < mHeight && j < mWidth); 33 | return mData[i * mWidth + j]; 34 | } 35 | 36 | void reshape(std::size_t height, std::size_t width) 37 | { 38 | mHeight = height; 39 | mWidth = width; 40 | mData.resize(mHeight * mWidth); 41 | } 42 | 43 | void reshape(std::size_t height, std::size_t width, const T& defaultValue) 44 | { 45 | mHeight = height; 46 | mWidth = width; 47 | mData.resize(mHeight * mWidth, defaultValue); 48 | } 49 | 50 | std::size_t getHeight() const 51 | { 52 | return mHeight; 53 | } 54 | 55 | std::size_t getWidth() const 56 | { 57 | return mWidth; 58 | } 59 | 60 | private: 61 | std::size_t mHeight; 62 | std::size_t mWidth; 63 | std::vector mData; 64 | }; 65 | 66 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # Project name 4 | 5 | project(RoomGenerator VERSION 1.0 LANGUAGES CXX) 6 | 7 | # Set warnings 8 | 9 | function(setWarnings target) 10 | target_compile_options(${target} PRIVATE 11 | -Wall 12 | -Wextra 13 | -Wshadow 14 | -Wnon-virtual-dtor 15 | -Wold-style-cast 16 | -Wcast-align 17 | -Wunused 18 | -Woverloaded-virtual 19 | -Wpedantic 20 | -Wconversion 21 | -Wsign-conversion 22 | -Wmisleading-indentation 23 | -Wduplicated-cond 24 | -Wduplicated-branches 25 | -Wlogical-op 26 | -Wnull-dereference 27 | -Wuseless-cast 28 | -Wdouble-promotion) 29 | endfunction() 30 | 31 | # Set standard 32 | 33 | function(setStandard target) 34 | target_compile_features(${target} PRIVATE cxx_std_20) 35 | endfunction() 36 | 37 | # Create library 38 | 39 | add_library(room_generator 40 | src/CornerConstraint.cpp 41 | src/DistanceToAnyWallConstraint.cpp 42 | src/DistanceToBorderConstraint.cpp 43 | src/DistanceToWallConstraint.cpp 44 | src/Grid.cpp 45 | src/NextToDoorConstraint.cpp 46 | src/Solver.cpp) 47 | target_include_directories(room_generator PUBLIC 48 | $ 49 | $) 50 | setWarnings(room_generator) 51 | setStandard(room_generator) 52 | 53 | include(GNUInstallDirs) 54 | install(TARGETS room_generator) 55 | install(DIRECTORY include DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 56 | 57 | 58 | # Examples 59 | 60 | option (BUILD_EXAMPLES "Build the examples." ON) 61 | if (BUILD_EXAMPLES) 62 | add_subdirectory(examples) 63 | endif() -------------------------------------------------------------------------------- /include/Grid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "Array2.h" 7 | #include "Box.h" 8 | #include "Direction.h" 9 | #include "Vector2.h" 10 | 11 | namespace room 12 | { 13 | 14 | struct Variable; 15 | 16 | class Grid 17 | { 18 | public: 19 | enum class Cell : int8_t 20 | { 21 | Empty = 0, 22 | Margin = 1, 23 | Full = -1, 24 | Wall = -2 25 | }; 26 | 27 | struct Door 28 | { 29 | Vector2i position; 30 | }; 31 | 32 | Grid(const Array2& cells, const std::vector& doors, bool connected); 33 | 34 | std::size_t getHeight() const; 35 | std::size_t getWidth() const; 36 | bool isDoor(int i, int j) const; 37 | template 38 | bool isDistantToWall(int i, int j, const Box& box, uint32_t distance) const; 39 | bool isInCorner(int i, int j, const Box& box) const; 40 | 41 | bool tryPlace(int i, int j, const Variable& variable); 42 | void remove(int i, int j, const Variable& variable); 43 | 44 | bool isConnected() const; 45 | 46 | Array2 getCells() const; 47 | 48 | private: 49 | Array2> mCells; 50 | Array2 mDoors; 51 | std::array, DirectionCount> mDistancesToWall; 52 | bool mConnected = true; 53 | 54 | bool mayDisconnect(int iStart, int iEnd, int jStart, int jEnd) const; 55 | 56 | bool isEmpty(std::size_t i, std::size_t j) const; 57 | bool isEmptyOrMargin(std::size_t i, std::size_t j) const; 58 | bool isFull(std::size_t i, std::size_t j) const; 59 | bool isWall(std::size_t i, std::size_t j) const; 60 | void setEmpty(std::size_t i, std::size_t j); 61 | void addMargin(std::size_t i, std::size_t j); 62 | void removeMargin(std::size_t i, std::size_t j); 63 | void setFull(std::size_t i, std::size_t j); 64 | std::array getCorners(int i, int j, const Box& box) const; 65 | bool checkInBox(const std::array& corners, 66 | std::invocable auto&& predicate); 67 | void applyInBox(const std::array& corners, 68 | std::invocable auto&& function); 69 | 70 | Array2& getDistancesToWall(Direction direction); 71 | const Array2& getDistancesToWall(Direction direction) const; 72 | }; 73 | 74 | } -------------------------------------------------------------------------------- /examples/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Solver.h" 3 | 4 | int main() 5 | { 6 | auto rd = std::random_device(); 7 | auto rng = room::RandomGenerator(rd()); 8 | auto variables = std::vector 9 | { 10 | room::Variable 11 | { 12 | room::Box{0, 0, 2, 1}, 13 | room::Box{0, 0, 2, 2} 14 | }, 15 | room::Variable 16 | { 17 | room::Box{0, 0, 1, 1}, 18 | room::Box{0, 0, 1, 2} 19 | }, 20 | room::Variable 21 | { 22 | room::Box{0, 0, 3, 3}, 23 | room::Box{0, 0, 3, 3} 24 | } 25 | }; 26 | auto optionalVariables = std::vector(); 27 | auto cells = room::Array2(9, 9, room::Grid::Cell::Empty); 28 | for (auto i = std::size_t(0); i < cells.getHeight(); ++i) 29 | { 30 | cells.get(i, 0) = room::Grid::Cell::Wall; 31 | cells.get(i, cells.getWidth() - 1) = room::Grid::Cell::Wall; 32 | } 33 | for (auto j = std::size_t(0); j < cells.getWidth(); ++j) 34 | { 35 | cells.get(0, j) = room::Grid::Cell::Wall; 36 | cells.get(cells.getHeight() - 1, j) = room::Grid::Cell::Wall; 37 | } 38 | auto doors = std::vector(); 39 | auto solver = room::Solver(rng, variables, optionalVariables, cells, doors, true); 40 | auto result = solver.solve(); 41 | if (!result.has_value()) 42 | std::cout << "No result!\n"; 43 | else 44 | { 45 | const auto& newCells = result->cells; 46 | for (auto i = std::size_t(0); i < newCells.getHeight(); ++i) 47 | { 48 | for (auto j = std::size_t(0); j < newCells.getWidth(); ++j) 49 | { 50 | switch (newCells.get(i, j)) 51 | { 52 | case room::Grid::Cell::Empty: 53 | std::cout << ' '; 54 | break; 55 | case room::Grid::Cell::Margin: 56 | std::cout << '.'; 57 | break; 58 | case room::Grid::Cell::Full: 59 | std::cout << 'X'; 60 | break; 61 | case room::Grid::Cell::Wall: 62 | std::cout << '#'; 63 | break; 64 | default: 65 | assert(false); 66 | break; 67 | } 68 | } 69 | std::cout << '\n'; 70 | } 71 | } 72 | return 0; 73 | } -------------------------------------------------------------------------------- /src/Solver.cpp: -------------------------------------------------------------------------------- 1 | #include "Solver.h" 2 | #include 3 | #include 4 | #include "Grid.h" 5 | 6 | namespace room 7 | { 8 | 9 | Solver::Solver(RandomGenerator& generator, std::vector variables, 10 | std::vector optionalVariables, const Array2& cells, 11 | const std::vector& doors, bool connected) : 12 | mGenerator(generator), 13 | mVariables(std::move(variables)), 14 | mOptionalVariables(std::move(optionalVariables)), 15 | mGrid(cells, doors, connected) 16 | { 17 | // TODO: use a grid slightly larger to avoid bounds checking 18 | } 19 | 20 | std::optional Solver::solve() 21 | { 22 | computeDomains(); 23 | orderVariables(); 24 | auto solution = Solution(); 25 | solution.positions.resize(mVariables.size()); 26 | auto found = solve(0, solution.positions); 27 | if (found) 28 | { 29 | placeOptionals(solution.optionalPositions); 30 | solution.cells = mGrid.getCells(); 31 | return solution; 32 | } 33 | else 34 | return std::nullopt; 35 | } 36 | 37 | void Solver::computeDomains() 38 | { 39 | for (auto& variable : mVariables) 40 | computeDomain(variable); 41 | for (auto& variable : mOptionalVariables) 42 | computeDomain(variable); 43 | } 44 | 45 | void Solver::computeDomain(Variable& variable) 46 | { 47 | // Set up domain 48 | // MAYBE: move this inside Grid and remove coordinates that collides with an initial obstacle 49 | auto box = variable.collisionBox.getUnion(variable.marginBox); 50 | box.width = std::max(box.width, 1); 51 | box.height = std::max(box.height, 1); 52 | auto iMin = -box.top; 53 | auto iMax = static_cast(mGrid.getHeight()) - box.getBottom(); 54 | auto jMin = -box.left; 55 | auto jMax = static_cast(mGrid.getWidth()) - box.getRight(); 56 | auto& values = variable.domain; 57 | values.reserve((static_cast((iMax - iMin + 1) * (jMax - jMin + 1)))); 58 | for (auto i = iMin; i <= iMax; ++i) 59 | { 60 | for (auto j = jMin; j <= jMax; ++j) 61 | values.emplace_back(j, i); 62 | } 63 | // Apply constraints 64 | if (variable.constraints != nullptr) 65 | { 66 | for (const auto& constraint : *variable.constraints) 67 | constraint->constrain(mGrid, variable); 68 | } 69 | // Shuffle domains 70 | std::shuffle(std::begin(values), std::end(values), mGenerator); 71 | } 72 | 73 | void Solver::orderVariables() 74 | { 75 | mIndices = std::vector(mVariables.size()); 76 | for (auto i = std::size_t(0); i < mIndices.size(); ++i) 77 | mIndices[i] = i; 78 | // Use the minimum-remaining-values heuristic 79 | std::sort(std::begin(mIndices), std::end(mIndices), 80 | [this](auto lhs, auto rhs) 81 | { 82 | return mVariables[lhs].domain.size() < mVariables[rhs].domain.size(); 83 | }); 84 | } 85 | 86 | bool Solver::solve(std::size_t depth, std::vector& positions) 87 | { 88 | if (depth >= mIndices.size()) 89 | return true; 90 | 91 | const auto& variable = mVariables[mIndices[depth]]; 92 | for (const auto& value : variable.domain) 93 | { 94 | if (mGrid.tryPlace(value.y, value.x, variable)) 95 | { 96 | positions[mIndices[depth]] = value; 97 | if (solve(depth + 1, positions)) 98 | return true; 99 | else 100 | { 101 | positions.pop_back(); 102 | mGrid.remove(value.y, value.x, variable); 103 | } 104 | } 105 | } 106 | return false; 107 | } 108 | 109 | void Solver::placeOptionals(std::vector>& positions) 110 | { 111 | // Sort by priority and shuffle the indices with same priority 112 | auto ranks = std::vector(mOptionalVariables.size()); 113 | std::iota(std::begin(ranks), std::end(ranks), std::size_t(0)); 114 | std::shuffle(std::begin(ranks), std::end(ranks), mGenerator); 115 | auto indices = std::vector(mOptionalVariables.size()); 116 | std::iota(std::begin(indices), std::end(indices), std::size_t(0)); 117 | std::sort(std::begin(indices), std::end(indices), [this, &ranks](std::size_t i, std::size_t j) 118 | { 119 | return mOptionalVariables[i].priority > mOptionalVariables[j].priority || 120 | (mOptionalVariables[i].priority == mOptionalVariables[j].priority && ranks[i] < ranks[j]); 121 | }); 122 | // Try to place objects 123 | // MAYBE: limit the number of tries 124 | positions.resize(mOptionalVariables.size()); 125 | for (auto i : indices) 126 | { 127 | const auto& variable = mOptionalVariables[i]; 128 | for (const auto& value : variable.domain) 129 | { 130 | if (mGrid.tryPlace(value.y, value.x, variable)) 131 | { 132 | positions[i] = value; 133 | break; 134 | } 135 | } 136 | } 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /src/Grid.cpp: -------------------------------------------------------------------------------- 1 | #include "Grid.h" 2 | #include 3 | #include 4 | #include 5 | #include "Variable.h" 6 | 7 | using namespace room; 8 | 9 | namespace room 10 | { 11 | 12 | Grid::Grid(const Array2& cells, const std::vector& doors, bool connected) : 13 | mCells(cells.getHeight(), cells.getWidth()), 14 | mDoors(cells.getHeight(), cells.getWidth(), false), 15 | mConnected(connected) 16 | { 17 | // Set up cells 18 | for (auto i = std::size_t(0); i < cells.getHeight(); ++i) 19 | { 20 | for (auto j = std::size_t(0); j < cells.getWidth(); ++j) 21 | mCells.get(i, j) = static_cast(cells.get(i, j)); 22 | } 23 | // Set doors 24 | for (const auto& door : doors) 25 | mDoors.get(static_cast(door.position.y), static_cast(door.position.x)) = true; 26 | // Compute distance to walls 27 | static constexpr auto MaxDistance = std::numeric_limits::max(); 28 | getDistancesToWall(Direction::North).reshape(cells.getHeight(), cells.getWidth()); 29 | for (auto j = std::size_t(0); j < cells.getWidth(); ++j) 30 | { 31 | auto distance = MaxDistance; 32 | for (auto i = std::size_t(0); i < cells.getHeight(); ++i) 33 | { 34 | if (isWall(i, j)) 35 | { 36 | getDistancesToWall(Direction::North).get(i, j) = MaxDistance; 37 | distance = 0; 38 | } 39 | else 40 | { 41 | getDistancesToWall(Direction::North).get(i, j) = distance; 42 | distance += distance != MaxDistance; 43 | } 44 | } 45 | } 46 | getDistancesToWall(Direction::West).reshape(cells.getHeight(), cells.getWidth()); 47 | for (auto i = std::size_t(0); i < cells.getHeight(); ++i) 48 | { 49 | auto distance = MaxDistance; 50 | for (auto j = std::size_t(0); j < cells.getWidth(); ++j) 51 | { 52 | if (isWall(i, j)) 53 | { 54 | getDistancesToWall(Direction::West).get(i, j) = MaxDistance; 55 | distance = 0; 56 | } 57 | else 58 | { 59 | getDistancesToWall(Direction::West).get(i, j) = distance; 60 | distance += distance != MaxDistance; 61 | } 62 | } 63 | } 64 | getDistancesToWall(Direction::South).reshape(cells.getHeight(), cells.getWidth()); 65 | for (auto j = std::size_t(0); j < cells.getWidth(); ++j) 66 | { 67 | auto distance = MaxDistance; 68 | for (auto i = static_cast(cells.getHeight()) - 1; i >= 0; --i) 69 | { 70 | auto y = static_cast(i); 71 | if (isWall(y, j)) 72 | { 73 | getDistancesToWall(Direction::South).get(y, j) = MaxDistance; 74 | distance = 0; 75 | } 76 | else 77 | { 78 | getDistancesToWall(Direction::South).get(y, j) = distance; 79 | distance += distance != MaxDistance; 80 | } 81 | } 82 | } 83 | getDistancesToWall(Direction::East).reshape(cells.getHeight(), cells.getWidth()); 84 | for (auto i = std::size_t(0); i < cells.getHeight(); ++i) 85 | { 86 | auto distance = MaxDistance; 87 | for (auto j = static_cast(cells.getWidth()) - 1; j >= 0; --j) 88 | { 89 | auto x = static_cast(j); 90 | if (isWall(i, x)) 91 | { 92 | getDistancesToWall(Direction::East).get(i, x) = MaxDistance; 93 | distance = 0; 94 | } 95 | else 96 | { 97 | getDistancesToWall(Direction::East).get(i, x) = distance; 98 | distance += distance != MaxDistance; 99 | } 100 | } 101 | } 102 | } 103 | 104 | std::size_t Grid::getHeight() const 105 | { 106 | return mCells.getHeight(); 107 | } 108 | 109 | std::size_t Grid::getWidth() const 110 | { 111 | return mCells.getWidth(); 112 | } 113 | 114 | bool Grid::isDoor(int i, int j) const 115 | { 116 | return mDoors.get(static_cast(i), static_cast(j)); 117 | } 118 | 119 | template 120 | bool Grid::isDistantToWall(int i, int j, const Box& box, uint32_t distance) const 121 | { 122 | auto minDistance = std::numeric_limits::max(); 123 | if constexpr (Direction == Direction::North) 124 | { 125 | auto y = static_cast(i + box.top); 126 | auto xStart = static_cast(j + box.left); 127 | auto xEnd = static_cast(j + box.getRight()); 128 | for (auto x = xStart; x < xEnd; ++x) 129 | minDistance = std::min(getDistancesToWall(Direction).get(y, x), minDistance); 130 | } 131 | else if constexpr (Direction == Direction::West) 132 | { 133 | auto yStart = static_cast(i + box.top); 134 | auto yEnd = static_cast(i + box.getBottom()); 135 | auto x = static_cast(j + box.left); 136 | for (auto y = yStart; y < yEnd; ++y) 137 | minDistance = std::min(getDistancesToWall(Direction).get(y, x), minDistance); 138 | } 139 | else if constexpr (Direction == Direction::South) 140 | { 141 | auto y = static_cast(i + box.getBottom() - 1); 142 | auto xStart = static_cast(j + box.left); 143 | auto xEnd = static_cast(j + box.getRight()); 144 | for (auto x = xStart; x < xEnd; ++x) 145 | minDistance = std::min(getDistancesToWall(Direction).get(y, x), minDistance); 146 | } 147 | else if constexpr (Direction == Direction::East) 148 | { 149 | auto yStart = static_cast(i + box.top); 150 | auto yEnd = static_cast(i + box.getBottom()); 151 | auto x = static_cast(j + box.getRight() - 1); 152 | for (auto y = yStart; y < yEnd; ++y) 153 | minDistance = std::min(getDistancesToWall(Direction).get(y, x), minDistance); 154 | } 155 | return minDistance == distance; 156 | } 157 | 158 | bool Grid::isInCorner(int i, int j, const Box& box) const 159 | { 160 | auto [yStart, yEnd, xStart, xEnd] = getCorners(i, j, box); 161 | return getDistancesToWall(Direction::North).get(yStart, xStart) + getDistancesToWall(Direction::West).get(yStart, xStart) == 0 || 162 | getDistancesToWall(Direction::North).get(yStart, xEnd - 1) + getDistancesToWall(Direction::East).get(yStart, xEnd - 1) == 0 || 163 | getDistancesToWall(Direction::South).get(yEnd - 1, xStart) + getDistancesToWall(Direction::West).get(yEnd - 1, xStart) == 0 || 164 | getDistancesToWall(Direction::South).get(yEnd - 1, xEnd - 1) + getDistancesToWall(Direction::East).get(yEnd - 1, xEnd - 1) == 0; 165 | } 166 | 167 | bool Grid::tryPlace(int i, int j, const Variable& variable) 168 | { 169 | // Check that the collision box is free 170 | const auto& collisionCorners = getCorners(i, j, variable.collisionBox); 171 | if (!checkInBox(collisionCorners, std::bind_front(&Grid::isEmpty, this))) 172 | return false; 173 | // Check that the margin box is free 174 | // MAYBE: do not check indices inside the collision box 175 | auto marginCorners = getCorners(i, j, variable.marginBox); 176 | if (!checkInBox(marginCorners, std::bind_front(&Grid::isEmptyOrMargin, this))) 177 | return false; 178 | // Add the variable 179 | applyInBox(marginCorners, std::bind_front(&Grid::addMargin, this)); 180 | applyInBox(collisionCorners, std::bind_front(&Grid::setFull, this)); 181 | // Check that adding the box does not disconnect the grid 182 | const auto& [yStart, yEnd, xStart, xEnd] = collisionCorners; 183 | if (mConnected && 184 | mayDisconnect(static_cast(yStart), static_cast(yEnd), static_cast(xStart), static_cast(xEnd)) && 185 | !isConnected()) 186 | { 187 | remove(i, j, variable); 188 | return false; 189 | } 190 | return true; 191 | } 192 | 193 | void Grid::remove(int i, int j, const Variable& variable) 194 | { 195 | // Set empty 196 | const auto& collisionCorners = getCorners(i, j, variable.collisionBox); 197 | applyInBox(collisionCorners, std::bind_front(&Grid::setEmpty, this)); 198 | // Remove margins 199 | applyInBox(getCorners(i, j, variable.marginBox), std::bind_front(&Grid::removeMargin, this)); 200 | } 201 | 202 | bool Grid::isConnected() const 203 | { 204 | static constexpr auto deltas = std::array 205 | { 206 | Vector2i{0, -1}, Vector2i{-1, 0}, Vector2i{1, 0}, Vector2i{0, 1} 207 | }; 208 | auto reachable = Array2(getHeight(), getWidth(), false); 209 | auto frontier = std::queue(); 210 | // Find an empty or margin cell to use as start of the BFS 211 | auto found = false; 212 | for (auto i = std::size_t(0); i < getHeight() && !found; ++i) 213 | { 214 | for (auto j = std::size_t(0); j < getWidth() && !found; ++j) 215 | { 216 | if (isEmptyOrMargin(i, j)) 217 | { 218 | reachable.get(i, j) = true; 219 | frontier.push(Vector2i{static_cast(j), static_cast(i)}); 220 | found = true; 221 | } 222 | } 223 | } 224 | // BFS 225 | while (!frontier.empty()) 226 | { 227 | auto cell = frontier.front(); 228 | frontier.pop(); 229 | for (const auto& delta : deltas) 230 | { 231 | auto i = static_cast(cell.y + delta.y); 232 | auto j = static_cast(cell.x + delta.x); 233 | if (i < reachable.getHeight() && j < reachable.getWidth() && 234 | !reachable.get(i, j) && isEmptyOrMargin(i, j)) 235 | { 236 | reachable.get(i, j) = true; 237 | frontier.push(Vector2i{static_cast(j), static_cast(i)}); 238 | } 239 | } 240 | } 241 | // Check that all empty or margin cells are reachable 242 | for (auto i = std::size_t(0); i < getHeight(); ++i) 243 | { 244 | for (auto j = std::size_t(0); j < getWidth(); ++j) 245 | { 246 | if (isEmptyOrMargin(i, j) && !reachable.get(i, j)) 247 | return false; 248 | } 249 | } 250 | return true; 251 | } 252 | 253 | Array2 Grid::getCells() const 254 | { 255 | auto cells = Array2(getHeight(), getWidth()); 256 | for (auto i = std::size_t(0); i < getHeight(); ++i) 257 | { 258 | for (auto j = std::size_t(0); j < getWidth(); ++j) 259 | { 260 | if (isWall(i, j)) 261 | cells.get(i, j) = Cell::Wall; 262 | else if (isFull(i, j)) 263 | cells.get(i, j) = Cell::Full; 264 | else if (isEmpty(i, j)) 265 | cells.get(i, j) = Cell::Empty; 266 | else 267 | cells.get(i, j) = Cell::Margin; 268 | } 269 | } 270 | return cells; 271 | } 272 | 273 | bool Grid::mayDisconnect(int iStart, int iEnd, int jStart, int jEnd) const 274 | { 275 | // This function checks the frontier of the box, if frontier is connected, the changes inside the box will leave the room connected 276 | // To check the connectedness, we compute the number of distinct walls in the frontier 277 | auto getState = [this](std::size_t y, std::size_t x){ return y >= getHeight() || x >= getWidth() || isFull(y, x); }; 278 | auto i = iStart - 1; 279 | auto j = jStart - 1; 280 | auto state = getState(static_cast(i), static_cast(j)); 281 | auto nbWalls = 0; 282 | auto updateState = [&getState, &state, &nbWalls, &i, &j]() 283 | { 284 | auto newState = getState(static_cast(i), static_cast(j)); 285 | nbWalls += static_cast(!state && newState); // We detect the beginning of a wall 286 | state = newState; 287 | }; 288 | for (j = jStart; j <= jEnd; ++j) 289 | updateState(); 290 | for (i = iStart, j = jEnd; i <= iEnd; ++i) 291 | updateState(); 292 | for (i = iEnd, j = jEnd - 1; j >= jStart - 1; --j) 293 | updateState(); 294 | for (i = iEnd - 1, j = jStart - 1; i >= iStart - 1; --i) 295 | updateState(); 296 | return nbWalls > 1; 297 | } 298 | 299 | bool Grid::isEmpty(std::size_t i, std::size_t j) const 300 | { 301 | return mCells.get(i, j) == 0; 302 | } 303 | 304 | bool Grid::isEmptyOrMargin(std::size_t i, std::size_t j) const 305 | { 306 | return mCells.get(i, j) >= 0; 307 | } 308 | 309 | bool Grid::isFull(std::size_t i, std::size_t j) const 310 | { 311 | return mCells.get(i, j) < 0; 312 | } 313 | 314 | bool Grid::isWall(std::size_t i, std::size_t j) const 315 | { 316 | return mCells.get(i, j) == static_cast(Cell::Wall); 317 | } 318 | 319 | void Grid::setEmpty(std::size_t i, std::size_t j) 320 | { 321 | assert(mCells.get(i, j) == static_cast(Grid::Cell::Full)); 322 | mCells.get(i, j) = 0; 323 | } 324 | 325 | void Grid::addMargin(std::size_t i, std::size_t j) 326 | { 327 | assert(isEmptyOrMargin(i, j)); 328 | ++mCells.get(i, j); 329 | } 330 | 331 | void Grid::removeMargin(std::size_t i, std::size_t j) 332 | { 333 | assert(isEmptyOrMargin(i, j)); // The cell may be empty if we just removed the collision box there 334 | auto& cell = mCells.get(i, j); 335 | cell = static_cast(std::max(0, cell - 1)); 336 | } 337 | 338 | void Grid::setFull(std::size_t i, std::size_t j) 339 | { 340 | assert(mCells.get(i, j) == 0 || mCells.get(i, j) == 1); // The cell may be a margin if we just set the margin box there 341 | mCells.get(i, j) = static_cast(Grid::Cell::Full); 342 | } 343 | 344 | std::array Grid::getCorners(int i, int j, const Box& box) const 345 | { 346 | auto yStart = static_cast(i + box.top); 347 | auto yEnd = static_cast(i + box.getBottom()); 348 | auto xStart = static_cast(j + box.left); 349 | auto xEnd = static_cast(j + box.getRight()); 350 | assert((yStart >= yEnd || (yStart < yEnd && yEnd <= getHeight())) && 351 | (xStart >= xEnd || (xStart < xEnd && xEnd <= getWidth()))); 352 | return {yStart, yEnd, xStart, xEnd}; 353 | } 354 | 355 | bool Grid::checkInBox(const std::array& corners, 356 | std::invocable auto&& predicate) 357 | { 358 | const auto& [yStart, yEnd, xStart, xEnd] = corners; 359 | for (auto y = yStart; y < yEnd; ++y) 360 | { 361 | for (auto x = xStart; x < xEnd; ++x) 362 | { 363 | if (!predicate(y, x)) 364 | return false; 365 | } 366 | } 367 | return true; 368 | } 369 | 370 | void Grid::applyInBox(const std::array& corners, 371 | std::invocable auto&& function) 372 | { 373 | const auto& [yStart, yEnd, xStart, xEnd] = corners; 374 | for (auto y = yStart; y < yEnd; ++y) 375 | { 376 | for (auto x = xStart; x < xEnd; ++x) 377 | function(y, x); 378 | } 379 | } 380 | 381 | Array2& Grid::getDistancesToWall(Direction direction) 382 | { 383 | return mDistancesToWall[static_cast(direction)]; 384 | } 385 | 386 | const Array2& Grid::getDistancesToWall(Direction direction) const 387 | { 388 | return mDistancesToWall[static_cast(direction)]; 389 | } 390 | 391 | } 392 | 393 | // Explicit instantiations of isDistantToWall 394 | template bool Grid::isDistantToWall(int i, int j, const Box& box, uint32_t distance) const; 395 | template bool Grid::isDistantToWall(int i, int j, const Box& box, uint32_t distance) const; 396 | template bool Grid::isDistantToWall(int i, int j, const Box& box, uint32_t distance) const; 397 | template bool Grid::isDistantToWall(int i, int j, const Box& box, uint32_t distance) const; --------------------------------------------------------------------------------