small refactor

This commit is contained in:
2026-02-28 17:57:24 +01:00
parent 3f71603961
commit ce05dd504a
37 changed files with 4393 additions and 3681 deletions

View File

@ -1,396 +1,509 @@
#include "puzzle.hpp"
#include "config.hpp"
#include <unordered_set>
#ifdef TRACY
#include "tracy.hpp"
#include <tracy/Tracy.hpp>
#include <tracy/Tracy.hpp>
#endif
auto Block::Hash() const -> std::size_t {
std::string s = std::format("{},{},{},{}", x, y, width, height);
return std::hash<std::string>{}(s);
auto puzzle::block::hash() const -> size_t
{
const std::string s = std::format("{},{},{},{}", x, y, width, height);
return std::hash<std::string>{}(s);
}
auto Block::Invalid() -> Block {
Block block = Block(0, 0, 1, 1, false, false);
block.width = 0;
block.height = 0;
return block;
auto puzzle::block::valid() const -> bool
{
return width > 0 && height > 0 && x >= 0 && x + width <= 9 && y >= 0 && y + height <= 9;
}
auto Block::IsValid() const -> bool { return width != 0 && height != 0; }
auto Block::ToString() 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));
} else if (immovable) {
return std::format("{}{}",
static_cast<char>(width + static_cast<int>('A') - 1),
static_cast<char>(height + static_cast<int>('A') - 1));
} else {
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 Block::GetPrincipalDirs() const -> int {
if (immovable) {
return 0;
}
if (width > height) {
return Direction::EAS | Direction::WES;
} else if (height > width) {
return Direction::NOR | Direction::SOU;
} else {
return Direction::NOR | Direction::EAS | Direction::SOU | Direction::WES;
}
}
auto Block::Covers(int xx, int yy) const -> bool {
return xx >= x && xx < x + width && yy >= y && yy < y + height;
}
auto Block::Collides(const Block &other) const -> bool {
return x < other.x + other.width && x + width > other.x &&
y < other.y + other.height && y + height > other.y;
}
auto State::Hash() const -> std::size_t {
return std::hash<std::string>{}(state);
}
auto State::HasWinCondition() const -> bool {
return target_x != 9 && target_y != 9;
}
auto State::IsWon() const -> bool {
if (!HasWinCondition()) {
return false;
}
for (const auto &block : *this) {
if (block.target) {
return block.x == target_x && block.y == target_y;
}
}
return false;
}
auto State::SetGoal(int x, int y) -> bool {
Block target_block = GetTargetBlock();
if (!target_block.IsValid() || x < 0 || x + target_block.width > width ||
y < 0 || y + target_block.height > height) {
return false;
}
if (target_x == x && target_y == y) {
target_x = 9;
target_y = 9;
} else {
target_x = x;
target_y = y;
}
state.replace(3, 1, std::format("{}", target_x));
state.replace(4, 1, std::format("{}", target_y));
return true;
}
auto State::ClearGoal() -> void {
target_x = 9;
target_y = 9;
state.replace(3, 2, "99");
}
auto State::AddColumn() const -> State {
State newstate = State(width + 1, height, restricted);
for (const auto &block : *this) {
newstate.AddBlock(block);
}
return newstate;
}
auto State::RemoveColumn() const -> State {
State newstate = State(width - 1, height, restricted);
for (const auto &block : *this) {
newstate.AddBlock(block);
}
return newstate;
}
auto State::AddRow() const -> State {
State newstate = State(width, height + 1, restricted);
for (const auto &block : *this) {
newstate.AddBlock(block);
}
return newstate;
}
auto State::RemoveRow() const -> State {
State newstate = State(width, height - 1, restricted);
for (const auto &block : *this) {
newstate.AddBlock(block);
}
return newstate;
}
auto State::AddBlock(const Block &block) -> bool {
if (block.x + block.width > width || block.y + block.height > height) {
return false;
}
for (Block b : *this) {
if (b.Collides(block)) {
return false;
}
}
int index = GetIndex(block.x, block.y);
state.replace(index, 2, block.ToString());
return true;
}
auto State::GetBlock(int x, int y) const -> Block {
if (x >= width || y >= height) {
return Block::Invalid();
}
for (Block b : *this) {
if (b.Covers(x, y)) {
return b;
}
}
return Block::Invalid();
}
auto State::GetBlockAt(int x, int y) const -> std::string {
return state.substr(GetIndex(x, y), 2);
}
auto State::GetTargetBlock() const -> Block {
for (Block b : *this) {
if (b.target) {
return b;
}
}
return Block::Invalid();
}
auto State::GetIndex(int x, int y) const -> int {
return prefix + (y * width + x) * 2;
}
auto State::RemoveBlock(int x, int y) -> bool {
Block block = GetBlock(x, y);
if (!block.IsValid()) {
return false;
}
int index = GetIndex(block.x, block.y);
state.replace(index, 2, "..");
return true;
}
auto State::ToggleTarget(int x, int y) -> bool {
Block block = GetBlock(x, y);
if (!block.IsValid() || block.immovable) {
return false;
}
// Remove the current target
int index;
for (const auto &b : *this) {
if (b.target) {
index = GetIndex(b.x, b.y);
state.replace(index, 2,
Block(b.x, b.y, b.width, b.height, false).ToString());
break;
}
}
// Add the new target
block.target = !block.target;
index = GetIndex(block.x, block.y);
state.replace(index, 2, block.ToString());
return true;
}
auto State::ToggleWall(int x, int y) -> bool {
Block block = GetBlock(x, y);
if (!block.IsValid() || block.target) {
return false;
}
// Add the new target
block.immovable = !block.immovable;
int index = GetIndex(block.x, block.y);
state.replace(index, 2, block.ToString());
return true;
}
auto State::ToggleRestricted() -> void {
restricted = !restricted;
state.replace(0, 1, restricted ? "R" : "F");
}
auto State::MoveBlockAt(int x, int y, Direction dir) -> bool {
Block block = GetBlock(x, y);
if (!block.IsValid() || block.immovable) {
return false;
}
int dirs = restricted ? block.GetPrincipalDirs()
: Direction::NOR | Direction::EAS | Direction::SOU |
Direction::WES;
// Get target block
int _target_x = block.x;
int _target_y = block.y;
switch (dir) {
case Direction::NOR:
if (!(dirs & Direction::NOR) || _target_y < 1) {
return false;
}
_target_y--;
break;
case Direction::EAS:
if (!(dirs & Direction::EAS) || _target_x + block.width >= width) {
return false;
}
_target_x++;
break;
case Direction::SOU:
if (!(dirs & Direction::SOU) || _target_y + block.height >= height) {
return false;
}
_target_y++;
break;
case Direction::WES:
if (!(dirs & Direction::WES) || _target_x < 1) {
return false;
}
_target_x--;
break;
}
Block target =
Block(_target_x, _target_y, block.width, block.height, block.target);
// Check collisions
for (Block b : *this) {
if (b != block && b.Collides(target)) {
return false;
}
}
RemoveBlock(x, y);
AddBlock(target);
return true;
}
auto State::GetNextStates() const -> std::vector<State> {
std::vector<State> new_states;
for (const Block &b : *this) {
int dirs = restricted ? b.GetPrincipalDirs()
: Direction::NOR | Direction::EAS | Direction::SOU |
Direction::WES;
if (b.immovable) {
continue;
auto puzzle::block::principal_dirs() const -> int
{
if (immovable) {
return 0;
}
if (dirs & Direction::NOR) {
State north = *this;
if (north.MoveBlockAt(b.x, b.y, Direction::NOR)) {
new_states.push_back(north);
}
if (width > height) {
return eas | wes;
}
if (dirs & Direction::EAS) {
State east = *this;
if (east.MoveBlockAt(b.x, b.y, Direction::EAS)) {
new_states.push_back(east);
}
if (height > width) {
return nor | sou;
}
if (dirs & Direction::SOU) {
State south = *this;
if (south.MoveBlockAt(b.x, b.y, Direction::SOU)) {
new_states.push_back(south);
}
}
if (dirs & Direction::WES) {
State west = *this;
if (west.MoveBlockAt(b.x, b.y, Direction::WES)) {
new_states.push_back(west);
}
}
}
return new_states;
return nor | eas | sou | wes;
}
auto State::Closure() const
-> std::pair<std::vector<State>,
std::vector<std::pair<std::size_t, std::size_t>>> {
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::valid_thorough() const -> bool
{
if (has_win_condition() && !try_get_target_block()) {
return false;
}
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 false;
}
// Check prefix
if (!std::string("FR").contains(state[0])) {
infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]);
return false;
}
if (restricted && state[0] != 'R') {
infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], restricted);
return false;
}
if (!std::string("3456789").contains(state[1]) || !std::string("3456789").contains(state[2])) {
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]);
return false;
}
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 false;
}
if (!std::string("012345678").contains(state[3]) || !std::string("012345678").contains(state[4])) {
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[1], state[2]);
return false;
}
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[1], state[2], target_x,
target_y);
return false;
}
// 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 false;
}
}
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 success;
}
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("{}", target_x));
p.state.replace(4, 1, std::format("{}", 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, "99");
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;
ZoneScoped;
#endif
std::vector<State> states;
std::vector<std::pair<std::size_t, std::size_t>> links;
infoln("Exploring state space, this might take a while...");
// Helper to construct the links vector
std::unordered_map<State, std::size_t> state_indices;
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<State> remaining_states;
remaining_states.insert(*this);
// Buffer for all states we want to call GetNextStates() on
std::unordered_set<puzzle> remaining_states;
remaining_states.insert(*this);
do {
const State current = *remaining_states.begin();
remaining_states.erase(current);
do {
const puzzle current = *remaining_states.begin();
remaining_states.erase(current);
if (!state_indices.contains(current)) {
state_indices.emplace(current, states.size());
states.push_back(current);
}
if (!state_indices.contains(current)) {
state_indices.emplace(current, state_pool.size());
state_pool.push_back(current);
}
for (const State &s : current.GetNextStates()) {
if (!state_indices.contains(s)) {
remaining_states.insert(s);
state_indices.emplace(s, states.size());
states.push_back(s);
}
links.emplace_back(state_indices.at(current), state_indices.at(s));
}
} while (remaining_states.size() > 0);
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());
std::cout << std::format("State space has size {} with {} transitions.",
states.size(), links.size())
<< std::endl;
infoln("State space has size {} with {} transitions.", state_pool.size(), links.size());
return std::make_pair(states, links);
return std::make_pair(state_pool, links);
}