#include "puzzle.hpp" #include #include #ifdef TRACY #include #endif auto puzzle::block::hash() const -> size_t { const std::string s = std::format("{},{},{},{}", x, y, width, height); return std::hash{}(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(width + static_cast('a') - 1), static_cast(height + static_cast('a') - 1)); } if (immovable) { return std::format("{}{}", static_cast(width + static_cast('A') - 1), static_cast(height + static_cast('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{}(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& 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 { const std::optional& 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(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 { 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 { 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 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 { const std::optional& 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 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 { 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& _p = p.try_add_block(b)) { p = *_p; } } return p; } auto puzzle::try_remove_column() const -> std::optional { 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& _p = p.try_add_block(b)) { p = *_p; } } return p; } auto puzzle::try_add_row() const -> std::optional { if (height >= 9) { return std::nullopt; } puzzle p = puzzle(width, height + 1, restricted); for (const block& b : *this) { if (const std::optional& _p = p.try_add_block(b)) { p = *_p; } } return p; } auto puzzle::try_remove_row() const -> std::optional { if (height == 0) { return std::nullopt; } puzzle p = puzzle(width, height - 1, restricted); for (const block& b : *this) { if (const std::optional& _p = p.try_add_block(b)) { p = *_p; } } return p; } auto puzzle::try_add_block(const block& b) const -> std::optional { 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 { const std::optional& 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 { std::optional 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& _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 { std::optional 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 { const std::optional& 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 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 { std::vector 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& north = try_move_block_at(b.x, b.y, nor)) { puzzles.push_back(*north); } } if (dirs & eas) { if (const std::optional& east = try_move_block_at(b.x, b.y, eas)) { puzzles.push_back(*east); } } if (dirs & sou) { if (const std::optional& south = try_move_block_at(b.x, b.y, sou)) { puzzles.push_back(*south); } } if (dirs & wes) { if (const std::optional& 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>> { #ifdef TRACY ZoneScoped; #endif infoln("Exploring state space, this might take a while..."); std::vector state_pool; std::unordered_map state_indices; // Helper to construct the links vector std::vector> links; // Buffer for all states we want to call GetNextStates() on std::unordered_set 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); }