533 lines
14 KiB
C++
533 lines
14 KiB
C++
#include "puzzle.hpp"
|
|
|
|
#include <optional>
|
|
#include <unordered_set>
|
|
|
|
#ifdef TRACY
|
|
#include <tracy/Tracy.hpp>
|
|
#endif
|
|
|
|
auto puzzle::block::hash() const -> size_t
|
|
{
|
|
const std::string s = std::format("{},{},{},{}", x, y, width, height);
|
|
return std::hash<std::string>{}(s);
|
|
}
|
|
|
|
auto puzzle::block::valid() const -> bool
|
|
{
|
|
return width > 0 && height > 0 && x >= 0 && x + width <= 9 && y >= 0 && y + height <= 9;
|
|
}
|
|
|
|
auto puzzle::block::string() const -> std::string
|
|
{
|
|
if (target) {
|
|
return std::format("{}{}", static_cast<char>(width + static_cast<int>('a') - 1),
|
|
static_cast<char>(height + static_cast<int>('a') - 1));
|
|
}
|
|
if (immovable) {
|
|
return std::format("{}{}", static_cast<char>(width + static_cast<int>('A') - 1),
|
|
static_cast<char>(height + static_cast<int>('A') - 1));
|
|
}
|
|
return std::format("{}{}", width, height);
|
|
}
|
|
|
|
auto puzzle::block::principal_dirs() const -> int
|
|
{
|
|
if (immovable) {
|
|
return 0;
|
|
}
|
|
|
|
if (width > height) {
|
|
return eas | wes;
|
|
}
|
|
if (height > width) {
|
|
return nor | sou;
|
|
}
|
|
return nor | eas | sou | wes;
|
|
}
|
|
|
|
auto puzzle::block::covers(const int _x, const int _y) const -> bool
|
|
{
|
|
return _x >= x && _x < x + width && _y >= y && _y < y + height;
|
|
}
|
|
|
|
auto puzzle::block::collides(const block& b) const -> bool
|
|
{
|
|
return x < b.x + b.width && x + width > b.x && y < b.y + b.height && y + height > b.y;
|
|
}
|
|
|
|
auto puzzle::get_index(const int x, const int y) const -> int
|
|
{
|
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
|
errln("Trying to calculating index of invalid board coordinates ({}, {})", x, y);
|
|
exit(1);
|
|
}
|
|
return PREFIX + (y * width + x) * 2;
|
|
}
|
|
|
|
auto puzzle::hash() const -> size_t
|
|
{
|
|
return std::hash<std::string>{}(state);
|
|
}
|
|
|
|
auto puzzle::has_win_condition() const -> bool
|
|
{
|
|
return target_x != MAX_WIDTH && target_y != MAX_HEIGHT;
|
|
}
|
|
|
|
auto puzzle::won() const -> bool
|
|
{
|
|
const std::optional<block>& b = try_get_target_block();
|
|
return has_win_condition() && b && b->x == target_x && b->y == target_y;
|
|
}
|
|
|
|
auto puzzle::valid() const -> bool
|
|
{
|
|
return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT;
|
|
}
|
|
|
|
auto puzzle::try_get_invalid_reason() const -> std::optional<std::string>
|
|
{
|
|
const std::optional<block>& b = try_get_target_block();
|
|
if (has_win_condition() && !b) {
|
|
return "Goal Without Target";
|
|
}
|
|
if (!has_win_condition() && b) {
|
|
return "Target Without Goal";
|
|
}
|
|
|
|
if (has_win_condition() && b && restricted) {
|
|
const int dirs = b->principal_dirs();
|
|
if ((dirs & nor && b->x != target_x) || (dirs & eas && b->y != target_y)) {
|
|
return "Goal Unreachable";
|
|
}
|
|
}
|
|
|
|
if (target_x > 0 && target_x + b->width < width && target_y > 0 && target_y + b->height < height) {
|
|
return "Goal Inside";
|
|
}
|
|
|
|
infoln("Validating puzzle {}", state);
|
|
|
|
if (static_cast<int>(state.length()) != width * height * 2 + PREFIX) {
|
|
infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(),
|
|
width, height);
|
|
return "Invalid Repr Length";
|
|
}
|
|
|
|
// Check prefix
|
|
if (!std::string("FR").contains(state[0])) {
|
|
infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]);
|
|
return "Invalid Restricted Repr";
|
|
}
|
|
if (restricted && state[0] != 'R') {
|
|
infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0],
|
|
restricted);
|
|
return "Restricted != Restricted Repr";
|
|
}
|
|
if (!std::string("0123456789").contains(state[1]) ||
|
|
!std::string("0123456789").contains(state[2])) {
|
|
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]);
|
|
return "Invalid Dims Repr";
|
|
}
|
|
if (std::stoi(state.substr(1, 1)) != width || std::stoi(state.substr(2, 1)) != height) {
|
|
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1],
|
|
state[2], width, height);
|
|
return "Dims != Dims Repr";
|
|
}
|
|
if (!std::string("0123456789").contains(state[3]) ||
|
|
!std::string("0123456789").contains(state[4])) {
|
|
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[3], state[4]);
|
|
return "Invalid Goal Repr";
|
|
}
|
|
if (std::stoi(state.substr(3, 1)) != target_x || std::stoi(state.substr(4, 1)) != target_y) {
|
|
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3],
|
|
state[4], target_x, target_y);
|
|
return "Goal != Goal Repr";
|
|
}
|
|
|
|
// Check blocks
|
|
const std::string allowed_chars = ".123456789abcdefghiABCDEFGHI";
|
|
for (const char c : state.substr(PREFIX, state.length() - PREFIX)) {
|
|
if (!allowed_chars.contains(c)) {
|
|
infoln("Puzzle invalid: Block {} has invalid character", c);
|
|
return "Invalid Block Repr";
|
|
}
|
|
}
|
|
|
|
const bool success =
|
|
width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT;
|
|
|
|
if (!success) {
|
|
infoln("Puzzle invalid: Board size {}x{} not in range [3-9]", width, height);
|
|
return "Invalid Dims";
|
|
} else {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
auto puzzle::try_get_block(const int x, const int y) const -> std::optional<block>
|
|
{
|
|
if (!covers(x, y)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
for (const block& b : *this) {
|
|
if (b.covers(x, y)) {
|
|
return b;
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto puzzle::try_get_target_block() const -> std::optional<block>
|
|
{
|
|
for (const block b : *this) {
|
|
if (b.target) {
|
|
return b;
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto puzzle::covers(const int x, const int y, const int w, const int h) const -> bool
|
|
{
|
|
return x >= 0 && x + w <= width && y >= 0 && y + h <= height;
|
|
}
|
|
|
|
auto puzzle::covers(const int x, const int y) const -> bool
|
|
{
|
|
return covers(x, y, 1, 1);
|
|
}
|
|
|
|
auto puzzle::covers(const block& b) const -> bool
|
|
{
|
|
return covers(b.x, b.y, b.width, b.height);
|
|
}
|
|
|
|
auto puzzle::try_toggle_restricted() const -> std::optional<puzzle>
|
|
{
|
|
puzzle p = *this;
|
|
p.restricted = !restricted;
|
|
p.state.replace(0, 1, p.restricted ? "R" : "F");
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_set_goal(const int x, const int y) const -> std::optional<puzzle>
|
|
{
|
|
const std::optional<block>& b = try_get_target_block();
|
|
if (!b || !covers(x, y, b->width, b->height)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = *this;
|
|
if (target_x == x && target_y == y) {
|
|
p.target_x = MAX_WIDTH;
|
|
p.target_y = MAX_HEIGHT;
|
|
} else {
|
|
p.target_x = x;
|
|
p.target_y = y;
|
|
}
|
|
|
|
p.state.replace(3, 1, std::format("{}", p.target_x));
|
|
p.state.replace(4, 1, std::format("{}", p.target_y));
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_clear_goal() const -> std::optional<puzzle>
|
|
{
|
|
puzzle p = *this;
|
|
p.target_x = MAX_WIDTH;
|
|
p.target_y = MAX_HEIGHT;
|
|
p.state.replace(3, 2, std::format("{}{}", MAX_WIDTH, MAX_HEIGHT));
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_add_column() const -> std::optional<puzzle>
|
|
{
|
|
if (width >= MAX_WIDTH) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = {width + 1, height, restricted};
|
|
|
|
// Non-fitting blocks won't be added
|
|
for (const block& b : *this) {
|
|
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
|
p = *_p;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_remove_column() const -> std::optional<puzzle>
|
|
{
|
|
if (width <= MIN_WIDTH) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = {width - 1, height, restricted};
|
|
|
|
// Non-fitting blocks won't be added
|
|
for (const block& b : *this) {
|
|
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
|
p = *_p;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_add_row() const -> std::optional<puzzle>
|
|
{
|
|
if (height >= 9) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = puzzle(width, height + 1, restricted);
|
|
|
|
for (const block& b : *this) {
|
|
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
|
p = *_p;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_remove_row() const -> std::optional<puzzle>
|
|
{
|
|
if (height == 0) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = puzzle(width, height - 1, restricted);
|
|
|
|
for (const block& b : *this) {
|
|
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
|
p = *_p;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_add_block(const block& b) const -> std::optional<puzzle>
|
|
{
|
|
if (!covers(b)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
for (block _b : *this) {
|
|
if (_b.collides(b)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
puzzle p = *this;
|
|
const int index = get_index(b.x, b.y);
|
|
p.state.replace(index, 2, b.string());
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_remove_block(const int x, const int y) const -> std::optional<puzzle>
|
|
{
|
|
const std::optional<block>& b = try_get_block(x, y);
|
|
if (!b) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
puzzle p = *this;
|
|
const int index = get_index(b->x, b->y);
|
|
p.state.replace(index, 2, "..");
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_toggle_target(const int x, const int y) const -> std::optional<puzzle>
|
|
{
|
|
std::optional<block> b = try_get_block(x, y);
|
|
if (!b || b->immovable) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Remove the current target if it exists
|
|
puzzle p = *this;
|
|
if (const std::optional<block>& _b = try_get_target_block()) {
|
|
const int index = get_index(_b->x, _b->y);
|
|
p.state.replace(index, 2, block(_b->x, _b->y, _b->width, _b->height, false).string());
|
|
}
|
|
|
|
// Add the new target
|
|
b->target = !b->target;
|
|
const int index = get_index(b->x, b->y);
|
|
p.state.replace(index, 2, b->string());
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional<puzzle>
|
|
{
|
|
std::optional<block> b = try_get_block(x, y);
|
|
if (!b || b->target) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Add the new target
|
|
puzzle p = *this;
|
|
b->immovable = !b->immovable;
|
|
const int index = get_index(b->x, b->y);
|
|
p.state.replace(index, 2, b->string());
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const
|
|
-> std::optional<puzzle>
|
|
{
|
|
const std::optional<block>& b = try_get_block(x, y);
|
|
if (!b || b->immovable) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const int dirs = restricted ? b->principal_dirs() : nor | eas | sou | wes;
|
|
|
|
// Get target block
|
|
int _target_x = b->x;
|
|
int _target_y = b->y;
|
|
switch (dir) {
|
|
case nor:
|
|
if (!(dirs & nor) || _target_y < 1) {
|
|
return std::nullopt;
|
|
}
|
|
_target_y--;
|
|
break;
|
|
case eas:
|
|
if (!(dirs & eas) || _target_x + b->width >= width) {
|
|
return std::nullopt;
|
|
}
|
|
_target_x++;
|
|
break;
|
|
case sou:
|
|
if (!(dirs & sou) || _target_y + b->height >= height) {
|
|
return std::nullopt;
|
|
}
|
|
_target_y++;
|
|
break;
|
|
case wes:
|
|
if (!(dirs & wes) || _target_x < 1) {
|
|
return std::nullopt;
|
|
}
|
|
_target_x--;
|
|
break;
|
|
}
|
|
const block moved_b = block(_target_x, _target_y, b->width, b->height, b->target);
|
|
|
|
// Check collisions
|
|
for (const block& _b : *this) {
|
|
if (_b != b && _b.collides(moved_b)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::optional<puzzle> p = try_remove_block(x, y);
|
|
if (!p) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
p = p->try_add_block(moved_b);
|
|
if (!p) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
auto puzzle::find_adjacent_puzzles() const -> std::vector<puzzle>
|
|
{
|
|
std::vector<puzzle> puzzles;
|
|
|
|
for (const block& b : *this) {
|
|
if (b.immovable) {
|
|
continue;
|
|
}
|
|
|
|
const int dirs = restricted ? b.principal_dirs() : nor | eas | sou | wes;
|
|
|
|
if (dirs & nor) {
|
|
if (const std::optional<puzzle>& north = try_move_block_at(b.x, b.y, nor)) {
|
|
puzzles.push_back(*north);
|
|
}
|
|
}
|
|
|
|
if (dirs & eas) {
|
|
if (const std::optional<puzzle>& east = try_move_block_at(b.x, b.y, eas)) {
|
|
puzzles.push_back(*east);
|
|
}
|
|
}
|
|
|
|
if (dirs & sou) {
|
|
if (const std::optional<puzzle>& south = try_move_block_at(b.x, b.y, sou)) {
|
|
puzzles.push_back(*south);
|
|
}
|
|
}
|
|
|
|
if (dirs & wes) {
|
|
if (const std::optional<puzzle>& west = try_move_block_at(b.x, b.y, wes)) {
|
|
puzzles.push_back(*west);
|
|
}
|
|
}
|
|
}
|
|
|
|
// for (const puzzle& p : puzzles) {
|
|
// println("Adjacent puzzle: {}", p.state);
|
|
// }
|
|
|
|
return puzzles;
|
|
}
|
|
|
|
auto puzzle::explore_state_space() const
|
|
-> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>
|
|
{
|
|
#ifdef TRACY
|
|
ZoneScoped;
|
|
#endif
|
|
|
|
infoln("Exploring state space, this might take a while...");
|
|
|
|
std::vector<puzzle> state_pool;
|
|
std::unordered_map<puzzle, std::size_t> state_indices; // Helper to construct the links vector
|
|
std::vector<std::pair<size_t, std::size_t>> links;
|
|
|
|
// Buffer for all states we want to call GetNextStates() on
|
|
std::unordered_set<puzzle> remaining_states;
|
|
remaining_states.insert(*this);
|
|
|
|
do {
|
|
const puzzle current = *remaining_states.begin();
|
|
remaining_states.erase(current);
|
|
|
|
if (!state_indices.contains(current)) {
|
|
state_indices.emplace(current, state_pool.size());
|
|
state_pool.push_back(current);
|
|
}
|
|
|
|
for (const puzzle& s : current.find_adjacent_puzzles()) {
|
|
if (!state_indices.contains(s)) {
|
|
remaining_states.insert(s);
|
|
state_indices.emplace(s, state_pool.size());
|
|
state_pool.push_back(s);
|
|
}
|
|
links.emplace_back(state_indices.at(current), state_indices.at(s));
|
|
}
|
|
} while (!remaining_states.empty());
|
|
|
|
infoln("State space has size {} with {} transitions.", state_pool.size(), links.size());
|
|
|
|
return std::make_pair(state_pool, links);
|
|
} |