implement very slow puzzle space exploration

This commit is contained in:
2026-03-04 19:08:16 +01:00
parent 2d111f58da
commit c9915852db
26 changed files with 1438 additions and 697 deletions

View File

@ -3,70 +3,16 @@
#include <algorithm>
#include <boost/unordered/unordered_flat_map.hpp>
auto puzzle::block::create_repr(const uint8_t x, const uint8_t y, const uint8_t w, const uint8_t h, const bool t,
auto puzzle::block::create_repr(const uint8_t x,
const uint8_t y,
const uint8_t w,
const uint8_t h,
const bool t,
const bool i) -> uint16_t
{
return block().set_x(x).set_y(y).set_width(w).set_height(h).set_target(t).set_immovable(i).repr & ~INVALID;
}
auto puzzle::block::set_x(const uint8_t x) const -> block
{
if (x > 7) {
throw std::invalid_argument("Block x-position out of bounds");
}
block b = *this;
set_bits(b.repr, X_S, X_E, x);
return b;
}
auto puzzle::block::set_y(const uint8_t y) const -> block
{
if (y > 7) {
throw std::invalid_argument("Block y-position out of bounds");
}
block b = *this;
set_bits(b.repr, Y_S, Y_E, y);
return b;
}
auto puzzle::block::set_width(const uint8_t width) const -> block
{
if (width - 1 > 7) {
throw std::invalid_argument("Block width out of bounds");
}
block b = *this;
set_bits(b.repr, WIDTH_S, WIDTH_E, width - 1u);
return b;
}
auto puzzle::block::set_height(const uint8_t height) const -> block
{
if (height - 1 > 7) {
throw std::invalid_argument("Block height out of bounds");
}
block b = *this;
set_bits(b.repr, HEIGHT_S, HEIGHT_E, height - 1u);
return b;
}
auto puzzle::block::set_target(const bool target) const -> block
{
block b = *this;
set_bits(b.repr, TARGET_S, TARGET_E, target);
return b;
}
auto puzzle::block::set_immovable(const bool immovable) const -> block
{
block b = *this;
set_bits(b.repr, IMMOVABLE_S, IMMOVABLE_E, immovable);
return b;
}
auto puzzle::block::unpack_repr() const -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>
{
const uint8_t x = get_x();
@ -79,46 +25,29 @@ auto puzzle::block::unpack_repr() const -> std::tuple<uint8_t, uint8_t, uint8_t,
return {x, y, w, h, t, i};
}
auto puzzle::block::get_x() const -> uint8_t
auto puzzle::block::hash() const -> size_t
{
return get_bits(repr, X_S, X_E);
return std::hash<uint16_t>{}(repr);
}
auto puzzle::block::get_y() const -> uint8_t
auto puzzle::block::position_independent_hash() const -> size_t
{
return get_bits(repr, Y_S, Y_E);
}
auto puzzle::block::get_width() const -> uint8_t
{
return get_bits(repr, WIDTH_S, WIDTH_E) + 1u;
}
auto puzzle::block::get_height() const -> uint8_t
{
return get_bits(repr, HEIGHT_S, HEIGHT_E) + 1u;
}
auto puzzle::block::get_target() const -> bool
{
return get_bits(repr, TARGET_S, TARGET_E);
}
auto puzzle::block::get_immovable() const -> bool
{
return get_bits(repr, IMMOVABLE_S, IMMOVABLE_E);
uint16_t r = repr;
clear_bits(r, X_S, X_E);
clear_bits(r, Y_S, Y_E);
return std::hash<uint16_t>{}(r);
}
auto puzzle::block::valid() const -> bool
{
const auto [x, y, w, h, t, i] = unpack_repr();
if (t && i) {
// This means the first bit is set, marking the block as empty
if (repr & INVALID) {
return false;
}
// This means the first bit is set, marking the block as empty
if (repr & INVALID) {
const auto [x, y, w, h, t, i] = unpack_repr();
if (t && i) {
return false;
}
@ -171,8 +100,13 @@ auto puzzle::create_meta(const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bo
return m;
}
auto puzzle::create_repr(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r,
const bool g, const std::array<uint16_t, MAX_BLOCKS>& b) -> repr_cooked
auto puzzle::create_repr(const uint8_t w,
const uint8_t h,
const uint8_t tx,
const uint8_t ty,
const bool r,
const bool g,
const std::array<uint16_t, MAX_BLOCKS>& b) -> repr_cooked
{
repr_cooked repr = puzzle().set_width(w).set_height(h).set_goal_x(tx).set_goal_y(ty).set_restricted(r).set_goal(g).
set_blocks(b).repr.cooked;
@ -180,7 +114,9 @@ auto puzzle::create_repr(const uint8_t w, const uint8_t h, const uint8_t tx, con
return repr;
}
auto puzzle::create_repr(const uint64_t byte_0, const uint64_t byte_1, const uint64_t byte_2,
auto puzzle::create_repr(const uint64_t byte_0,
const uint64_t byte_1,
const uint64_t byte_2,
const uint64_t byte_3) -> repr_cooked
{
repr_u repr{};
@ -199,64 +135,6 @@ auto puzzle::create_repr(const std::string& string_repr) -> repr_cooked
return *repr;
}
auto puzzle::set_restricted(const bool restricted) const -> puzzle
{
uint16_t meta = repr.cooked.meta;
set_bits(meta, RESTRICTED_S, RESTRICTED_E, restricted);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_width(const uint8_t width) const -> puzzle
{
if (width - 1 > MAX_WIDTH) {
throw "Board width out of bounds";
}
uint16_t meta = repr.cooked.meta;
set_bits(meta, WIDTH_S, WIDTH_E, width - 1u);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_height(const uint8_t height) const -> puzzle
{
if (height - 1 > MAX_HEIGHT) {
throw "Board height out of bounds";
}
uint16_t meta = repr.cooked.meta;
set_bits(meta, HEIGHT_S, HEIGHT_E, height - 1u);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_goal(const bool goal) const -> puzzle
{
uint16_t meta = repr.cooked.meta;
set_bits(meta, GOAL_S, GOAL_E, goal);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_goal_x(const uint8_t target_x) const -> puzzle
{
if (target_x >= MAX_WIDTH) {
throw "Board target x out of bounds";
}
uint16_t meta = repr.cooked.meta;
set_bits(meta, GOAL_X_S, GOAL_X_E, target_x);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_goal_y(const uint8_t target_y) const -> puzzle
{
if (target_y >= MAX_HEIGHT) {
throw "Board target y out of bounds";
}
uint16_t meta = repr.cooked.meta;
set_bits(meta, GOAL_Y_S, GOAL_Y_E, target_y);
return puzzle(meta, repr.cooked.blocks);
}
auto puzzle::set_blocks(std::array<uint16_t, MAX_BLOCKS> blocks) const -> puzzle
{
puzzle p = *this;
@ -277,36 +155,6 @@ auto puzzle::unpack_meta() const -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_
return {w, h, tx, ty, r, g};
}
auto puzzle::get_restricted() const -> bool
{
return get_bits(repr.cooked.meta, RESTRICTED_S, RESTRICTED_E);
}
auto puzzle::get_width() const -> uint8_t
{
return get_bits(repr.cooked.meta, WIDTH_S, WIDTH_E) + 1u;
}
auto puzzle::get_height() const -> uint8_t
{
return get_bits(repr.cooked.meta, HEIGHT_S, HEIGHT_E) + 1u;
}
auto puzzle::get_goal() const -> bool
{
return get_bits(repr.cooked.meta, GOAL_S, GOAL_E);
}
auto puzzle::get_goal_x() const -> uint8_t
{
return get_bits(repr.cooked.meta, GOAL_X_S, GOAL_X_E);
}
auto puzzle::get_goal_y() const -> uint8_t
{
return get_bits(repr.cooked.meta, GOAL_Y_S, GOAL_Y_E);
}
auto puzzle::hash() const -> size_t
{
size_t h = 0;
@ -542,7 +390,7 @@ auto puzzle::try_get_invalid_reason() const -> std::optional<std::string>
const auto [w, h, gx, gy, r, g] = unpack_meta();
traceln("Validating puzzle \"{}\"", string_repr());
// traceln("Validating puzzle \"{}\"", string_repr());
const std::optional<block>& b = try_get_target_block();
if (get_goal() && !b) {
@ -896,64 +744,8 @@ auto puzzle::try_move_block_at(const uint8_t x, const uint8_t y, const direction
return p;
}
auto puzzle::try_move_block_at_fast(const uint64_t bitmap, const uint8_t block_idx,
const direction dir) const -> std::optional<puzzle>
{
const block b = block(repr.cooked.blocks[block_idx]);
const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr();
if (bi) {
return std::nullopt;
}
const auto [w, h, gx, gy, r, g] = unpack_meta();
const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes;
// Get target block
int _target_x = bx;
int _target_y = by;
switch (dir) {
case nor:
if (!(dirs & nor) || _target_y < 1) {
return std::nullopt;
}
--_target_y;
break;
case eas:
if (!(dirs & eas) || _target_x + bw >= w) {
return std::nullopt;
}
++_target_x;
break;
case sou:
if (!(dirs & sou) || _target_y + bh >= h) {
return std::nullopt;
}
++_target_y;
break;
case wes:
if (!(dirs & wes) || _target_x < 1) {
return std::nullopt;
}
--_target_x;
break;
}
// Check collisions
const uint64_t bm = bitmap_clear_block(bitmap, b);
if (bitmap_check_collision(bm, b, dir)) {
return std::nullopt;
}
// Replace block
const std::array<uint16_t, MAX_BLOCKS> blocks = sorted_replace(repr.cooked.blocks, block_idx,
block::create_repr(
_target_x, _target_y, bw, bh, bt));
// This constructor doesn't sort
return puzzle(std::make_tuple(w, h, gx, gy, r, g), blocks);
}
auto puzzle::sorted_replace(std::array<uint16_t, MAX_BLOCKS> blocks, const uint8_t idx,
auto puzzle::sorted_replace(std::array<uint16_t, MAX_BLOCKS> blocks,
const uint8_t idx,
const uint16_t new_val) -> std::array<uint16_t, MAX_BLOCKS>
{
// Remove old entry
@ -987,95 +779,69 @@ auto puzzle::blocks_bitmap() const -> uint64_t
}
auto [x, y, w, h, t, im] = b.unpack_repr();
const uint8_t width = get_width();
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
bitmap |= 1ULL << ((y + dy) * 8 + (x + dx));
bitmap_set_bit(bitmap, width, x + dx, y + dy);
}
}
}
return bitmap;
}
inline auto puzzle::bitmap_set_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> uint64_t
auto puzzle::blocks_bitmap_h() const -> uint64_t
{
return bitmap & ~(1ULL << (y * 8 + x));
}
uint64_t bitmap = 0;
for (uint8_t i = 0; i < MAX_BLOCKS; ++i) {
block b(repr.cooked.blocks[i]);
if (!b.valid()) {
break;
}
const int dirs = b.principal_dirs();
if (!(dirs & eas)) {
continue;
}
inline auto puzzle::bitmap_get_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> bool
{
return bitmap & (1ULL << (y * 8 + x));
}
auto [x, y, w, h, t, im] = b.unpack_repr();
const uint8_t width = get_width();
auto puzzle::bitmap_clear_block(uint64_t bitmap, const block b) -> uint64_t
{
const auto [x, y, w, h, t, i] = b.unpack_repr();
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
bitmap = bitmap_set_bit(bitmap, x + dx, y + dy);
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
bitmap_set_bit(bitmap, width, x + dx, y + dy);
}
}
}
return bitmap;
}
auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b) -> bool
auto puzzle::blocks_bitmap_v() const -> uint64_t
{
const auto [x, y, w, h, t, i] = b.unpack_repr();
uint64_t bitmap = 0;
for (uint8_t i = 0; i < MAX_BLOCKS; ++i) {
block b(repr.cooked.blocks[i]);
if (!b.valid()) {
break;
}
const int dirs = b.principal_dirs();
if (!(dirs & sou)) {
continue;
}
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
if (bitmap_get_bit(bitmap, x + dx, y + dy)) {
return true; // collision
auto [x, y, w, h, t, im] = b.unpack_repr();
const uint8_t width = get_width();
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
bitmap_set_bit(bitmap, width, x + dx, y + dy);
}
}
}
return false;
}
auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b, const direction dir) -> bool
{
const auto [x, y, w, h, t, i] = b.unpack_repr();
switch (dir) {
case nor: // Check the row above: (x...x+w-1, y-1)
for (int dx = 0; dx < w; ++dx) {
if (bitmap_get_bit(bitmap, x + dx, y - 1)) {
return true;
}
}
break;
case sou: // Check the row below: (x...x+w-1, y+h)
for (int dx = 0; dx < w; ++dx) {
if (bitmap_get_bit(bitmap, x + dx, y + h)) {
return true;
}
}
break;
case wes: // Check the column left: (x-1, y...y+h-1)
for (int dy = 0; dy < h; ++dy) {
if (bitmap_get_bit(bitmap, x - 1, y + dy)) {
return true;
}
}
break;
case eas: // Check the column right: (x+w, y...y+h-1)
for (int dy = 0; dy < h; ++dy) {
if (bitmap_get_bit(bitmap, x + w, y + dy)) {
return true;
}
}
break;
}
return false;
return bitmap;
}
auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>
{
const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
std::vector<puzzle> state_pool;
boost::unordered_flat_map<puzzle, std::size_t, puzzle_hasher> state_indices;
std::vector<std::pair<size_t, size_t>> links;
@ -1083,6 +849,11 @@ auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::
// Buffer for all states we want to call GetNextStates() on
std::vector<size_t> queue; // indices into state_pool
#ifdef WIP
// Store an index to the blocks array of a state for each occupied bitmap cell
std::array<uint8_t, 64> bitmap_block_indices;
#endif
// Start with the current state
state_indices.emplace(*this, 0);
state_pool.push_back(*this);
@ -1095,6 +866,24 @@ auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::
// Make a copy because references might be invalidated when inserting into the vector
const puzzle current = state_pool[current_idx];
#ifdef WIP
// Build bitmap-block indices
for (size_t i = 0; i < MAX_BLOCKS; ++i) {
const block b = block(current.repr.cooked.blocks[i]);
const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr();
if (!b.valid()) {
break;
}
for (uint8_t x = bx; x < bx + bw; ++x) {
for (uint8_t y = by; y < by + bh; ++y) {
bitmap_block_indices[y * current.get_width() + x] = i;
}
}
}
#endif
// TODO: I can just dispatch different functions depending on if the board is restricted or contains walls
current.for_each_adjacent([&](const puzzle& p)
{
auto [it, inserted] = state_indices.emplace(p, state_pool.size());
@ -1106,10 +895,239 @@ auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::
});
}
const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
infoln("Explored puzzle. Took {} ms.", std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());
infoln("State space has size {} with {} transitions.", state_pool.size(), links.size());
return {std::move(state_pool), std::move(links)};
}
auto puzzle::get_cluster_id_and_solution() const -> std::pair<puzzle, bool>
{
const auto& [puzzles, moves] = explore_state_space();
bool solution = false;
puzzle min = puzzles[0];
for (size_t i = 0; i < puzzles.size(); ++i) {
if (puzzles[i] < min) {
min = puzzles[i];
}
if (puzzles[i].goal_reached()) {
solution = true;
}
}
return {min, solution};
}
auto puzzle::bitmap_find_first_empty(const uint64_t bitmap, int& x, int& y) const -> bool
{
x = 0;
y = 0;
// Bitmap is empty of first slot is empty
if (bitmap_is_empty(bitmap) || !(bitmap & 1u)) {
return true;
}
// Bitmap is full
if (bitmap_is_full(bitmap)) {
return false;
}
// Find the next more significant empty bit (we know the first slot is full)
int ls_set = 0;
bool next_set = true;
while (next_set && ls_set < get_width() * get_height() - 1) {
next_set = bitmap & (1ul << (ls_set + 1));
++ls_set;
}
x = ls_set % get_width();
y = ls_set / get_width();
return true;
}
auto puzzle::generate_block_sequences(
const boost::unordered_flat_set<block, block_hasher2, block_equal2>& permitted_blocks,
const block target_block,
const size_t max_blocks,
std::vector<block>& current_sequence,
const int current_area,
const int board_area,
const std::function<void(const std::vector<block>&)>& callback) -> void
{
if (!current_sequence.empty()) {
callback(current_sequence);
}
if (current_sequence.size() == max_blocks) {
return;
}
for (const block b : permitted_blocks) {
const int new_area = current_area + b.get_width() * b.get_height();
if (new_area > board_area) {
continue;
}
// Explore all sequences with the block placed, then continue the loop
current_sequence.push_back(b);
generate_block_sequences(permitted_blocks,
target_block,
max_blocks,
current_sequence,
new_area,
board_area,
callback);
current_sequence.pop_back();
}
}
auto puzzle::place_block_sequence(const puzzle& p,
const uint64_t& bitmap,
const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>& p_repr,
const std::vector<block>& sequence,
const block target_block,
const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>& target_block_pos_range,
const bool has_target,
const size_t index,
const std::function<void(const puzzle&)>& callback) -> void
{
if (index == sequence.size()) {
// All blocks placed
callback(p);
return;
}
if (!has_target && p.get_restricted()) {
// Place target block (restricted movement)
const auto [txs, tys, txe, tye] = target_block_pos_range;
for (int tx = txs; tx <= txe; ++tx) {
for (int ty = tys; ty <= tye; ++ty) {
block t = target_block;
t = t.set_x(tx);
t = t.set_y(ty);
if (!p.covers(t)) {
continue;
}
const std::array<uint16_t, MAX_BLOCKS> blocks = sorted_replace(p.repr.cooked.blocks, 0, t.repr);
const puzzle next_p = puzzle(p_repr, blocks);
uint64_t next_bm = bitmap;
next_p.bitmap_set_block(next_bm, t);
// Place the remaining blocks for each possible target block configuration
// traceln("Generating block sequence for target at {},{}", tx, ty);
next_p.place_block_sequence(next_p,
next_bm,
p_repr,
sequence,
target_block,
target_block_pos_range,
true,
index,
callback);
}
}
return;
}
if (!has_target && !p.get_restricted()) {
// Place target block (free movement)
// TODO
}
int x, y;
if (!p.bitmap_find_first_empty(bitmap, x, y)) {
// No space remaining
callback(p);
return;
}
block b = sequence[index];
b = b.set_x(static_cast<uint8_t>(x));
b = b.set_y(static_cast<uint8_t>(y));
// Place the next block and call the resulting subtree, then remove the block and continue here
if (!p.bitmap_check_collision(bitmap, b) && p.covers(b)) {
// Shift the sequence by 1 (index + 1), because the target block is inserted separately
const std::array<uint16_t, MAX_BLOCKS> blocks = sorted_replace(p.repr.cooked.blocks, index + 1, b.repr);
const puzzle next_p = puzzle(p_repr, blocks);
uint64_t next_bm = bitmap;
next_p.bitmap_set_block(next_bm, b);
next_p.place_block_sequence(next_p,
next_bm,
p_repr,
sequence,
target_block,
target_block_pos_range,
true,
index + 1,
callback);
}
// Create an empty cell and call the resulting subtree (without advancing the block index)
uint64_t next_bm = bitmap;
bitmap_set_bit(next_bm, p.get_width(), b.get_x(), b.get_y());
p.place_block_sequence(p, next_bm, p_repr, sequence, target_block, target_block_pos_range, true, index, callback);
}
auto puzzle::explore_puzzle_space(const boost::unordered_flat_set<block, block_hasher2, block_equal2>& permitted_blocks,
const block target_block,
const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>& target_block_pos_range,
const size_t max_blocks,
const std::optional<BS::thread_pool<>* const> thread_pool) const ->
boost::unordered_flat_set<puzzle, puzzle_hasher>
{
const auto [w, h, gx, gy, r, g] = unpack_meta();
// Implemented in the slowest, stupidest way for now:
// 1. Iterate through all possible permitted_blocks permutations using recursive tree descent
// 2. Find the cluster id of the permutation by populating the entire state space
// - We could do some preprocessing to quickly reduce the numeric value
// of the state and check if its already contained in visited_clusters,
// this could save some state space calculations.
// 3. Add it to visited_clusters if unseen
std::mutex mtx;
boost::unordered_flat_set<puzzle, puzzle_hasher> visited_clusters;
// TODO: Can't even parallelize this. Or just start at different initial puzzles?
const puzzle empty_puzzle = puzzle(w, h, gx, gy, r, g);
const auto board_repr = std::make_tuple(w, h, gx, gy, r, g);
std::vector<block> current_sequence;
int total = 0;
generate_block_sequences(permitted_blocks,
target_block,
max_blocks - 1, // Make space for the target block
current_sequence,
target_block.get_width() * target_block.get_height(), // Starting area
get_width() * get_height(),
[&](const std::vector<block>& sequence)
{
place_block_sequence(empty_puzzle,
0,
board_repr,
sequence,
target_block,
target_block_pos_range,
false,
0,
[&](const puzzle& p)
{
const auto [cluster_id, winnable] = p.get_cluster_id_and_solution();
std::lock_guard<std::mutex> lock(mtx);
++total;
if (winnable) {
visited_clusters.emplace(cluster_id);
}
});
});
infoln("Found {} of {} clusters with a solution", visited_clusters.size(), total);
return visited_clusters;
}