Compare commits

..

19 Commits

Author SHA1 Message Date
6248c10a25 update tracy to v0.13.1 2026-03-12 20:18:36 +01:00
3230d806f7 restructure puzzle space generation (for boards up to 5x5)
- uses a huge global seen-states-cache. Not scalable without more
filtering
2026-03-07 23:33:55 +01:00
51723353fd update physics parameters 2026-03-06 22:49:37 +01:00
5289c8407a update screenshot 2026-03-06 22:25:37 +01:00
1b6f597cd5 fix windows build (requires glew built for windows) 2026-03-06 22:03:40 +01:00
e482adbb76 draw graph edges much faster (using raw vertex array) 2026-03-06 21:16:57 +01:00
591f018685 enable asynchronous octree building (somehow this is stable with morton octree) 2026-03-06 20:04:27 +01:00
fe9a54a8da implement graph node mouse collisions (unoptimized iterating over all masses for now) 2026-03-06 13:25:35 +01:00
6bfe217fee update color scheme 2026-03-06 12:52:47 +01:00
836b42f425 color nodes based on target distance 2026-03-06 03:30:31 +01:00
6ab935c9be pre-calculate morton codes in threadpool 2026-03-06 02:51:07 +01:00
c060cfd35d replace recursive octree implementation with morton code version (FML) 2026-03-06 02:20:28 +01:00
9f31d4e6ef add libmorton library dependency 2026-03-05 22:28:09 +01:00
9de0d06806 reformat 2026-03-05 22:00:28 +01:00
c8d6541221 add hashset benchmarks 2026-03-05 20:03:26 +01:00
950da499f0 reorder puzzle functions 2026-03-05 20:03:16 +01:00
025cbfdf3b move bits functions to separate file + fix missing defaults with disabled program_options on windows 2026-03-05 19:13:44 +01:00
d4f83e11db add .desktop icon to package 2026-03-05 02:05:02 +01:00
db588bd57b fix conditional threadpool include 2026-03-05 02:04:48 +01:00
39 changed files with 2923 additions and 1893 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ cmake-build-release
/perf.data
/perf.data.old
/clusters.puzzle
/benchs.json
/benchs.old.json

View File

@ -18,24 +18,27 @@ option(DISABLE_BENCH "Disable building benchmarks" OFF)
# Headers + Sources (excluding main.cpp)
set(SOURCES
src/backward.cpp
src/bits.cpp
src/cpu_layout_engine.cpp
src/cpu_spring_system.cpp
src/graph_distances.cpp
src/input_handler.cpp
src/load_save.cpp
src/mass_spring_system.cpp
src/octree.cpp
src/orbit_camera.cpp
src/puzzle.cpp
src/renderer.cpp
src/state_manager.cpp
src/threaded_physics.cpp
src/user_interface.cpp
)
# Libraries
include(FetchContent)
find_package(raylib REQUIRED)
find_package(GLEW REQUIRED)
find_package(libmorton REQUIRED)
find_package(Boost COMPONENTS program_options REQUIRED)
set(LIBS raylib Boost::headers Boost::program_options)
set(LIBS raylib GLEW::GLEW Boost::headers Boost::program_options)
set(FLAGS "")
if(WIN32)
@ -51,12 +54,14 @@ if(NOT DISABLE_BACKWARD)
list(APPEND LIBS Backward::Backward)
list(APPEND FLAGS BACKWARD)
message("-- BACKWARD: Enabled")
endif()
if(NOT DISABLE_TRACY)
FetchContent_Declare(tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG v0.11.1
GIT_TAG v0.13.1
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
@ -66,6 +71,8 @@ if(NOT DISABLE_TRACY)
list(APPEND LIBS TracyClient)
list(APPEND FLAGS TRACY)
message("-- TRACY: Enabled")
endif()
# Set this after fetching tracy to hide tracy's warnings.
@ -106,10 +113,12 @@ if(NOT DISABLE_TESTS AND NOT WIN32)
add_executable(tests ${TEST_SOURCES} ${SOURCES})
target_include_directories(tests PRIVATE include)
target_link_libraries(tests Catch2::Catch2WithMain raylib)
target_link_libraries(tests Catch2::Catch2WithMain raylib GLEW::GLEW)
include(Catch)
catch_discover_tests(tests)
message("-- TESTS: Enabled")
endif()
# Benchmarking
@ -122,15 +131,17 @@ if(NOT DISABLE_BENCH AND NOT WIN32)
add_executable(benchmarks ${BENCH_SOURCES} ${SOURCES})
target_include_directories(benchmarks PRIVATE include)
target_link_libraries(benchmarks benchmark raylib)
target_link_libraries(benchmarks benchmark raylib GLEW::GLEW)
message("-- BENCHMARKS: Enabled")
endif()
# LTO
include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error)
if(supported)
message(STATUS "IPO / LTO enabled")
message("-- IPO/LTO: Enabled")
set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(STATUS "IPO / LTO not supported")
message("-- IPO/LTO: Disabled")
endif()

View File

@ -1,6 +1,11 @@
// ReSharper disable CppTooWideScope
// ReSharper disable CppDFAUnreadVariable
#include "puzzle.hpp"
#include <random>
#include <unordered_set>
#include <benchmark/benchmark.h>
#include <boost/unordered/unordered_flat_map.hpp>
static std::vector<std::string> puzzles = {
// 0: RushHour 1
@ -35,6 +40,113 @@ static std::vector<std::string> puzzles = {
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]",
};
template <u8 N>
struct uint_hasher
{
int64_t nums;
auto operator()(const std::array<u64, N>& ints) const noexcept -> size_t
{
size_t h = 0;
for (size_t i = 0; i < N; ++i) {
puzzle::hash_combine(h, ints[i]);
}
return h;
}
};
template <u8 N>
static auto unordered_set_uint64(benchmark::State& state) -> void
{
std::random_device random_device;
std::mt19937 generator(random_device());
std::uniform_int_distribution<u64> distribution(
std::numeric_limits<u64>::min(),
std::numeric_limits<u64>::max()
);
std::unordered_set<std::array<u64, N>, uint_hasher<N>> set;
std::array<u64, N> ints;
for (size_t i = 0; i < N; ++i) {
ints[i] = distribution(generator);
}
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(ints);
}
benchmark::DoNotOptimize(set);
}
}
template <u8 N>
static auto unordered_flat_set_uint64(benchmark::State& state) -> void
{
std::random_device random_device;
std::mt19937 generator(random_device());
std::uniform_int_distribution<u64> distribution(
std::numeric_limits<u64>::min(),
std::numeric_limits<u64>::max()
);
boost::unordered_flat_set<std::array<u64, N>, uint_hasher<N>> set;
std::array<u64, N> ints;
for (size_t i = 0; i < N; ++i) {
ints[i] = distribution(generator);
}
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(ints);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_block_hasher(benchmark::State& state) -> void
{
blockset set;
const block b = block(2, 3, 1, 2, true, false);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(b);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_block_hasher2(benchmark::State& state) -> void
{
blockset2 set;
const block b = block(2, 3, 1, 2, true, false);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(b);
}
benchmark::DoNotOptimize(set);
}
}
static auto unordered_flat_set_puzzle_hasher(benchmark::State& state) -> void
{
puzzleset set;
const puzzle p = puzzle(puzzles[0]);
for (auto _ : state) {
for (size_t i = 0; i < 100000; ++i) {
set.emplace(p);
}
benchmark::DoNotOptimize(set);
}
}
static auto explore_state_space(benchmark::State& state) -> void
{
const puzzle p = puzzle(puzzles[state.range(0)]);
@ -48,43 +160,52 @@ static auto explore_state_space(benchmark::State& state) -> void
static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void
{
// ReSharper disable once CppTooWideScope
constexpr uint8_t max_blocks = 5;
constexpr u8 max_blocks = 5;
constexpr uint8_t board_width = 4;
constexpr uint8_t board_height = 5;
constexpr uint8_t goal_x = board_width - 1;
constexpr uint8_t goal_y = 2;
constexpr u8 board_width = 4;
constexpr u8 board_height = 5;
constexpr u8 goal_x = board_width - 1;
constexpr u8 goal_y = 2;
constexpr bool restricted = true;
const boost::unordered_flat_set<puzzle::block, block_hasher2, block_equal2> permitted_blocks = {
puzzle::block(0, 0, 2, 1, false, false),
puzzle::block(0, 0, 3, 1, false, false),
puzzle::block(0, 0, 1, 2, false, false),
puzzle::block(0, 0, 1, 3, false, false)
const blockset2 permitted_blocks = {
block(0, 0, 2, 1, false, false),
block(0, 0, 3, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 1, 3, false, false)
};
const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false);
constexpr std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> target_block_pos_range = {
const block target_block = block(0, 0, 2, 1, true, false);
constexpr std::tuple<u8, u8, u8, u8> target_block_pos_range = {
0,
goal_y,
board_width - 1,
goal_x,
goal_y
};
const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true);
for (auto _ : state) {
boost::unordered_flat_set<puzzle, puzzle_hasher> result = p.explore_puzzle_space(
puzzleset result = p.explore_puzzle_space(
permitted_blocks,
target_block,
target_block_pos_range,
max_blocks,
0,
std::nullopt);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(unordered_set_uint64<4>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_set_uint64<8>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_set_uint64<16>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<4>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<8>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_uint64<16>)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_block_hasher)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_block_hasher2)->Unit(benchmark::kMicrosecond);
BENCHMARK(unordered_flat_set_puzzle_hasher)->Unit(benchmark::kMicrosecond);
BENCHMARK(explore_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond);
BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond);

6
flake.lock generated
View File

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1772479524,
"narHash": "sha256-u7nCaNiMjqvKpE+uZz9hE7pgXXTmm5yvdtFaqzSzUQI=",
"lastModified": 1773201692,
"narHash": "sha256-NXrKzNMniu4Oam2kAFvqJ3GB2kAvlAFIriTAheaY8hw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4215e62dc2cd3bc705b0a423b9719ff6be378a43",
"rev": "b6067cc0127d4db9c26c79e4de0513e58d0c40c9",
"type": "github"
},
"original": {

115
flake.nix
View File

@ -111,9 +111,9 @@ rec {
abbr -a release "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a run "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a run-clusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --w=6 --h=6 --gx=4 --gy=2 --blocks=4"
abbr -a runclusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --moves=10 --blocks=4"
abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests"
abbr -a runbenchs "${buildRelease} && sudo cpupower frequency-set --governor performance && ./cmake-build-release/benchmarks; sudo cpupower frequency-set --governor powersave"
abbr -a runbenchs "mv -f benchs.json benchs.old.json; ${buildRelease} && sudo cpupower frequency-set --governor performance && ./cmake-build-release/benchmarks --benchmark_out=benchs.json --benchmark_out_format=console; sudo cpupower frequency-set --governor powersave"
abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings"
abbr -a runvalgrind "${buildDebug} && valgrind --leak-check=full --show-reachable=no --show-leak-kinds=definite,indirect,possible --track-origins=no --suppressions=valgrind.supp --log-file=valgrind.log ./cmake-build-debug/masssprings && cat valgrind.log"
abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data"
@ -135,7 +135,7 @@ rec {
# Define custom dependencies
# ===========================================================================================
raygui = stdenv.mkDerivation (finalAttrs: {
raygui = stdenv.mkDerivation rec {
pname = "raygui";
version = "4.0-unstable-2026-02-24";
@ -163,13 +163,13 @@ rec {
Name: raygui
Description: Simple and easy-to-use immediate-mode gui library
URL: https://github.com/raysan5/raygui
Version: ${finalAttrs.version}
Version: ${version}
Cflags: -I"{includedir}"
EOF
runHook postInstall
'';
});
};
thread-pool = stdenv.mkDerivation {
pname = "thread-pool";
@ -185,8 +185,69 @@ rec {
# Header-only library
dontBuild = true;
installPhase = ''
runHook preInstall
mkdir -p $out
mv ./include $out/include
cp -rv ./include $out/include
runHook postInstall
'';
};
# We can use the pkgs.stdenv for Linux+Windows because it's a header only library.
# The build is required to create the pkg-config/cmake configuration files.
libmorton = stdenv.mkDerivation {
pname = "libmorton";
version = "0.2.12-unstable-2023-05-24";
src = pkgs.fetchFromGitHub {
owner = "Forceflow";
repo = "libmorton";
rev = "7923faa88d7e564020b2d5d408bf8c186ecbe363";
hash = "sha256-5LHiWu2GIuDmfM2gXGbRsFasE7AmVCSRphNdFElbbjk=";
};
nativeBuildInputs = with pkgs; [cmake];
cmakeFlags = [
"-DBUILD_TESTING=OFF"
"-DCMAKE_INSTALL_INCLUDEDIR=include"
"-DCMAKE_INSTALL_DATADIR=share"
];
};
glew-windows = windowsPkgs.stdenv.mkDerivation rec {
pname = "glew-windows";
version = "2.2.0";
src = pkgs.fetchurl {
url = "https://github.com/nigels-com/glew/releases/download/glew-${version}/glew-${version}.tgz";
hash = "sha256-1PyCiTz7ABCVeNChojN/uMozWzzsz5e5flzH8I5DU+E=";
};
nativeBuildInputs = with pkgs; [
cmake
ninja
pkg-config
];
preConfigure = ''
cd build/cmake
'';
cmakeFlags = [
"-DBUILD_UTILS=OFF"
"-DGLEW_OSMESA=OFF"
"-DBUILD_SHARED_LIBS=ON"
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
];
installPhase = ''
runHook preInstall
cmake --install . --prefix "$out"
runHook postInstall
'';
};
@ -224,11 +285,13 @@ rec {
# C/C++:
raylib
raygui
glew
thread-pool
libmorton
boost
# Debugging/Testing/Profiling
tracy-wayland
tracy_0_13
backward-cpp
libbfd
catch2_3
@ -268,15 +331,29 @@ rec {
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib
cp ./${pname} $out/lib/
cp $src/default.puzzle $out/lib/
cp -r $src/fonts $out/lib/fonts
cp -r $src/shader $out/lib/shader
cp -rv $src/default.puzzle $out/lib/
cp -rv $src/fonts $out/lib/fonts
cp -rv $src/shader $out/lib/shader
# The wrapper enters the correct working dir, so fonts/shaders/presets are available
mkdir -p $out/bin
makeWrapper $out/lib/${pname} $out/bin/${pname} --chdir "${placeholder "out"}/lib"
makeWrapper $out/lib/${pname} $out/bin/${pname} --chdir "$out/lib"
# Generate a .desktop file
mkdir -p $out/share/applications
cat <<INI > $out/share/applications/${pname}.desktop
[Desktop Entry]
Terminal=true
Name=PuzzleSpaces
Exec=$out/bin/${pname} %f
Type=Application
INI
runHook postInstall
'';
};
@ -294,7 +371,9 @@ rec {
buildInputs = with windowsPkgs; [
raylib
raygui
glew-windows
thread-pool
libmorton
# Disable stacktrace since that's platform dependant and won't cross compile to windows
# https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/libraries/boost/generic.nix#L43
@ -316,11 +395,15 @@ rec {
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp ./${pname}.exe $out/bin/
cp $src/default.puzzle $out/bin/
cp -r $src/fonts $out/bin/fonts
cp -r $src/shader $out/bin/shader
cp -rv ./${pname}.exe $out/bin/
cp -rv $src/default.puzzle $out/bin/
cp -rv $src/fonts $out/bin/fonts
cp -rv $src/shader $out/bin/shader
runHook postInstall
'';
};
in rec {
@ -364,7 +447,9 @@ rec {
# C/C++:
raylib
raygui
glew
thread-pool
libmorton
boost
# Debugging/Testing/Profiling

69
include/bits.hpp Normal file
View File

@ -0,0 +1,69 @@
#ifndef BITS_HPP_
#define BITS_HPP_
#include "util.hpp"
#include <concepts>
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto create_mask(const u8 first, const u8 last) -> T
{
// If the mask width is equal the type width return all 1s instead of shifting
// as shifting by type-width is undefined behavior.
if (static_cast<size_t>(last - first + 1) >= sizeof(T) * 8) {
return ~T{0};
}
// Example: first=4, last=7, 7-4+1=4
// 1 << 4 = 0b00010000
// 32 - 1 = 0b00001111
// 31 << 4 = 0b11110000
// Subtracting 1 generates a consecutive mask.
return ((T{1} << (last - first + 1)) - 1) << first;
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto clear_bits(T& bits, const u8 first, const u8 last) -> void
{
const T mask = create_mask<T>(first, last);
bits = bits & ~mask;
}
template <class T, class U>
requires std::unsigned_integral<T> && std::unsigned_integral<U>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto set_bits(T& bits, const u8 first, const u8 last, const U value) -> void
{
const T mask = create_mask<T>(first, last);
// Example: first=4, last=6, value=0b1110, bits = 0b 01111110
// mask = 0b 01110000
// bits & ~mask = 0b 00001110
// value << 4 = 0b 11100000
// (value << 4) & mask = 0b 01100000
// (bits & ~mask) | (value << 4) & mask = 0b 01101110
// Insert position: ^^^
// First clear the bits, then | with the value positioned at the insertion point.
// The value may be larger than [first, last], extra bits are ignored.
bits = (bits & ~mask) | ((static_cast<T>(value) << first) & mask);
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto get_bits(const T bits, const u8 first, const u8 last) -> T
{
const T mask = create_mask<T>(first, last);
// We can >> without sign extension because T is unsigned_integral
return (bits & mask) >> first;
}
auto print_bitmap(u64 bitmap, u8 w, u8 h, const std::string& title) -> void;
#endif

View File

@ -3,9 +3,9 @@
#include <raylib.h>
// TODO: Using the octree from the last frame completely breaks the physics :/
// #define ASYNC_OCTREE
// Calculate the octree parallel to the layout calculation.
// Layout uses the octree from last frame.
#define ASYNC_OCTREE
// Gets set by CMake
// #define THREADPOOL // Enable physics threadpool
@ -23,6 +23,7 @@
#define BS_THREAD_POOL_NATIVE_EXTENSIONS
// ReSharper disable once CppUnusedIncludeDirective
#include <BS_thread_pool.hpp>
using threadpool = std::optional<BS::thread_pool<>* const>;
#if defined(_WIN32) // raylib uses these names as function parameters
#undef near
#undef far
@ -46,12 +47,14 @@ constexpr int FONT_SIZE = 26;
// Camera Controls
constexpr float CAMERA_FOV = 90.0;
constexpr float FOV_SPEED = 1.0;
constexpr float FOV_MULTIPLIER = 4.0;
constexpr float MIN_FOV = 10.0;
constexpr float MAX_FOV = 180.0;
constexpr float MAX_PERSP_FOV = 120.0;
constexpr float MAX_ORTHO_FOV = 540.0;
constexpr float CAMERA_DISTANCE = 150.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float MIN_CAMERA_DISTANCE = 2.0;
constexpr float MAX_CAMERA_DISTANCE = 2000.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float ZOOM_MULTIPLIER = 4.0;
constexpr float PAN_SPEED = 2.0;
constexpr float PAN_MULTIPLIER = 10.0;
@ -63,24 +66,27 @@ constexpr float TARGET_UPS = 90; // How often to update physics
constexpr float TIMESTEP = 1.0 / TARGET_UPS; // Update interval in seconds
constexpr float SIM_SPEED = 4.0; // How large each update should be
constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_CONSTANT = 5.0; // Mass spring system
constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system
constexpr float SPRING_K = 4.0; // Mass spring system
constexpr float DAMPENING_K = 1.5; // Mass spring system
constexpr float REST_LENGTH = 3.0; // Mass spring system
constexpr float VERLET_DAMPENING = 0.05; // [0, 1]
constexpr float VERLET_DAMPENING = 0.1; // [0, 1]
constexpr float BH_FORCE = 2.5; // Barnes-Hut [1.0, 3.0]
constexpr float THETA = 0.8; // Barnes-Hut [0.5, 1.0]
constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0]
constexpr float THETA = 1.0; // Barnes-Hut [0.5, 1.0]
constexpr float SOFTENING = 0.05; // Barnes-Hut [0.01, 1.0]
// Graph Drawing
constexpr Color EDGE_COLOR = DARKBLUE;
constexpr float VERTEX_SIZE = 0.5;
static const Color VERTEX_COLOR = Fade(BLUE, 0.5);
constexpr Color VERTEX_VISITED_COLOR = DARKBLUE;
constexpr Color VERTEX_PATH_COLOR = GREEN;
constexpr Color VERTEX_TARGET_COLOR = RED;
static const Color EDGE_COLOR = Fade(BLUE, 0.3);
constexpr int DRAW_EDGES_LIMIT = 5'000'000;
constexpr float VERTEX_SIZE = 0.75;
constexpr int DRAW_VERTICES_LIMIT = 1'000'000;
static const Color VERTEX_COLOR = Fade(BLUE, 0.8);
constexpr Color VERTEX_VISITED_COLOR = ORANGE;
constexpr Color VERTEX_START_COLOR = ORANGE;
constexpr Color VERTEX_CURRENT_COLOR = PURPLE;
constexpr int DRAW_VERTICES_LIMIT = 1000000;
constexpr Color VERTEX_CURRENT_COLOR = ORANGE;
constexpr Color VERTEX_PATH_COLOR = GREEN;
constexpr Color VERTEX_TARGET_COLOR = GREEN;
static const Color VERTEX_CLOSEST_COLOR = Fade(PINK, 1.0);
static const Color VERTEX_FARTHEST_COLOR = Fade(DARKBLUE, 0.8);
// Klotski Drawing
constexpr int BOARD_PADDING = 10;
@ -91,4 +97,8 @@ constexpr Color BLOCK_COLOR = DARKBLUE;
constexpr Color TARGET_BLOCK_COLOR = RED;
constexpr Color WALL_COLOR = BLACK;
// Threadpool
static constexpr int SMALL_TASK_BLOCK_SIZE = 256; // Weirdly larger blocks decrease performance...
static constexpr int LARGE_TASK_BLOCK_SIZE = 256;
#endif

View File

@ -2,6 +2,8 @@
#define PHYSICS_HPP_
#include "config.hpp"
#include "cpu_spring_system.hpp"
#include "util.hpp"
#include <atomic>
#include <condition_variable>
@ -13,7 +15,7 @@
#include <variant>
#include <vector>
class threaded_physics
class cpu_layout_engine
{
struct add_mass
{};
@ -57,24 +59,21 @@ class threaded_physics
};
private:
std::optional<BS::thread_pool<>* const> thread_pool;
threadpool thread_pool;
std::thread physics;
public:
physics_state state;
public:
explicit threaded_physics(
const std::optional<BS::thread_pool<>* const> _thread_pool = std::nullopt)
explicit cpu_layout_engine(
const threadpool _thread_pool = std::nullopt)
: thread_pool(_thread_pool), physics(physics_thread, std::ref(state), std::ref(thread_pool))
{}
threaded_physics(const threaded_physics& copy) = delete;
auto operator=(const threaded_physics& copy) -> threaded_physics& = delete;
threaded_physics(threaded_physics&& move) = delete;
auto operator=(threaded_physics&& move) -> threaded_physics& = delete;
NO_COPY_NO_MOVE(cpu_layout_engine);
~threaded_physics()
~cpu_layout_engine()
{
state.running = false;
state.data_ready_cnd.notify_all();
@ -88,14 +87,14 @@ private:
#endif
static auto physics_thread(physics_state& state,
std::optional<BS::thread_pool<>* const> thread_pool) -> void;
threadpool thread_pool) -> void;
public:
auto clear_cmd() -> void;
auto add_mass_cmd() -> void;
auto add_spring_cmd(size_t a, size_t b) -> void;
auto add_mass_springs_cmd(size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void;
const std::vector<spring>& springs) -> void;
};
#endif
#endif

View File

@ -0,0 +1,47 @@
#ifndef MASS_SPRING_SYSTEM_HPP_
#define MASS_SPRING_SYSTEM_HPP_
#include "octree.hpp"
#include "config.hpp"
#include <optional>
#include <raylib.h>
using spring = std::pair<size_t, size_t>;
class cpu_spring_system
{
public:
octree tree;
// This is the main ownership of all the states/masses/springs.
std::vector<Vector3> positions;
std::vector<Vector3> previous_positions; // for verlet integration
std::vector<Vector3> velocities;
std::vector<Vector3> forces;
std::vector<spring> springs;
public:
cpu_spring_system() {}
NO_COPY_NO_MOVE(cpu_spring_system);
public:
auto clear() -> void;
auto add_mass() -> void;
auto add_spring(size_t a, size_t b) -> void;
auto clear_forces() -> void;
auto calculate_spring_force(size_t s) -> void;
auto calculate_spring_forces(threadpool thread_pool = std::nullopt) -> void;
auto calculate_repulsion_forces(threadpool thread_pool = std::nullopt) -> void;
auto integrate_velocity(size_t m, float dt) -> void;
auto integrate_position(size_t m, float dt) -> void;
auto verlet_update(size_t m, float dt) -> void;
auto update(float dt, threadpool thread_pool = std::nullopt) -> void;
auto center_masses(threadpool thread_pool = std::nullopt) -> void;
};
#endif

View File

@ -1,7 +1,8 @@
#ifndef DISTANCE_HPP_
#define DISTANCE_HPP_
#include <cstddef>
#include "cpu_spring_system.hpp"
#include <vector>
class graph_distances
@ -15,7 +16,8 @@ public:
auto clear() -> void;
[[nodiscard]] auto empty() const -> bool;
auto calculate_distances(size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges,
auto calculate_distances(size_t node_count,
const std::vector<spring>& edges,
const std::vector<size_t>& targets) -> void;
[[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector<size_t>;

View File

@ -22,8 +22,7 @@ struct show_yes_no_message
std::function<void()> on_yes;
};
struct show_save_preset_window
{};
struct show_save_preset_window {};
using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>;
@ -75,6 +74,7 @@ public:
bool mark_path = false;
bool mark_solutions = false;
bool connect_solutions = false;
bool color_by_distance = false;
// Camera
bool camera_lock = true;
@ -86,16 +86,17 @@ public:
Vector2 mouse = Vector2Zero();
Vector2 last_mouse = Vector2Zero();
// State selection from graph
size_t collision_mass = -1;
public:
input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera)
input_handler(state_manager& _state, orbit_camera& _camera)
: state(_state), camera(_camera)
{
init_handlers();
}
input_handler(const input_handler& copy) = delete;
auto operator=(const input_handler& copy) -> input_handler& = delete;
input_handler(input_handler&& move) = delete;
auto operator=(input_handler&& move) -> input_handler& = delete;
NO_COPY_NO_MOVE(input_handler);
private:
auto init_handlers() -> void;
@ -122,6 +123,7 @@ public:
auto add_block() -> void;
auto remove_block() -> void;
auto place_goal() const -> void;
auto select_state() const -> void;
// Key actions
auto toggle_camera_lock() -> void;
@ -139,6 +141,7 @@ public:
auto clear_graph() -> void;
auto toggle_mark_solutions() -> void;
auto toggle_connect_solutions() -> void;
auto toggle_color_by_distance() -> void;
auto toggle_mark_path() -> void;
auto goto_optimal_next_state() const -> void;
auto goto_most_distant_state() const -> void;
@ -158,18 +161,14 @@ public:
// General
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button, const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_released_handler(MouseButton button,
const std::function<void(input_handler&)>& handler)
-> void;
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_pressed_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_pressed_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
auto handle_input() -> void;
};

View File

@ -1,63 +0,0 @@
#ifndef MASS_SPRING_SYSTEM_HPP_
#define MASS_SPRING_SYSTEM_HPP_
#include "octree.hpp"
#include "config.hpp"
#include <optional>
#include <raylib.h>
class mass_spring_system
{
public:
class spring
{
public:
size_t a;
size_t b;
public:
spring(const size_t _a, const size_t _b)
: a(_a), b(_b) {}
};
public:
static constexpr int SMALL_TASK_BLOCK_SIZE = 256;
static constexpr int LARGE_TASK_BLOCK_SIZE = 256;
octree tree;
// This is the main ownership of all the states/masses/springs.
std::vector<Vector3> positions;
std::vector<Vector3> previous_positions; // for verlet integration
std::vector<Vector3> velocities;
std::vector<Vector3> forces;
std::vector<spring> springs;
public:
mass_spring_system() {}
mass_spring_system(const mass_spring_system& copy) = delete;
auto operator=(const mass_spring_system& copy) -> mass_spring_system& = delete;
mass_spring_system(mass_spring_system& move) = delete;
auto operator=(mass_spring_system&& move) -> mass_spring_system& = delete;
public:
auto clear() -> void;
auto add_mass() -> void;
auto add_spring(size_t a, size_t b) -> void;
auto clear_forces() -> void;
auto calculate_spring_force(size_t s) -> void;
auto calculate_spring_forces(std::optional<BS::thread_pool<>* const> thread_pool = std::nullopt) -> void;
auto calculate_repulsion_forces(std::optional<BS::thread_pool<>* const> thread_pool = std::nullopt) -> void;
auto integrate_velocity(size_t m, float dt) -> void;
auto integrate_position(size_t m, float dt) -> void;
auto verlet_update(size_t m, float dt) -> void;
auto update(float dt, std::optional<BS::thread_pool<>* const> thread_pool = std::nullopt) -> void;
auto center_masses(std::optional<BS::thread_pool<>* const> thread_pool = std::nullopt) -> void;
};
#endif

View File

@ -1,11 +1,15 @@
#ifndef OCTREE_HPP_
#define OCTREE_HPP_
#include "util.hpp"
#include "config.hpp"
#include <array>
#include <vector>
#include <raylib.h>
#include <raymath.h>
#include <libmorton/morton.h>
class octree
{
@ -14,38 +18,193 @@ class octree
public:
Vector3 mass_center = Vector3Zero();
float mass_total = 0.0;
Vector3 box_min = Vector3Zero(); // area start
Vector3 box_max = Vector3Zero(); // area end
u8 depth = 0;
float size = 0.0f; // Because our octree cells are cubic we don't need to store the bounds
std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1};
int mass_id = -1;
bool leaf = true;
public:
[[nodiscard]] auto child_count() const -> int;
};
public:
static constexpr int MAX_DEPTH = 20;
private:
// 21 * 3 = 63, fits in u64 for combined x/y/z morton-code
static constexpr int MAX_DEPTH = 21;
std::vector<node> nodes;
// This approach is actually slower than the array of nodes
// beacuse we access all the attributes in the same function
// std::vector<Vector3> mass_centers;
// std::vector<float> mass_totals;
// std::vector<Vector3> box_mins;
// std::vector<Vector3> box_maxs;
// std::vector<std::array<int, 8>> childrens;
// std::vector<int> mass_ids;
// std::vector<u8> leafs; // bitpacked std::vector<bool> is a lot slower
public:
octree() = default;
// octree(const octree& copy) = delete;
// auto operator=(const octree& copy) -> octree& = delete;
// octree(octree&& move) = delete;
// auto operator=(octree&& move) -> octree& = delete;
// Required for async octree
// NO_COPY_NO_MOVE(octree);
private:
[[nodiscard]] INLINE static inline auto get_octant(const Vector3& box_min,
const Vector3& box_max,
const Vector3& pos) -> int;
[[nodiscard]] INLINE static inline auto get_child_bounds(const Vector3& box_min,
const Vector3& box_max,
int octant) -> std::pair<Vector3, Vector3>;
// Map a floating point coordinate to a discrete integer (so its morton-code can be computed)
// The "bits" parameter determines the discrete axis resolution
[[nodiscard]] INLINE static inline auto quantize_axis(float coordinate,
float box_min,
float box_max,
int bits) -> u32;
[[nodiscard]] INLINE static inline auto pos_to_morton(const Vector3& p,
const Vector3& root_min,
const Vector3& root_max) -> u64;
[[nodiscard]] INLINE static inline auto jitter_pos(Vector3 p,
u32 seed,
const Vector3& root_min,
const Vector3& root_max,
float root_extent) -> Vector3;
// Use this to obtain an ancestor node of a leaf node (on any level).
// Because the morton codes (interleaved coordinates) encode the octree path, we can take
// the morten code of any leaf and only take the 3*n first interleaved bits to find the
// leaf ancestor on level n.
// Leaf Code: [101 110 100 001] -> Ancestors (from leaf to root):
// - [101 110 100]
// - [101 110]
// - [101] (root)
[[nodiscard]] INLINE static inline auto path_to_ancestor(u64 leaf_code, int leaf_depth, int depth) -> u64;
// Use this to obtain the octant a leaf node is contained in (on any level).
// The 3 interleaved bits in the morten code encode the octant [0, 7].
// Leaf Code: [101 110 100 001] -> Octants:
// - [100] (Level 2)
// - [110] (Level 1)
// - [101] (Level 0)
[[nodiscard]] INLINE static inline auto octant_at_level(u64 leaf_code, int level, int leaf_depth) -> int;
public:
[[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int;
[[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
-> std::pair<Vector3, Vector3>;
auto create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int;
auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void;
static auto build_octree(octree& t, const std::vector<Vector3>& positions) -> void;
auto clear() -> void;
auto reserve(size_t count) -> void;
[[nodiscard]] auto empty() const -> bool;
[[nodiscard]] auto root() const -> const node&;
[[nodiscard]] auto calculate_force(int node_idx, const Vector3& pos) const -> Vector3;
// Morton/linear octree implementation
static auto build_octree_morton(octree& t,
const std::vector<Vector3>& positions,
const std::optional<BS::thread_pool<>*>& thread_pool) -> void;
[[nodiscard]] auto calculate_force_morton(int node_idx, const Vector3& pos, int self_id) const -> Vector3;
};
INLINE inline auto octree::get_octant(const Vector3& box_min, const Vector3& box_max, const Vector3& pos) -> int
{
auto [cx, cy, cz] = (box_min + box_max) / 2.0f;
// The octant is encoded as a 3-bit integer "zyx". The node area is split
// along all 3 axes, if a position is right of an axis, this bit is set to 1.
// If a position is right of the x-axis and y-axis and left of the z-axis, the
// encoded octant is "011".
return (pos.x >= cx) | ((pos.y >= cy) << 1) | ((pos.z >= cz) << 2);
}
INLINE inline auto octree::get_child_bounds(const Vector3& box_min,
const Vector3& box_max,
const int octant) -> std::pair<Vector3, Vector3>
{
auto [cx, cy, cz] = (box_min + box_max) / 2.0f;
Vector3 min = Vector3Zero();
Vector3 max = Vector3Zero();
// If (octant & 1), the octant is to the right of the node region's x-axis
// (see GetOctant). This means the left bound is the x-axis and the right
// bound the node's region max.
min.x = octant & 1 ? cx : box_min.x;
max.x = octant & 1 ? box_max.x : cx;
min.y = octant & 2 ? cy : box_min.y;
max.y = octant & 2 ? box_max.y : cy;
min.z = octant & 4 ? cz : box_min.z;
max.z = octant & 4 ? box_max.z : cz;
return std::make_pair(min, max);
}
INLINE inline auto octree::quantize_axis(const float coordinate,
const float box_min,
const float box_max,
const int bits) -> u32
{
const float extent = box_max - box_min;
if (extent <= 0.0f) {
return 0;
}
float normalized = (coordinate - box_min) / extent; // normalize to [0,1]
normalized = std::max(0.0f, std::min(normalized, std::nextafter(1.0f, 0.0f))); // avoid exactly 1.0
// bits up to 21 => (1u << bits) safe in 32-bit
const u32 grid_max = (1u << bits) - 1u;
return static_cast<u32>(normalized * static_cast<float>(grid_max));
}
INLINE inline auto octree::pos_to_morton(const Vector3& p, const Vector3& root_min, const Vector3& root_max) -> u64
{
const u32 x = quantize_axis(p.x, root_min.x, root_max.x, MAX_DEPTH);
const u32 y = quantize_axis(p.y, root_min.y, root_max.y, MAX_DEPTH);
const u32 z = quantize_axis(p.z, root_min.z, root_max.z, MAX_DEPTH);
return libmorton::morton3D_64_encode(x, y, z);
}
INLINE inline auto octree::jitter_pos(Vector3 p,
const u32 seed,
const Vector3& root_min,
const Vector3& root_max,
const float root_extent) -> Vector3
{
// Use a hash to calculate a deterministic jitter: The same position should always get the same jitter.
// We want this to get stable physics, particles at the same position shouldn't get different jitters
// across frames...
u32 h = (seed ^ 61u) ^ (seed >> 16);
h *= 9u;
h = h ^ (h >> 4);
h *= 0x27d4eb2du;
h = h ^ (h >> 15);
// finest cell size at depth L
const float finest_cell = root_extent / static_cast<float>(1u << MAX_DEPTH);
const float s = finest_cell * 1e-4f; // small pp
p.x += (h & 1u) ? +s : -s;
p.y += (h & 2u) ? +s : -s;
p.z += (h & 4u) ? +s : -s;
// clamp back into bounds just in case
p.x = std::max(root_min.x, std::min(p.x, root_max.x));
p.y = std::max(root_min.y, std::min(p.y, root_max.y));
p.z = std::max(root_min.z, std::min(p.z, root_max.z));
return p;
}
INLINE inline auto octree::path_to_ancestor(const u64 leaf_code, const int leaf_depth, const int depth) -> u64
{
// keep top 3*depth bits; drop the rest
const int drop = 3 * (leaf_depth - depth);
return (drop > 0) ? (leaf_code >> drop) : leaf_code;
}
INLINE inline auto octree::octant_at_level(const u64 leaf_code, const int level, const int leaf_depth) -> int
{
// level 1 => child of root => topmost 3 bits
const int shift = 3 * (leaf_depth - level);
return static_cast<int>((leaf_code >> shift) & 0x7ull);
}
#endif

View File

@ -24,8 +24,7 @@ public:
auto pan(Vector2 last_mouse, Vector2 mouse) -> void;
auto update(const Vector3& current_target, const Vector3& mass_center, bool lock,
bool mass_center_lock) -> void;
auto update(const Vector3& current_target, const Vector3& mass_center, bool lock, bool mass_center_lock) -> void;
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -14,55 +14,73 @@ class renderer
{
private:
const state_manager& state;
const input_handler& input;
input_handler& input;
user_interface& gui;
const orbit_camera& camera;
RenderTexture graph_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture graph_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
// TODO: Those should be moved to the user_interface.h
RenderTexture klotski_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Batching
// Edges
unsigned int edge_vao_id = 0;
unsigned int edge_vbo_id = 0;
std::vector<Vector3> edge_vertices;
Shader edge_shader = LoadShader("shader/edge_vertex.glsl", "shader/edge_fragment.glsl");
int edge_color_loc = -1;
std::vector<std::pair<Vector3, Vector3>> connections;
// Instancing
// Vertex instancing
static constexpr int INSTANCE_COLOR_ATTR = 5;
std::vector<Matrix> transforms;
std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
Shader instancing_shader =
LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
unsigned int color_vbo_id = 0;
public:
renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input,
user_interface& _gui)
// TODO: I am allocating HUGE vertex buffers instead of resizing dynamically...
// Edges: 5'000'000 * 2 * 12 Byte ~= 115 MB
// Verts: 1'000'000 * 16 Byte ~= 15 MB
// This is also allocated on the CPU by the vectors
renderer(const orbit_camera& _camera, const state_manager& _state, input_handler& _input, user_interface& _gui)
: state(_state), input(_input), gui(_gui), camera(_camera)
{
// Edges
edge_shader.locs[SHADER_LOC_VERTEX_POSITION] = GetShaderLocationAttrib(edge_shader, "vertexPosition");
edge_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(edge_shader, "mvp");
edge_shader.locs[SHADER_LOC_COLOR_DIFFUSE] = GetShaderLocation(edge_shader, "colDiffuse");
edge_color_loc = GetShaderLocation(edge_shader, "colDiffuse");
edge_vertices.reserve(DRAW_EDGES_LIMIT * 2);
edge_vao_id = rlLoadVertexArray();
edge_vbo_id = rlLoadVertexBuffer(nullptr, DRAW_EDGES_LIMIT * 2 * sizeof(Vector3), true);
rlEnableVertexArray(edge_vao_id);
rlEnableVertexBuffer(edge_vbo_id);
rlSetVertexAttribute(0, 3, RL_FLOAT, false, sizeof(Vector3), 0);
rlEnableVertexAttribute(0);
rlDisableVertexBuffer();
rlDisableVertexArray();
// Vertex instancing
instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
GetShaderLocationAttrib(instancing_shader, "instanceTransform");
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] =
GetShaderLocation(instancing_shader, "viewPos");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib(
instancing_shader,
"instanceTransform");
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos");
// infoln("LOC vertexPosition: {}",
// rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
// infoln("LOC instanceTransform: {}",
// rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
// infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor"));
// vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR;
vertex_mat.shader = instancing_shader;
transforms.reserve(DRAW_VERTICES_LIMIT);
colors.reserve(DRAW_VERTICES_LIMIT);
color_vbo_id = rlLoadVertexBuffer(colors.data(), DRAW_VERTICES_LIMIT * sizeof(Color), true);
color_vbo_id = rlLoadVertexBuffer(nullptr, DRAW_VERTICES_LIMIT * sizeof(Color), true);
rlEnableVertexArray(cube_instance.vaoId);
rlEnableVertexBuffer(color_vbo_id);
@ -74,10 +92,7 @@ public:
rlDisableVertexArray();
}
renderer(const renderer& copy) = delete;
auto operator=(const renderer& copy) -> renderer& = delete;
renderer(renderer&& move) = delete;
auto operator=(renderer&& move) -> renderer& = delete;
NO_COPY_NO_MOVE(renderer);
~renderer()
{
@ -85,12 +100,19 @@ public:
UnloadRenderTexture(klotski_target);
UnloadRenderTexture(menu_target);
// Edges
rlUnloadVertexArray(edge_vao_id);
rlUnloadVertexBuffer(edge_vbo_id);
UnloadShader(edge_shader);
// Instancing
UnloadMaterial(vertex_mat);
UnloadMesh(cube_instance);
// I think the shader already gets unloaded with the material?
// UnloadShader(instancing_shader);
rlUnloadVertexBuffer(color_vbo_id);
}
private:
@ -102,8 +124,7 @@ private:
auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
public:
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count,
size_t spring_count) -> void;
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void;
};
#endif

View File

@ -3,7 +3,7 @@
#include "graph_distances.hpp"
#include "load_save.hpp"
#include "threaded_physics.hpp"
#include "cpu_layout_engine.hpp"
#include "puzzle.hpp"
#include <boost/unordered/unordered_flat_map.hpp>
@ -12,7 +12,7 @@
class state_manager
{
private:
threaded_physics& physics;
cpu_layout_engine& physics;
std::string preset_file;
size_t current_preset = 0;
@ -22,9 +22,9 @@ private:
// State storage (store states twice for bidirectional lookup).
// Everything else should only store indices to state_pool.
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
boost::unordered_flat_map<puzzle, size_t, puzzle_hasher> state_indices; // Maps states to indices
std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
puzzlemap<size_t> state_indices; // Maps states to indices
std::vector<spring> links; // Indices are equal to mass_springs springs indices
graph_distances node_target_distances; // Buffered and reused if the graph doesn't change
boost::unordered_flat_set<size_t> winning_indices; // Indices of all states where the board is solved
@ -42,16 +42,13 @@ private:
bool edited = false;
public:
state_manager(threaded_physics& _physics, const std::string& _preset_file)
state_manager(cpu_layout_engine& _physics, const std::string& _preset_file)
: physics(_physics), preset_file(_preset_file)
{
reload_preset_file();
}
state_manager(const state_manager& copy) = delete;
auto operator=(const state_manager& copy) -> state_manager& = delete;
state_manager(state_manager&& move) = delete;
auto operator=(state_manager&& move) -> state_manager& = delete;
NO_COPY_NO_MOVE(state_manager);
private:
/**
@ -83,8 +80,7 @@ private:
* @param states List of states to insert
* @param _links List of links to insert
*/
auto synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void;
auto synced_insert_statespace(const std::vector<puzzle>& states, const std::vector<spring>& _links) -> void;
/**
* Clears all states and links (and related) from the state_manager and the physics system.
@ -130,7 +126,7 @@ public:
[[nodiscard]] auto get_target_count() const -> size_t;
[[nodiscard]] auto get_link_count() const -> size_t;
[[nodiscard]] auto get_path_length() const -> size_t;
[[nodiscard]] auto get_links() const -> const std::vector<std::pair<size_t, size_t>>&;
[[nodiscard]] auto get_links() const -> const std::vector<spring>&;
[[nodiscard]] auto get_winning_indices() const -> const boost::unordered_flat_set<size_t>&;
[[nodiscard]] auto get_visit_counts() const -> const boost::unordered_flat_map<size_t, int>&;
[[nodiscard]] auto get_winning_path() const -> const std::vector<size_t>&;
@ -141,6 +137,7 @@ public:
[[nodiscard]] auto get_current_preset_comment() const -> const std::string&;
[[nodiscard]] auto has_history() const -> bool;
[[nodiscard]] auto has_distances() const -> bool;
[[nodiscard]] auto get_distances() const -> std::vector<int>;
[[nodiscard]] auto get_total_moves() const -> size_t;
[[nodiscard]] auto was_edited() const -> bool;
};

View File

@ -22,15 +22,17 @@ class user_interface
const int padding;
public:
grid(const int _x, const int _y, const int _width, const int _height, const int _columns,
const int _rows, const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows),
padding(_padding)
{}
grid(const int _x,
const int _y,
const int _width,
const int _height,
const int _columns,
const int _rows,
const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), padding(_padding) {}
public:
auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows)
-> void;
auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows) -> void;
auto update_bounds(int _x, int _y, int _width, int _height) -> void;
auto update_bounds(int _x, int _y) -> void;
@ -38,8 +40,7 @@ class user_interface
[[nodiscard]] auto bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
[[nodiscard]] auto square_bounds() const -> Rectangle;
[[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const
-> Rectangle;
[[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
};
struct style
@ -87,14 +88,17 @@ private:
grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD);
grid board_grid =
grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().get_width(), state.get_current_state().get_height(), BOARD_PADDING);
grid board_grid = grid(0,
MENU_HEIGHT,
GetScreenWidth() / 2,
GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().get_width(),
state.get_current_state().get_height(),
BOARD_PADDING);
grid graph_overlay_grid = grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD);
grid debug_overlay_grid =
grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD);
grid debug_overlay_grid = grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD);
// Windows
@ -114,10 +118,7 @@ public:
init();
}
user_interface(const user_interface& copy) = delete;
auto operator=(const user_interface& copy) -> user_interface& = delete;
user_interface(user_interface&& move) = delete;
auto operator=(user_interface&& move) -> user_interface& = delete;
NO_COPY_NO_MOVE(user_interface);
private:
static auto init() -> void;
@ -133,32 +134,68 @@ private:
[[nodiscard]] static auto popup_bounds() -> Rectangle;
auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true,
auto draw_button(Rectangle bounds,
const std::string& label,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x, int y, int width, int height, const std::string& label,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x,
int y,
int width,
int height,
const std::string& label,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds,
const std::string& off_label,
const std::string& on_label,
int* active,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label,
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_toggle_slider(int x,
int y,
int width,
int height,
const std::string& off_label,
const std::string& on_label,
int* active,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds,
const std::string& label,
int* value,
int min,
int max,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label,
int* value, int min, int max, Color color, bool enabled = true,
auto draw_menu_spinner(int x,
int y,
int width,
int height,
const std::string& label,
int* value,
int min,
int max,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true,
auto draw_label(Rectangle bounds,
const std::string& text,
Color color,
bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_board_block(int x, int y, int width, int height, Color color,
bool enabled = true) const -> bool;
auto draw_board_block(int x, int y, int width, int height, Color color, bool enabled = true) const -> bool;
[[nodiscard]] auto window_open() const -> bool;

View File

@ -1,13 +1,67 @@
#ifndef UTIL_HPP_
#define UTIL_HPP_
#include <vector>
#include <iostream>
#include <raylib.h>
#define INLINE __attribute__((always_inline))
#define PACKED __attribute__((packed))
enum ctrl
#define STARTTIME const auto start = std::chrono::high_resolution_clock::now()
#define ENDTIME(msg, cast, unit) const auto end = std::chrono::high_resolution_clock::now(); \
infoln("{}. Took {}{}.", msg, std::chrono::duration_cast<cast>(end - start).count(), unit)
#define COMMENT if (false)
#define NO_COPY_NO_MOVE(typename) \
typename(const typename& copy) = delete; \
auto operator=(const typename& copy) -> typename& = delete; \
typename(typename&& move) = delete; \
auto operator=(typename&& move) -> typename& = delete;
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using u64 = uint64_t;
using i8 = int8_t;
using i16 = int16_t;
using i32 = int32_t;
using i64 = int64_t;
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
};
inline auto binom(const int n, const int k) -> int
{
std::vector<int> solutions(k);
solutions[0] = n - k + 1;
for (int i = 1; i < k; ++i) {
solutions[i] = solutions[i - 1] * (n - k + 1 + i) / (i + 1);
}
return solutions[k - 1];
}
// Enums
enum dir : u8
{
nor = 1 << 0,
eas = 1 << 1,
sou = 1 << 2,
wes = 1 << 3,
};
// Ansi
enum class ctrl : u8
{
reset = 0,
bold_bright = 1,
@ -18,120 +72,40 @@ enum ctrl
inverse_off = 27
};
enum fg
enum class fg : u8
{
fg_black = 30,
fg_red = 31,
fg_green = 32,
fg_yellow = 33,
fg_blue = 34,
fg_magenta = 35,
fg_cyan = 36,
fg_white = 37
black = 30,
red = 31,
green = 32,
yellow = 33,
blue = 34,
magenta = 35,
cyan = 36,
white = 37
};
enum bg
enum class bg : u8
{
bg_black = 40,
bg_red = 41,
bg_green = 42,
bg_yellow = 43,
bg_blue = 44,
bg_magenta = 45,
bg_cyan = 46,
bg_white = 47
black = 40,
red = 41,
green = 42,
yellow = 43,
blue = 44,
magenta = 45,
cyan = 46,
white = 47
};
inline auto ansi_bold_fg(const fg color) -> std::string
{
return std::format("\033[{};{}m", static_cast<int>(bold_bright), static_cast<int>(color));
return std::format("\033[{};{}m", static_cast<int>(ctrl::bold_bright), static_cast<int>(color));
}
inline auto ansi_reset() -> std::string
{
return std::format("\033[{}m", static_cast<int>(reset));
return std::format("\033[{}m", static_cast<int>(ctrl::reset));
}
// Bit shifting + masking
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto create_mask(const uint8_t first, const uint8_t last) -> T
{
// If the mask width is equal the type width return all 1s instead of shifting
// as shifting by type-width is undefined behavior.
if (static_cast<size_t>(last - first + 1) >= sizeof(T) * 8) {
return ~T{0};
}
// Example: first=4, last=7, 7-4+1=4
// 1 << 4 = 0b00010000
// 32 - 1 = 0b00001111
// 31 << 4 = 0b11110000
// Subtracting 1 generates a consecutive mask.
return ((T{1} << (last - first + 1)) - 1) << first;
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto clear_bits(T& bits, const uint8_t first, const uint8_t last) -> void
{
const T mask = create_mask<T>(first, last);
bits = bits & ~mask;
}
template <class T, class U>
requires std::unsigned_integral<T> && std::unsigned_integral<U>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto set_bits(T& bits, const uint8_t first, const uint8_t last, const U value) -> void
{
const T mask = create_mask<T>(first, last);
// Example: first=4, last=6, value=0b1110, bits = 0b 01111110
// mask = 0b 01110000
// bits & ~mask = 0b 00001110
// value << 4 = 0b 11100000
// (value << 4) & mask = 0b 01100000
// (bits & ~mask) | (value << 4) & mask = 0b 01101110
// Insert position: ^^^
// First clear the bits, then | with the value positioned at the insertion point.
// The value may be larger than [first, last], extra bits are ignored.
bits = (bits & ~mask) | ((static_cast<T>(value) << first) & mask);
}
template <class T>
requires std::unsigned_integral<T>
// ReSharper disable once CppRedundantInlineSpecifier
INLINE inline auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T
{
const T mask = create_mask<T>(first, last);
// We can >> without sign extension because T is unsigned_integral
return (bits & mask) >> first;
}
// std::variant visitor
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
};
// Enums
enum direction
{
nor = 1 << 0,
eas = 1 << 1,
sou = 1 << 2,
wes = 1 << 3,
};
// Output
inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream&
@ -150,44 +124,33 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
template <typename... Args>
auto traceln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}TRACE{}]: ", ansi_bold_fg(fg_cyan), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
std::cout << std::format("[{}TRACE{}]: ", ansi_bold_fg(fg::cyan), ansi_reset()) << std::format(
fmt,
std::forward<Args>(args)...) << std::endl;
}
template <typename... Args>
auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg::blue), ansi_reset()) << std::format(
fmt,
std::forward<Args>(args)...) << std::endl;
}
template <typename... Args>
auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg::yellow), ansi_reset()) << std::format(
fmt,
std::forward<Args>(args)...) << std::endl;
}
template <typename... Args>
auto errln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
}
inline auto print_bitmap(const uint64_t bitmap, const uint8_t w, const uint8_t h, const std::string& title) -> void
{
traceln("{}:", title);
traceln("{}", std::string(2 * w - 1, '='));
for (size_t y = 0; y < w; ++y) {
std::cout << " ";
for (size_t x = 0; x < h; ++x) {
std::cout << static_cast<int>(get_bits(bitmap, y * w + x, y * h + x)) << " ";
}
std::cout << "\n";
}
std::cout << std::flush;
traceln("{}", std::string(2 * w - 1, '='));
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg::red), ansi_reset()) << std::format(
fmt,
std::forward<Args>(args)...) << std::endl;
}
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 795 KiB

View File

@ -0,0 +1,9 @@
#version 330
uniform vec4 colDiffuse;
out vec4 finalColor;
void main()
{
finalColor = colDiffuse;
}

9
shader/edge_vertex.glsl Normal file
View File

@ -0,0 +1,9 @@
#version 330
in vec3 vertexPosition;
uniform mat4 mvp;
void main()
{
gl_Position = mvp * vec4(vertexPosition, 1.0);
}

15
src/bits.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "bits.hpp"
auto print_bitmap(const u64 bitmap, const u8 w, const u8 h, const std::string& title) -> void {
traceln("{}:", title);
traceln("{}", std::string(2 * w - 1, '='));
for (size_t y = 0; y < h; ++y) {
std::cout << " ";
for (size_t x = 0; x < w; ++x) {
std::cout << static_cast<int>(get_bits(bitmap, y * w + x, y * w + x)) << " ";
}
std::cout << "\n";
}
std::cout << std::flush;
traceln("{}", std::string(2 * w - 1, '='));
}

View File

@ -1,6 +1,6 @@
#include "threaded_physics.hpp"
#include "cpu_layout_engine.hpp"
#include "config.hpp"
#include "mass_spring_system.hpp"
#include "cpu_spring_system.hpp"
#include "util.hpp"
#include <chrono>
@ -10,16 +10,16 @@
#include <vector>
#ifdef ASYNC_OCTREE
auto threaded_physics::set_octree_pool_thread_name(size_t idx) -> void
auto cpu_layout_engine::set_octree_pool_thread_name(size_t idx) -> void
{
BS::this_thread::set_os_thread_name(std::format("octree-{}", idx));
// traceln("Using thread \"{}\"", BS::this_thread::get_os_thread_name().value_or("INVALID NAME"));
}
#endif
auto threaded_physics::physics_thread(physics_state& state, const std::optional<BS::thread_pool<>* const> thread_pool) -> void
auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool thread_pool) -> void
{
mass_spring_system mass_springs;
cpu_spring_system mass_springs;
#ifdef ASYNC_OCTREE
BS::this_thread::set_os_thread_name("physics");
@ -28,7 +28,6 @@ auto threaded_physics::physics_thread(physics_state& state, const std::optional<
std::future<void> octree_future;
octree tree_buffer;
size_t last_mass_count = 0;
infoln("Using asynchronous octree builder.");
#endif
const auto visitor = overloads{
@ -90,20 +89,20 @@ auto threaded_physics::physics_thread(physics_state& state, const std::optional<
// Start building the octree for the next physics update.
// Move the snapshot into the closure so it doesn't get captured by reference (don't use [&])
octree_future = octree_thread.submit_task([&tree_buffer, positions = std::move(positions)]()
octree_future = octree_thread.submit_task([&tree_buffer, &thread_pool, positions = std::move(positions)]()
{
octree::build_octree(tree_buffer, positions);
octree::build_octree_morton(tree_buffer, positions, thread_pool);
});
// Rebuild the tree synchronously if we changed the number of masses to not use
// an empty tree from the last frame in the frame where the graph was generated
if (last_mass_count != mass_springs.positions.size()) {
traceln("Rebuilding octree synchronously because graph size changed");
octree::build_octree(mass_springs.tree, mass_springs.positions);
octree::build_octree_morton(mass_springs.tree, mass_springs.positions, thread_pool);
last_mass_count = mass_springs.positions.size();
}
#else
octree::build_octree(mass_springs.tree, mass_springs.positions);
octree::build_octree_morton(mass_springs.tree, mass_springs.positions, thread_pool);
#endif
mass_springs.clear_forces();
@ -139,10 +138,11 @@ auto threaded_physics::physics_thread(physics_state& state, const std::optional<
#else
std::unique_lock<std::mutex> lock(state.data_mtx);
#endif
state.data_consumed_cnd.wait(lock, [&]
{
return state.data_consumed || !state.running.load();
});
state.data_consumed_cnd.wait(lock,
[&]
{
return state.data_consumed || !state.running.load();
});
if (!state.running.load()) {
// Running turned false while we were waiting for the condition
break;
@ -154,10 +154,10 @@ auto threaded_physics::physics_thread(physics_state& state, const std::optional<
loop_iterations = 0;
ups_accumulator = std::chrono::duration<double>(0);
}
if (mass_springs.tree.nodes.empty()) {
if (mass_springs.tree.empty()) {
state.mass_center = Vector3Zero();
} else {
state.mass_center = mass_springs.tree.nodes[0].mass_center;
state.mass_center = mass_springs.tree.root().mass_center;
}
state.masses.clear();
@ -176,12 +176,13 @@ auto threaded_physics::physics_thread(physics_state& state, const std::optional<
state.data_ready_cnd.notify_all();
#ifdef TRACY
FrameMarkEnd("PhysicsThreadProduceLock"); FrameMarkEnd("PhysicsThread");
FrameMarkEnd("PhysicsThreadProduceLock");
FrameMarkEnd("PhysicsThread");
#endif
}
}
auto threaded_physics::clear_cmd() -> void
auto cpu_layout_engine::clear_cmd() -> void
{
{
#ifdef TRACY
@ -193,7 +194,7 @@ auto threaded_physics::clear_cmd() -> void
}
}
auto threaded_physics::add_mass_cmd() -> void
auto cpu_layout_engine::add_mass_cmd() -> void
{
{
#ifdef TRACY
@ -205,7 +206,7 @@ auto threaded_physics::add_mass_cmd() -> void
}
}
auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void
auto cpu_layout_engine::add_spring_cmd(const size_t a, const size_t b) -> void
{
{
#ifdef TRACY
@ -217,8 +218,7 @@ auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void
}
}
auto threaded_physics::add_mass_springs_cmd(const size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void
auto cpu_layout_engine::add_mass_springs_cmd(const size_t num_masses, const std::vector<spring>& springs) -> void
{
{
#ifdef TRACY

View File

@ -1,69 +1,20 @@
#include "mass_spring_system.hpp"
#include "cpu_spring_system.hpp"
#include "config.hpp"
#include <cfloat>
#include <cstring>
auto mass_spring_system::calculate_spring_force(const size_t s) -> void
{
const spring _s = springs[s];
const Vector3 a_pos = positions[_s.a];
const Vector3 b_pos = positions[_s.b];
const Vector3 a_vel = velocities[_s.a];
const Vector3 b_vel = velocities[_s.b];
const Vector3 delta_pos = a_pos - b_pos;
const Vector3 delta_vel = a_vel - b_vel;
const float sq_len = Vector3DotProduct(delta_pos, delta_pos);
const float inv_len = 1.0f / sqrt(sq_len);
const float len = sq_len * inv_len;
const float hooke = SPRING_CONSTANT * (len - REST_LENGTH);
const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_vel, delta_pos) * inv_len;
const Vector3 a_force = Vector3Scale(delta_pos, -(hooke + dampening) * inv_len);
const Vector3 b_force = a_force * -1.0f;
forces[_s.a] += a_force;
forces[_s.b] += b_force;
}
auto mass_spring_system::integrate_velocity(const size_t m, const float dt) -> void
{
const Vector3 acc = forces[m] / MASS;
velocities[m] += acc * dt;
}
auto mass_spring_system::integrate_position(const size_t m, const float dt) -> void
{
previous_positions[m] = positions[m];
positions[m] += velocities[m] * dt;
}
auto mass_spring_system::verlet_update(const size_t m, const float dt) -> void
{
const Vector3 acc = (forces[m] / MASS) * dt * dt;
const Vector3 pos = positions[m];
Vector3 delta_pos = pos - previous_positions[m];
delta_pos *= 1.0 - VERLET_DAMPENING; // Minimal dampening
positions[m] += delta_pos + acc;
previous_positions[m] = pos;
}
auto mass_spring_system::clear() -> void
auto cpu_spring_system::clear() -> void
{
positions.clear();
previous_positions.clear();
velocities.clear();
forces.clear();
springs.clear();
tree.nodes.clear();
tree.clear();
}
auto mass_spring_system::add_mass() -> void
auto cpu_spring_system::add_mass() -> void
{
// Adding all positions to (0, 0, 0) breaks the octree
@ -80,21 +31,24 @@ auto mass_spring_system::add_mass() -> void
forces.emplace_back(Vector3Zero());
}
auto mass_spring_system::add_spring(size_t a, size_t b) -> void
auto cpu_spring_system::add_spring(size_t a, size_t b) -> void
{
// Update masses to be located along a random walk when adding the springs
const Vector3& mass_a = positions[a];
const Vector3& mass_b = positions[b];
Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))};
offset = Vector3Normalize(offset) * REST_LENGTH;
Vector3 offset{
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))
};
// By spawning the masses close together, we "explode" them naturally, so they cluster faster (also looks cool)
offset = Vector3Normalize(offset) * REST_LENGTH * 0.1;
// If the offset moves the mass closer to the current center of mass, flip it
if (!tree.nodes.empty()) {
const Vector3 mass_center_direction =
Vector3Subtract(positions[a], tree.nodes[0].mass_center);
if (!tree.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(positions[a], tree.root().mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
@ -112,28 +66,53 @@ auto mass_spring_system::add_spring(size_t a, size_t b) -> void
springs.emplace_back(a, b);
}
auto mass_spring_system::clear_forces() -> void
auto cpu_spring_system::clear_forces() -> void
{
#ifdef TRACY
#ifdef TRACY
ZoneScoped;
#endif
#endif
memset(forces.data(), 0, forces.size() * sizeof(Vector3));
}
auto mass_spring_system::calculate_spring_forces(
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
auto cpu_spring_system::calculate_spring_force(const size_t s) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const spring _s = springs[s];
const Vector3 a_pos = positions[_s.first];
const Vector3 b_pos = positions[_s.second];
const Vector3 a_vel = velocities[_s.first];
const Vector3 b_vel = velocities[_s.second];
const auto solve_spring_force = [&](const int i) { calculate_spring_force(i); };
const Vector3 delta_pos = a_pos - b_pos;
const Vector3 delta_vel = a_vel - b_vel;
const float sq_len = Vector3DotProduct(delta_pos, delta_pos);
const float inv_len = 1.0f / sqrt(sq_len);
const float len = sq_len * inv_len;
const float hooke = SPRING_K * (len - REST_LENGTH);
const float dampening = DAMPENING_K * Vector3DotProduct(delta_vel, delta_pos) * inv_len;
const Vector3 a_force = Vector3Scale(delta_pos, -(hooke + dampening) * inv_len);
const Vector3 b_force = a_force * -1.0f;
forces[_s.first] += a_force;
forces[_s.second] += b_force;
}
auto cpu_spring_system::calculate_spring_forces(const threadpool thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const auto solve_spring_force = [&](const int i)
{
calculate_spring_force(i);
};
if (thread_pool) {
(*thread_pool)
->submit_loop(0, springs.size(), solve_spring_force, SMALL_TASK_BLOCK_SIZE)
.wait();
(*thread_pool)->submit_loop(0, springs.size(), solve_spring_force, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < springs.size(); ++i) {
solve_spring_force(i);
@ -141,24 +120,21 @@ auto mass_spring_system::calculate_spring_forces(
}
}
auto mass_spring_system::calculate_repulsion_forces(
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
auto cpu_spring_system::calculate_repulsion_forces(const threadpool thread_pool) -> void
{
#ifdef TRACY
#ifdef TRACY
ZoneScoped;
#endif
#endif
const auto solve_octree = [&](const int i)
{
const Vector3 force = tree.calculate_force(0, positions[i]);
const Vector3 force = tree.calculate_force_morton(0, positions[i], i);
forces[i] += force;
};
// Calculate forces using Barnes-Hut
if (thread_pool) {
(*thread_pool)
->submit_loop(0, positions.size(), solve_octree, LARGE_TASK_BLOCK_SIZE)
.wait();
(*thread_pool)->submit_loop(0, positions.size(), solve_octree, LARGE_TASK_BLOCK_SIZE).wait();
} else {
for (size_t i = 0; i < positions.size(); ++i) {
solve_octree(i);
@ -166,14 +142,40 @@ auto mass_spring_system::calculate_repulsion_forces(
}
}
auto mass_spring_system::update(const float dt,
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
auto cpu_spring_system::integrate_velocity(const size_t m, const float dt) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const Vector3 acc = forces[m] / MASS;
velocities[m] += acc * dt;
}
const auto update = [&](const int i) { verlet_update(i, dt); };
auto cpu_spring_system::integrate_position(const size_t m, const float dt) -> void
{
previous_positions[m] = positions[m];
positions[m] += velocities[m] * dt;
}
auto cpu_spring_system::verlet_update(const size_t m, const float dt) -> void
{
const Vector3 acc = (forces[m] / MASS) * dt * dt;
const Vector3 pos = positions[m];
Vector3 delta_pos = pos - previous_positions[m];
delta_pos *= 1.0 - VERLET_DAMPENING; // Minimal dampening
positions[m] += delta_pos + acc;
previous_positions[m] = pos;
}
auto cpu_spring_system::update(const float dt, const threadpool thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
const auto update = [&](const int i)
{
verlet_update(i, dt);
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait();
@ -184,8 +186,7 @@ auto mass_spring_system::update(const float dt,
}
}
auto mass_spring_system::center_masses(const std::optional<BS::thread_pool<>* const> thread_pool)
-> void
auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void
{
Vector3 mean = Vector3Zero();
for (const Vector3& pos : positions) {
@ -193,7 +194,10 @@ auto mass_spring_system::center_masses(const std::optional<BS::thread_pool<>* co
}
mean /= static_cast<float>(positions.size());
const auto center_mass = [&](const int i) { positions[i] -= mean; };
const auto center_mass = [&](const int i)
{
positions[i] -= mean;
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait();
@ -202,4 +206,4 @@ auto mass_spring_system::center_masses(const std::optional<BS::thread_pool<>* co
center_mass(i);
}
}
}
}

View File

@ -15,7 +15,7 @@ auto graph_distances::empty() const -> bool
}
auto graph_distances::calculate_distances(const size_t node_count,
const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<spring>& edges,
const std::vector<size_t>& targets) -> void
{
// Build a list of adjacent nodes to speed up BFS

View File

@ -18,6 +18,7 @@ auto input_handler::init_handlers() -> void
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::add_block);
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_add_block);
register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::place_goal);
register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::select_state);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block);
@ -42,6 +43,7 @@ auto input_handler::init_handlers() -> void
register_key_pressed_handler(KEY_C, &input_handler::clear_graph);
register_key_pressed_handler(KEY_I, &input_handler::toggle_mark_solutions);
register_key_pressed_handler(KEY_O, &input_handler::toggle_connect_solutions);
register_key_pressed_handler(KEY_Z, &input_handler::toggle_color_by_distance);
register_key_pressed_handler(KEY_TAB, &input_handler::toggle_editing);
register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement);
@ -142,12 +144,17 @@ auto input_handler::camera_zoom() const -> void
auto input_handler::camera_fov() const -> void
{
if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_LEFT_SHIFT)) {
if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL)) {
return;
}
const float wheel = GetMouseWheelMove();
camera.fov -= wheel * FOV_SPEED;
if (IsKeyDown(KEY_LEFT_SHIFT)) {
camera.fov -= wheel * FOV_SPEED * FOV_MULTIPLIER;
} else {
camera.fov -= wheel * FOV_SPEED;
}
}
auto input_handler::select_block() -> void
@ -201,7 +208,7 @@ auto input_handler::add_block() -> void
has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) {
const std::optional<puzzle>& next = current.try_add_block(
puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) {
sel_x = block_add_x;
@ -217,7 +224,7 @@ auto input_handler::add_block() -> void
auto input_handler::remove_block() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle::block>& b = current.try_get_block(hov_x, hov_y);
const std::optional<block>& b = current.try_get_block(hov_x, hov_y);
if (!editing || has_block_add_xy || !b) {
return;
}
@ -239,7 +246,7 @@ auto input_handler::remove_block() -> void
auto input_handler::place_goal() const -> void
{
const puzzle& current = state.get_current_state();
if (!editing || !current.covers(hov_x, hov_y)) {
if (!editing || !mouse_in_board_pane() || !current.covers(hov_x, hov_y)) {
return;
}
@ -251,6 +258,16 @@ auto input_handler::place_goal() const -> void
state.edit_starting_state(*next);
}
auto input_handler::select_state() const -> void
{
if (!mouse_in_graph_pane() || collision_mass == static_cast<size_t>(-1)) {
return;
}
const puzzle& selected = state.get_state(collision_mass);
state.update_current_state(selected);
}
auto input_handler::toggle_camera_lock() -> void
{
if (!camera_lock) {
@ -407,6 +424,11 @@ auto input_handler::toggle_connect_solutions() -> void
connect_solutions = !connect_solutions;
}
auto input_handler::toggle_color_by_distance() -> void
{
color_by_distance = !color_by_distance;
}
auto input_handler::toggle_mark_path() -> void
{
mark_path = !mark_path;

View File

@ -62,7 +62,10 @@ auto append_preset_file(const std::string& preset_file, const std::string& prese
return true;
}
auto append_preset_file_quiet(const std::string& preset_file, const std::string& preset_name, const puzzle& p, const bool validate) -> bool
auto append_preset_file_quiet(const std::string& preset_file,
const std::string& preset_name,
const puzzle& p,
const bool validate) -> bool
{
if (validate && p.try_get_invalid_reason()) {
return false;

View File

@ -1,26 +1,50 @@
#include <chrono>
#include <mutex>
#include <raylib.h>
#include "config.hpp"
#include "input_handler.hpp"
#include "threaded_physics.hpp"
#include "cpu_layout_engine.hpp"
#include "renderer.hpp"
#include "state_manager.hpp"
#include "user_interface.hpp"
#include <chrono>
#include <mutex>
#include <GL/glew.h>
#include <raylib.h>
#include <filesystem>
#ifndef WIN32
#if not defined(_WIN32)
#include <boost/program_options.hpp>
namespace po = boost::program_options;
#endif
// Threadpool setup
#ifdef THREADPOOL
auto set_pool_thread_name(size_t idx) -> void
{
BS::this_thread::set_os_thread_name(std::format("worker-{}", idx));
}
BS::thread_pool<> threads(std::thread::hardware_concurrency() - 2, set_pool_thread_name);
constexpr threadpool thread_pool = &threads;
#else
constexpr threadpool thread_pool = std::nullopt;
#endif
// Argparse defaults
std::string preset_file = "default.puzzle";
std::string output_file = "clusters.puzzle";
int max_blocks = 5;
int min_moves = 10;
// Puzzle space setup
int board_width;
int board_height;
int goal_x;
int goal_y;
bool restricted;
blockset2 permitted_blocks;
block target_block;
std::tuple<u8, u8, u8, u8> target_block_pos_range;
// TODO: Implement state discovery/enumeration
// - Find all possible initial board states (single one for each possible statespace).
// Currently wer're just finding all states given the initial state
// - Would allow to generate random puzzles with a certain move count
// TODO: Export cluster to graphviz
// TODO: Fix naming:
// - Target: The block that has to leave the board to win
@ -33,29 +57,6 @@ namespace po = boost::program_options;
// TODO: Add state space generation time to debug overlay
// TODO: Move selection accordingly when undoing moves (need to diff two states and get the moved blocks)
// TODO: Click states in the graph to display them in the board
#ifdef THREADPOOL
auto set_pool_thread_name(size_t idx) -> void
{
BS::this_thread::set_os_thread_name(std::format("worker-{}", idx));
}
BS::thread_pool<> threads(std::thread::hardware_concurrency() - 2, set_pool_thread_name);
constexpr std::optional<BS::thread_pool<>* const> thread_pool = &threads;
#else
constexpr std::optional<BS::thread_pool<>* const> thread_pool = std::nullopt;
#endif
std::string preset_file;
std::string output_file;
int max_blocks = 5;
int board_width = 6;
int board_height = 6;
int goal_x = 4;
int goal_y = 2;
bool restricted = true;
auto ui_mode() -> int
{
// RayLib window setup
@ -66,8 +67,15 @@ auto ui_mode() -> int
SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN);
InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings");
// GLEW setup
glewExperimental = GL_TRUE;
const GLenum glew_err = glewInit();
if (glew_err != GLEW_OK) {
TraceLog(LOG_FATAL, "Failed to initialize GLEW: %s", glewGetErrorString(glew_err));
}
// Game setup
threaded_physics physics(thread_pool);
cpu_layout_engine physics(thread_pool);
state_manager state(physics, preset_file);
orbit_camera camera;
input_handler input(state, camera);
@ -161,51 +169,87 @@ auto ui_mode() -> int
return 0;
}
auto rush_hour_puzzle_space() -> int
auto rush_hour_puzzle_space() -> void
{
const boost::unordered_flat_set<puzzle::block, block_hasher2, block_equal2> permitted_blocks = {
puzzle::block(0, 0, 2, 1, false, false),
puzzle::block(0, 0, 3, 1, false, false),
puzzle::block(0, 0, 1, 2, false, false),
puzzle::block(0, 0, 1, 3, false, false)
board_width = 6;
board_height = 6;
goal_x = 4;
goal_y = 2;
restricted = true;
permitted_blocks = {
block(0, 0, 2, 1, false, false),
block(0, 0, 3, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 1, 3, false, false)
};
const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false);
const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> target_block_pos_range = {0, goal_y, board_width - 1, goal_y};
target_block = block(0, 0, 2, 1, true, false);
target_block_pos_range = {0, goal_y, board_width - target_block.get_width(), goal_y};
}
infoln("Exploring Rush-Hour puzzle space:");
auto klotski_puzzle_space() -> void
{
board_width = 4;
board_height = 5;
goal_x = 1;
goal_y = 3;
restricted = false;
permitted_blocks = {
block(0, 0, 1, 1, false, false),
block(0, 0, 1, 2, false, false),
block(0, 0, 2, 1, false, false),
};
target_block = block(0, 0, 2, 2, true, false);
target_block_pos_range = {
0,
0,
board_width - target_block.get_width(),
board_height - target_block.get_height(),
};
}
auto puzzle_space() -> int
{
// We don't only pick max_blocks out of n (with duplicates), but also 1 out of n, 2, 3, ... max_blocks-1 out of n
int upper_set_count = 0;
for (int i = 1; i <= max_blocks; ++i) {
upper_set_count += binom(permitted_blocks.size() + i - 1, i);
}
infoln("Exploring puzzle space:");
infoln("- Size: {}x{}", board_width, board_height);
infoln("- Goal: {},{}", goal_x, goal_y);
infoln("- Restricted: {}", restricted);
infoln("- Max Blocks: {}", max_blocks);
infoln("- Min Moves: {}", min_moves);
infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height());
infoln("- Max Sets: {}", upper_set_count);
infoln("- Permitted block sizes:");
std::cout << " ";
for (const puzzle::block b : permitted_blocks) {
for (const block b : permitted_blocks) {
std::cout << std::format(" {}x{},", b.get_width(), b.get_height());
}
std::cout << std::endl;
const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true);
const boost::unordered_flat_set<puzzle, puzzle_hasher> result = p.explore_puzzle_space(
STARTTIME;
const puzzleset result = p.explore_puzzle_space(
permitted_blocks,
target_block,
target_block_pos_range,
max_blocks,
min_moves,
thread_pool);
ENDTIME(std::format("Found {} different clusters", result.size()), std::chrono::seconds, "s");
const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
infoln("Found {} different clusters. Took {}s.",
result.size(),
std::chrono::duration_cast<std::chrono::seconds>(end - start).count());
// TODO: The exported clusters are the numerically smallest state of the cluster.
// Not the state with the longest path.
infoln("Sorting clusters...");
std::vector<puzzle> result_sorted{result.begin(), result.end()};
std::ranges::sort(result_sorted, std::ranges::greater{});
// for (const puzzle& _p : result_sorted) {
// traceln("{}", _p.string_repr());
// }
infoln("Saving clusters...");
size_t i = 0;
size_t success = 0;
std::filesystem::remove(output_file);
@ -226,26 +270,32 @@ auto rush_hour_puzzle_space() -> int
enum class runmode
{
USER_INTERFACE, RUSH_HOUR_PUZZLE_SPACE, EXIT,
USER_INTERFACE,
RUSH_HOUR_PUZZLE_SPACE,
KLOTSKI_PUZZLE_SPACE,
EXIT,
};
auto argparse(const int argc, char* argv[]) -> runmode
{
#ifndef WIN32
#if not defined(_WIN32)
po::options_description desc("Allowed options");
desc.add_options() //
("help", "produce help message") //
("presets", po::value<std::string>()->default_value("default.puzzle"), "load presets from file") //
("output", po::value<std::string>()->default_value("clusters.puzzle"), "output file for generated clusters") //
("space", po::value<std::string>()->value_name("rh|klotski"), "generate puzzle space with ruleset") //
("w", po::value<int>()->default_value(6), "board width") //
("h", po::value<int>()->default_value(6), "board height") //
("gx", po::value<int>()->default_value(4), "board goal horizontal position") //
("gy", po::value<int>()->default_value(2), "board goal vertical position") //
("free", "allow free block movement") //
("blocks", po::value<int>()->default_value(5), "block limit for puzzle space generation") //
desc.add_options() //
("help", "produce help message") //
("presets", po::value<std::string>()->default_value(preset_file), "load presets from file") //
("output", po::value<std::string>()->default_value(output_file), "output file for generated clusters") //
("space", po::value<std::string>()->value_name("rh|klotski"), "generate puzzle space with ruleset") //
// ("w", po::value<int>()->default_value(board_width)->value_name("[3, 8]"), "board width") //
// ("h", po::value<int>()->default_value(board_height)->value_name("[3, 8"), "board height") //
// ("gx", po::value<int>()->default_value(goal_x)->value_name("[0, w-1]"), "board goal horizontal position") //
// ("gy", po::value<int>()->default_value(goal_y)->value_name("[0, h-1]"), "board goal vertical position") //
// ("free", "allow free block movement") //
("blocks",
po::value<int>()->default_value(max_blocks)->value_name("[1, 15]"),
"block limit for puzzle space generation") //
("moves",
po::value<int>()->default_value(min_moves),
"only save puzzles with at least this many required moves") //
;
po::positional_options_description positional;
@ -259,56 +309,53 @@ auto argparse(const int argc, char* argv[]) -> runmode
std::cout << desc << std::endl;
return runmode::EXIT;
}
if (vm.contains("output")) {
output_file = vm["output"].as<std::string>();
}
if (vm.contains("w")) {
board_width = vm["w"].as<int>();
board_width = std::max(static_cast<int>(puzzle::MIN_WIDTH),
std::min(board_width, static_cast<int>(puzzle::MAX_WIDTH)));
}
if (vm.contains("h")) {
board_height = vm["h"].as<int>();
board_height = std::max(static_cast<int>(puzzle::MIN_HEIGHT),
std::min(board_height, static_cast<int>(puzzle::MAX_HEIGHT)));
}
if (vm.contains("gx")) {
goal_x = vm["gx"].as<int>();
goal_x = std::max(0, std::min(goal_x, static_cast<int>(puzzle::MAX_WIDTH) - 1));
}
if (vm.contains("gy")) {
goal_y = vm["gy"].as<int>();
goal_y = std::max(0, std::min(goal_y, static_cast<int>(puzzle::MAX_HEIGHT) - 1));
}
if (vm.contains("free")) {
restricted = false;
}
// if (vm.contains("w")) {
// board_width = vm["w"].as<int>();
// board_width = std::max(static_cast<int>(puzzle::MIN_WIDTH),
// std::min(board_width, static_cast<int>(puzzle::MAX_WIDTH)));
// }
// if (vm.contains("h")) {
// board_height = vm["h"].as<int>();
// board_height = std::max(static_cast<int>(puzzle::MIN_HEIGHT),
// std::min(board_height, static_cast<int>(puzzle::MAX_HEIGHT)));
// }
// if (vm.contains("gx")) {
// goal_x = vm["gx"].as<int>();
// goal_x = std::max(0, std::min(goal_x, static_cast<int>(puzzle::MAX_WIDTH) - 1));
// }
// if (vm.contains("gy")) {
// goal_y = vm["gy"].as<int>();
// goal_y = std::max(0, std::min(goal_y, static_cast<int>(puzzle::MAX_HEIGHT) - 1));
// }
// if (vm.contains("free")) {
// restricted = false;
// }
if (vm.contains("blocks")) {
max_blocks = vm["blocks"].as<int>();
max_blocks = std::max(1, std::min(max_blocks, static_cast<int>(puzzle::MAX_BLOCKS)));
}
if (vm.contains("moves")) {
min_moves = vm["moves"].as<int>();
min_moves = std::max(0, min_moves);
}
if (vm.contains("space")) {
const std::string ruleset = vm["space"].as<std::string>();
if (ruleset == "rh") {
return runmode::RUSH_HOUR_PUZZLE_SPACE;
}
if (ruleset == "klotski") {
throw std::runtime_error("Not implemented");
return runmode::KLOTSKI_PUZZLE_SPACE;
}
}
if (vm.contains("presets")) {
preset_file = vm["presets"].as<std::string>();
}
#endif
#endif
return runmode::USER_INTERFACE;
}
@ -328,7 +375,13 @@ auto main(const int argc, char* argv[]) -> int
#endif
infoln("Using background thread for physics.");
infoln("Using octree-barnes-hut for graph layout.");
infoln("Using linear octree + Barnes-Hut for graph layout.");
#ifdef ASYNC_OCTREE
infoln("Using asynchronous octree build.");
#else
infoln("Using synchronous octree build.");
#endif
#ifdef THREADPOOL
infoln("Additional thread-pool enabled ({} threads).", threads.get_thread_count());
@ -340,10 +393,14 @@ auto main(const int argc, char* argv[]) -> int
case runmode::USER_INTERFACE:
return ui_mode();
case runmode::RUSH_HOUR_PUZZLE_SPACE:
return rush_hour_puzzle_space();
rush_hour_puzzle_space();
return puzzle_space();
case runmode::KLOTSKI_PUZZLE_SPACE:
klotski_puzzle_space();
return puzzle_space();
case runmode::EXIT:
return 0;
};
return 1;
}
}

View File

@ -4,224 +4,332 @@
#include <cfloat>
#include <raymath.h>
auto octree::node::child_count() const -> int
auto octree::clear() -> void
{
int child_count = 0;
for (const int child : children) {
if (child != -1) {
++child_count;
}
}
return child_count;
nodes.clear();
}
auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
auto octree::reserve(const size_t count) -> void
{
const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
// The octant is encoded as a 3-bit integer "zyx". The node area is split
// along all 3 axes, if a position is right of an axis, this bit is set to 1.
// If a position is right of the x-axis and y-axis and left of the z-axis, the
// encoded octant is "011".
int octant = 0;
if (pos.x >= cx) {
octant |= 1;
}
if (pos.y >= cy) {
octant |= 2;
}
if (pos.z >= cz) {
octant |= 4;
}
return octant;
nodes.reserve(count);
}
auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3>
auto octree::empty() const -> bool
{
const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
Vector3 min = Vector3Zero();
Vector3 max = Vector3Zero();
// If (octant & 1), the octant is to the right of the node region's x-axis
// (see GetOctant). This means the left bound is the x-axis and the right
// bound the node's region max.
min.x = octant & 1 ? cx : n.box_min.x;
max.x = octant & 1 ? n.box_max.x : cx;
min.y = octant & 2 ? cy : n.box_min.y;
max.y = octant & 2 ? n.box_max.y : cy;
min.z = octant & 4 ? cz : n.box_min.z;
max.z = octant & 4 ? n.box_max.z : cz;
return std::make_pair(min, max);
return nodes.empty();
}
auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int
auto octree::root() const -> const node&
{
node n;
n.box_min = box_min;
n.box_max = box_max;
nodes.emplace_back(n);
return static_cast<int>(nodes.size() - 1);
return nodes[0];
}
auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
const int depth) -> void
{
// infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y,
// pos.z, node_idx, depth);
if (depth > MAX_DEPTH) {
throw std::runtime_error(std::format("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})",
node_idx, nodes[node_idx].box_min.x, nodes[node_idx].box_min.y,
nodes[node_idx].box_min.z, nodes[node_idx].box_max.x,
nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y,
pos.z));
}
// NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during
// this function
// We can place the particle in the empty leaf
if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) {
nodes[node_idx].mass_id = mass_id;
nodes[node_idx].mass_center = pos;
nodes[node_idx].mass_total = mass;
return;
}
// The leaf is occupied, we need to subdivide
if (nodes[node_idx].leaf) {
const int existing_id = nodes[node_idx].mass_id;
const Vector3 existing_pos = nodes[node_idx].mass_center;
const float existing_mass = nodes[node_idx].mass_total;
// If positions are identical we jitter the particles
const Vector3 diff = Vector3Subtract(pos, existing_pos);
if (diff == Vector3Zero()) {
// warnln("Trying to insert an identical partical into octree (jittering position)");
Vector3 jittered = pos;
jittered.x += 0.001;
jittered.y += 0.001;
insert(node_idx, mass_id, jittered, mass, depth);
return;
// Could also merge them, but that leads to the octree having less leafs than we have
// masses nodes[node_idx].mass_total += mass; return;
}
// Convert the leaf to an internal node
nodes[node_idx].mass_id = -1;
nodes[node_idx].leaf = false;
nodes[node_idx].mass_total = 0.0;
nodes[node_idx].mass_center = Vector3Zero();
// Re-insert the existing mass into a new empty leaf (see above)
const int oct = get_octant(node_idx, existing_pos);
if (nodes[node_idx].children[oct] == -1) {
const auto& [min, max] = get_child_bounds(node_idx, oct);
const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
}
insert(nodes[node_idx].children[oct], existing_id, existing_pos, existing_mass, depth + 1);
}
// Insert the new mass
const int oct = get_octant(node_idx, pos);
if (nodes[node_idx].children[oct] == -1) {
const auto& [min, max] = get_child_bounds(node_idx, oct);
const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
}
insert(nodes[node_idx].children[oct], mass_id, pos, mass, depth + 1);
// Update the center of mass
const float new_mass = nodes[node_idx].mass_total + mass;
nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass;
nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass;
nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass;
nodes[node_idx].mass_total = new_mass;
}
auto octree::build_octree(octree& t, const std::vector<Vector3>& positions) -> void
// Replaced the 50 line recursive octree insertion with this morton bitch to gain 5 UPS, FML
auto octree::build_octree_morton(octree& t,
const std::vector<Vector3>& positions,
const std::optional<BS::thread_pool<>*>& thread_pool) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
t.nodes.clear();
t.nodes.reserve(positions.size() * 2);
t.clear();
if (positions.empty()) {
return;
}
// Compute bounding box around all masses
Vector3 min{FLT_MAX, FLT_MAX, FLT_MAX};
Vector3 max{-FLT_MAX, -FLT_MAX, -FLT_MAX};
Vector3 root_min{FLT_MAX, FLT_MAX, FLT_MAX};
Vector3 root_max{-FLT_MAX, -FLT_MAX, -FLT_MAX};
for (const auto& [x, y, z] : positions) {
min.x = std::min(min.x, x);
max.x = std::max(max.x, x);
min.y = std::min(min.y, y);
max.y = std::max(max.y, y);
min.z = std::min(min.z, z);
max.z = std::max(max.z, z);
root_min.x = std::min(root_min.x, x);
root_max.x = std::max(root_max.x, x);
root_min.y = std::min(root_min.y, y);
root_max.y = std::max(root_max.y, y);
root_min.z = std::min(root_min.z, z);
root_max.z = std::max(root_max.z, z);
}
// Pad the bounding box
constexpr float pad = 1.0;
min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad));
max = Vector3Add(max, Vector3Scale(Vector3One(), pad));
constexpr float pad = 1.0f;
root_min = Vector3Subtract(root_min, Vector3Scale(Vector3One(), pad));
root_max = Vector3Add(root_max, Vector3Scale(Vector3One(), pad));
// Make it cubic (so subdivisions are balanced)
const float max_extent = std::max({max.x - min.x, max.y - min.y, max.z - min.z});
max = Vector3Add(min, Vector3Scale(Vector3One(), max_extent));
const float max_extent = std::max({root_max.x - root_min.x, root_max.y - root_min.y, root_max.z - root_min.z});
root_max = Vector3Add(root_min, Vector3Scale(Vector3One(), max_extent));
// Root node spans the entire area
const int root = t.create_empty_leaf(min, max);
const float root_extent = root_max.x - root_min.x; // cubic
for (size_t i = 0; i < positions.size(); ++i) {
t.insert(root, static_cast<int>(i), positions[i], MASS, 0);
// Container for building the particle list before sorting by morton code
struct sort_node
{
u64 code;
u32 id;
Vector3 pos;
};
// Calculate morton code for each node
std::vector<sort_node> sort_container;
sort_container.resize(positions.size());
const auto calculate_morton = [&](const u32 i)
{
sort_container[i] = {pos_to_morton(positions[i], root_min, root_max), i, positions[i]};
};
if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), calculate_morton, SMALL_TASK_BLOCK_SIZE).wait();
} else {
for (u32 i = 0; i < positions.size(); ++i) {
calculate_morton(i);
}
}
// Sort the list by morton codes. Because positions close to each other have similar morten codes,
// this provides us with "spatial locality" in the datastructure.
auto sort_by_code = [&]()
{
std::ranges::sort(sort_container,
[](const sort_node& a, const sort_node& b)
{
if (a.code != b.code) {
return a.code < b.code;
}
return a.id < b.id;
});
};
sort_by_code();
// Resolve duplicates by jittering the later one deterministically and re-encoding.
for (int seed = 0; seed < 8; ++seed) {
bool had_dupes = false;
for (size_t i = 1; i < sort_container.size(); ++i) {
// Because elements are spatially ordered after sorting, we can scan for duplicates in O(n)
if (sort_container[i].code == sort_container[i - 1].code) {
had_dupes = true;
sort_container[i].pos = jitter_pos(sort_container[i].pos,
sort_container[i].id + seed * 0x9e3779b9u,
root_min,
root_max,
root_extent);
sort_container[i].code = pos_to_morton(sort_container[i].pos, root_min, root_max);
}
}
if (!had_dupes) {
break;
}
sort_by_code();
}
// Sanity check
for (size_t i = 1; i < sort_container.size(); ++i) {
if (sort_container[i].code == sort_container[i - 1].code) {
throw std::runtime_error("Duplicates remain after jittering");
}
}
std::vector<std::vector<node>> tree_levels;
tree_levels.assign(MAX_DEPTH + 1, {});
// Leaves at MAX_DEPTH: 1 particle per leaf in morton order (close particles close together)
auto& leafs = tree_levels[MAX_DEPTH];
leafs.reserve(sort_container.size());
const float leaf_size = root_extent / static_cast<float>(1u << MAX_DEPTH);
for (const auto& [code, id, pos] : sort_container) {
node leaf;
leaf.leaf = true;
leaf.mass_id = static_cast<int>(id);
leaf.depth = MAX_DEPTH;
leaf.size = leaf_size;
leaf.mass_total = MASS;
leaf.mass_center = pos; // force uses mass_center instead of jittered position
leaf.children.fill(-1);
leafs.push_back(leaf);
}
// We now have to group the particles (currently we have only sorted the leaves),
// but upwards subdivisions have to be created.
// For grouping, store a nodes local index in its level.
struct leaf
{
u64 leaf_code;
int depth;
int level_index;
};
std::vector<leaf> leaves;
leaves.reserve(tree_levels[MAX_DEPTH].size());
for (int i = 0; i < static_cast<int>(tree_levels[MAX_DEPTH].size()); ++i) {
leaves.emplace_back(sort_container[static_cast<size_t>(i)].code, MAX_DEPTH, i);
}
// Build internal levels from MAX_DEPTH-1 to 0
for (int current_depth = MAX_DEPTH - 1; current_depth >= 0; --current_depth) {
auto& current_level = tree_levels[current_depth];
current_level.clear();
std::vector<leaf> next_refs;
next_refs.reserve(leaves.size());
const float parent_size = root_extent / static_cast<float>(1u << current_depth);
size_t i = 0;
while (i < leaves.size()) {
const u64 key = path_to_ancestor(leaves[i].leaf_code, MAX_DEPTH, current_depth);
size_t j = i + 1;
while (j < leaves.size() && path_to_ancestor(leaves[j].leaf_code, MAX_DEPTH, current_depth) == key) {
++j;
}
const size_t group_size = j - i;
// Unary compression: just carry the child ref upward unchanged.
if (group_size == 1) {
next_refs.push_back(leaves[i]);
i = j;
continue;
}
node parent;
parent.leaf = false;
parent.mass_id = -1;
parent.depth = current_depth;
parent.size = parent_size;
parent.children.fill(-1);
float mass_total = 0.0f;
Vector3 mass_center_acc = Vector3Zero();
for (size_t k = i; k < j; ++k) {
const int child_depth = leaves[k].depth;
const int child_local = leaves[k].level_index;
// Read the child from its actual stored level.
const node& child = tree_levels[child_depth][child_local];
// Which octant of this parent does it belong to?
// Octant comes from the NEXT level after current_depth,
// but the child might skip levels due to compression.
// We must use the child's first level under the parent (current_depth+1).
const int oct = octant_at_level(leaves[k].leaf_code, current_depth + 1, MAX_DEPTH);
// Store global child reference: we only have an int slot, so we need a single index space.
parent.children[oct] = (child_depth << 24) | (child_local & 0x00FFFFFF);
mass_total += child.mass_total;
mass_center_acc = Vector3Add(mass_center_acc, Vector3Scale(child.mass_center, child.mass_total));
}
parent.mass_total = mass_total;
parent.mass_center = (mass_total > 0.0f) ? Vector3Scale(mass_center_acc, 1.0f / mass_total) : Vector3Zero();
const int parent_local = static_cast<int>(current_level.size());
current_level.push_back(parent);
next_refs.push_back({leaves[i].leaf_code, current_depth, parent_local});
i = j;
}
leaves.swap(next_refs);
}
// Flatten levels and fix child indices from local->global.
// All depth 0 nodes come first, then depth 1, last depth MAX_DEPTH.
std::vector<int> offsets(tree_levels.size(), 0);
int total = 0;
for (int d = 0; d <= MAX_DEPTH; ++d) {
offsets[d] = total;
total += static_cast<int>(tree_levels[d].size());
}
t.nodes.clear();
t.nodes.reserve(total);
for (int d = 0; d <= MAX_DEPTH; ++d) {
t.nodes.insert(t.nodes.end(), tree_levels[d].begin(), tree_levels[d].end());
}
// Fix child indices: convert local index in levels[d+1] to global index in t.nodes
for (int d = 0; d <= MAX_DEPTH; ++d) {
const int base = offsets[d];
for (int i2 = 0; i2 < static_cast<int>(tree_levels[d].size()); ++i2) {
node& n = t.nodes[base + i2];
if (!n.leaf) {
for (int c = 0; c < 8; ++c) {
int packed = n.children[c];
if (packed >= 0) {
const int child_depth = (packed >> 24) & 0xFF;
const int child_local = packed & 0x00FFFFFF;
n.children[c] = offsets[child_depth] + child_local;
}
}
}
}
}
// const size_t _leaves = tree_levels[MAX_DEPTH].size();
// const size_t _total = t.nodes.size();
// const size_t _internal = _total - _leaves;
// traceln("Morton octree nodes: {}, leaves: {}, ratio: {:.3} children per internal node.",
// _total,
// _leaves,
// static_cast<float>(_total - 1) / _internal);
}
auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Vector3
auto octree::calculate_force_morton(const int node_idx, const Vector3& pos, const int self_id) const -> Vector3
{
if (node_idx < 0) {
return Vector3Zero();
}
const node& n = nodes[node_idx];
if (std::abs(n.mass_total) <= 0.001f) {
return Vector3Zero();
}
// Force accumulator
float fx = 0.0f;
float fy = 0.0f;
float fz = 0.0f;
const Vector3 diff = Vector3Subtract(pos, n.mass_center);
float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
std::vector<int> stack;
stack.reserve(512);
stack.push_back(node_idx);
// Softening
dist_sq += SOFTENING;
constexpr float theta2 = THETA * THETA;
// Barnes-Hut
const float size = n.box_max.x - n.box_min.x;
if (n.leaf || size * size / dist_sq < THETA * THETA) {
const float dist = std::sqrt(dist_sq);
const float force_mag = BH_FORCE * n.mass_total / dist_sq;
while (!stack.empty()) {
const int idx = stack.back();
stack.pop_back();
return Vector3Scale(diff, force_mag / dist);
}
const node& n = nodes[idx];
// Collect child forces
Vector3 force = Vector3Zero();
for (const int child : n.children) {
if (child >= 0) {
const Vector3 child_force = calculate_force(child, pos);
// No self-force for single-particle leafs
if (n.leaf && n.mass_id == self_id) {
continue;
}
force = Vector3Add(force, child_force);
const float dx = pos.x - n.mass_center.x;
const float dy = pos.y - n.mass_center.y;
const float dz = pos.z - n.mass_center.z;
const float dist_sq = dx * dx + dy * dy + dz * dz + SOFTENING;
// BarnesHut criterion
if (n.leaf || ((n.size * n.size) / dist_sq) < theta2) {
const float inv_dist = 1.0f / std::sqrt(dist_sq);
const float force_mag = (BH_FORCE * n.mass_total) / dist_sq; // ~ 1/r^2
const float s = force_mag * inv_dist; // scale by 1/r to get vector
fx += dx * s;
fy += dy * s;
fz += dz * s;
continue;
}
for (int c = 0; c < 8; ++c) {
const int child = n.children[c];
if (child >= 0) {
stack.push_back(child);
}
}
}
return force;
return Vector3{fx, fy, fz};
}

View File

@ -31,24 +31,27 @@ auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void
const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));
const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward));
const Vector3 offset =
Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
const Vector3 offset = Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
target = Vector3Add(target, offset);
}
auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center,
const bool lock, const bool mass_center_lock) -> void
auto orbit_camera::update(const Vector3& current_target,
const Vector3& mass_center,
const bool lock,
const bool mass_center_lock) -> void
{
if (lock) {
if (mass_center_lock) {
target = Vector3MoveTowards(target, mass_center,
CAMERA_SMOOTH_SPEED * GetFrameTime() *
Vector3Length(Vector3Subtract(target, mass_center)));
target = Vector3MoveTowards(target,
mass_center,
CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length(
Vector3Subtract(target, mass_center)));
} else {
target = Vector3MoveTowards(target, current_target,
CAMERA_SMOOTH_SPEED * GetFrameTime() *
Vector3Length(Vector3Subtract(target, current_target)));
target = Vector3MoveTowards(target,
current_target,
CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length(
Vector3Subtract(target, current_target)));
}
}
@ -63,7 +66,11 @@ auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_cen
const float y = sin(angle_y) * actual_distance;
const float z = cos(angle_y) * cos(angle_x) * actual_distance;
fov = Clamp(fov, MIN_FOV, MAX_FOV);
if (projection == CAMERA_ORTHOGRAPHIC) {
fov = Clamp(fov, MIN_FOV, MAX_ORTHO_FOV);
} else {
fov = Clamp(fov, MIN_FOV, MAX_PERSP_FOV);
}
camera.position = Vector3Add(target, Vector3(x, y, z));
camera.target = target;

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
#include <raylib.h>
#include <raymath.h>
#include <rlgl.h>
#include <GL/glew.h>
auto renderer::update_texture_sizes() -> void
{
@ -34,10 +35,24 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
return;
}
// Prepare connection batching
// Prepare edge buffer
{
#ifdef TRACY
ZoneNamedN(prepare_connections, "PrepareConnectionsBatching", true);
ZoneNamedN(prepare_edge_buffers, "PrepareEdgeBuffers", true);
#endif
edge_vertices.clear();
for (const auto& [from, to] : state.get_links()) {
edge_vertices.push_back(masses[from]);
edge_vertices.push_back(masses[to]);
}
rlUpdateVertexBuffer(edge_vbo_id, edge_vertices.data(), edge_vertices.size() * sizeof(Vector3), 0);
}
// Prepare connection drawing
{
#ifdef TRACY
ZoneNamedN(prepare_connections, "PrepareConnectionsDrawing", true);
#endif
connections.clear();
@ -47,7 +62,6 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
const Vector3& current_mass = masses[state.get_current_index()];
const Vector3& winning_mass = masses[_state];
connections.emplace_back(current_mass, winning_mass);
DrawLine3D(current_mass, winning_mass, Fade(TARGET_BLOCK_COLOR, 0.5));
}
}
}
@ -63,6 +77,67 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
transforms.clear();
colors.clear();
// Collisions
// TODO: This would benefit greatly from a spatial data structure.
// Would it be worth to copy the octree from the physics thread?
input.collision_mass = -1;
if (input.mouse_in_graph_pane() && IsKeyDown(KEY_Q)) {
#ifdef TRACY
ZoneNamedN(mass_collisions, "MassCollisions", true);
#endif
const Ray ray = GetScreenToWorldRayEx(
GetMousePosition() - Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT),
camera.camera,
graph_target.texture.width,
graph_target.texture.height);
// Ray collision hit info
size_t mass = 0;
for (const auto& [x, y, z] : masses) {
const RayCollision collision = GetRayCollisionBox(ray,
BoundingBox{
{
x - VERTEX_SIZE / 2.0f,
y - VERTEX_SIZE / 2.0f,
z - VERTEX_SIZE / 2.0f
},
{
x + VERTEX_SIZE / 2.0f,
y + VERTEX_SIZE / 2.0f,
z + VERTEX_SIZE / 2.0f
}
});
if (collision.hit) {
input.collision_mass = mass;
break;
}
++mass;
}
}
// Find max distance to interpolate colors in the given [0, max] range
int max_distance = 0;
for (const int distance : state.get_distances()) {
if (distance > max_distance) {
max_distance = distance;
}
}
const auto lerp_color = [&](const Color from, const Color to, const int distance)
{
const float weight = 1.0 - static_cast<float>(distance) / max_distance;
Color result;
result.r = static_cast<u8>((1 - weight) * from.r + weight * to.r);
result.g = static_cast<u8>((1 - weight) * from.g + weight * to.g);
result.b = static_cast<u8>((1 - weight) * from.b + weight * to.b);
result.a = static_cast<u8>((1 - weight) * from.a + weight * to.a);
return result;
};
const std::vector<int>& distances = state.get_distances();
size_t mass = 0;
for (const auto& [x, y, z] : masses) {
transforms.emplace_back(MatrixTranslate(x, y, z));
@ -81,9 +156,14 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
} else if (state.get_visit_counts().at(mass) > 0) {
// Visited vertex
c = VERTEX_VISITED_COLOR;
} else if (input.color_by_distance && distances.size() == masses.size()) {
c = lerp_color(VERTEX_FARTHEST_COLOR, VERTEX_CLOSEST_COLOR, static_cast<float>(distances[mass]));
}
if (mass == input.collision_mass) {
c = RED;
}
// Current vertex is drawn as individual cube to increase its size
// Current vertex is drawn as individual cube to increase its size
colors.emplace_back(c);
++mass;
}
@ -96,23 +176,53 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
ClearBackground(RAYWHITE);
BeginMode3D(camera.camera);
// Draw springs (batched)
rlDrawRenderBatchActive();
// Draw edges
{
#ifdef TRACY
ZoneNamedN(draw_springs, "DrawSprings", true);
#endif
rlBegin(RL_LINES);
for (const auto& [from, to] : state.get_links()) {
if (masses.size() > from && masses.size() > to) {
const auto& [ax, ay, az] = masses[from];
const auto& [bx, by, bz] = masses[to];
rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
rlVertex3f(ax, ay, az);
rlVertex3f(bx, by, bz);
}
}
rlEnd();
rlEnableShader(edge_shader.id);
Matrix modelview = rlGetMatrixModelview();
Matrix projection = rlGetMatrixProjection();
Matrix mvp = MatrixMultiply(modelview, projection);
rlSetUniformMatrix(edge_shader.locs[SHADER_LOC_MATRIX_MVP], mvp);
const std::array<float, 4> edge_color = {
EDGE_COLOR.r / 255.0f,
EDGE_COLOR.g / 255.0f,
EDGE_COLOR.b / 255.0f,
EDGE_COLOR.a / 255.0f
};
rlSetUniform(edge_color_loc, edge_color.data(), SHADER_UNIFORM_VEC4, 1);
glBindVertexArray(edge_vao_id);
glDrawArrays(GL_LINES, 0, edge_vertices.size());
glBindVertexArray(0);
rlDisableShader();
// This draws triangles:
// rlEnableVertexArray(edge_vao_id);
// rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
// rlDrawVertexArray(0, edge_vertices.size());
// rlDisableVertexArray();
// This is fucking slow:
// rlBegin(RL_LINES);
// for (const auto& [from, to] : state.get_links()) {
// if (masses.size() > from && masses.size() > to) {
// const auto& [ax, ay, az] = masses[from];
// const auto& [bx, by, bz] = masses[to];
// rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
// rlVertex3f(ax, ay, az);
// rlVertex3f(bx, by, bz);
// }
// }
// rlEnd();
}
// Draw masses (instanced)
@ -181,32 +291,47 @@ auto renderer::draw_menu() const -> void
EndTextureMode();
}
auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
auto renderer::draw_textures(const int fps,
const int ups,
const size_t mass_count,
const size_t spring_count) const -> void
{
BeginDrawing();
DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Vector2(0, 0), WHITE);
DrawTextureRec(menu_target.texture,
Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Vector2(0, 0),
WHITE);
DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), WHITE);
DrawTextureRec(graph_target.texture, Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height),
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
Vector2(0, MENU_HEIGHT),
WHITE);
DrawTextureRec(graph_target.texture,
Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height),
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT),
WHITE);
// Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK);
DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT),
1.0f,
BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f,
MENU_HEIGHT,
GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT),
1.0f,
BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT), 1.0f, BLACK);
gui.draw(fps, ups, mass_count, spring_count);
EndDrawing();
}
auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups, const size_t mass_count,
auto renderer::render(const std::vector<Vector3>& masses,
const int fps,
const int ups,
const size_t mass_count,
const size_t spring_count) -> void
{
update_texture_sizes();

View File

@ -28,7 +28,7 @@ auto state_manager::synced_insert_link(size_t first_index, size_t second_index)
}
auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void
const std::vector<spring>& _links) -> void
{
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared");
@ -374,7 +374,7 @@ auto state_manager::get_path_length() const -> size_t
return winning_path.size();
}
auto state_manager::get_links() const -> const std::vector<std::pair<size_t, size_t>>&
auto state_manager::get_links() const -> const std::vector<spring>&
{
return links;
}
@ -429,6 +429,11 @@ auto state_manager::has_distances() const -> bool
return !node_target_distances.empty();
}
auto state_manager::get_distances() const -> std::vector<int>
{
return node_target_distances.distances;
}
auto state_manager::get_total_moves() const -> size_t
{
return total_moves;

View File

@ -7,8 +7,12 @@
#define RAYGUI_IMPLEMENTATION
#include <raygui.h>
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height,
const int _columns, const int _rows) -> void
auto user_interface::grid::update_bounds(const int _x,
const int _y,
const int _width,
const int _height,
const int _columns,
const int _rows) -> void
{
x = _x;
y = _y;
@ -51,7 +55,9 @@ auto user_interface::grid::bounds(const int _x, const int _y, const int _width,
const int cell_width = (width - padding) / columns;
const int cell_height = (height - padding) / rows;
return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding,
return Rectangle(x + _x * cell_width + padding,
y + _y * cell_height + padding,
_width * cell_width - padding,
_height * cell_height - padding);
}
@ -65,7 +71,9 @@ auto user_interface::grid::square_bounds() const -> Rectangle
return bounds;
}
auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width,
auto user_interface::grid::square_bounds(const int _x,
const int _y,
const int _width,
const int _height) const -> Rectangle
{
// Assumes each cell is square, so either width or height are not completely
@ -84,8 +92,10 @@ auto user_interface::grid::square_bounds(const int _x, const int _y, const int _
const int x_offset = (width - grid_width) / 2;
const int y_offset = (height - grid_height) / 2;
return Rectangle(x_offset + _x * (cell_size + padding) + padding, y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1));
return Rectangle(x_offset + _x * (cell_size + padding) + padding,
y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1),
_height * cell_size + padding * (_height - 1));
}
auto user_interface::init() -> void
@ -146,16 +156,26 @@ auto user_interface::get_default_style() -> default_style
// access...
return {
{
GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL),
GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED),
GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)
GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL),
GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL),
GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL),
GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED),
GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED),
GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED),
GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)
},
GuiGetStyle(DEFAULT, BACKGROUND_COLOR), GuiGetStyle(DEFAULT, LINE_COLOR), GuiGetStyle(DEFAULT, TEXT_SIZE),
GuiGetStyle(DEFAULT, TEXT_SPACING), GuiGetStyle(DEFAULT, TEXT_LINE_SPACING),
GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)
GuiGetStyle(DEFAULT, BACKGROUND_COLOR),
GuiGetStyle(DEFAULT, LINE_COLOR),
GuiGetStyle(DEFAULT, TEXT_SIZE),
GuiGetStyle(DEFAULT, TEXT_SPACING),
GuiGetStyle(DEFAULT, TEXT_LINE_SPACING),
GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL),
GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)
};
}
@ -191,14 +211,21 @@ auto user_interface::get_component_style(const int component) -> component_style
{
return {
{
GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)
GuiGetStyle(component, BORDER_COLOR_NORMAL),
GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, TEXT_COLOR_NORMAL),
GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED),
GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED),
GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED),
GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED),
GuiGetStyle(component, TEXT_COLOR_DISABLED)
},
GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, BORDER_WIDTH),
GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT)
};
}
@ -229,11 +256,16 @@ auto user_interface::set_component_style(const int component, const component_st
auto user_interface::popup_bounds() -> Rectangle
{
return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f,
static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, POPUP_WIDTH, POPUP_HEIGHT);
static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f,
POPUP_WIDTH,
POPUP_HEIGHT);
}
auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color,
const bool enabled, const int font_size) const -> int
auto user_interface::draw_button(const Rectangle bounds,
const std::string& label,
const Color color,
const bool enabled,
const int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -263,16 +295,25 @@ auto user_interface::draw_button(const Rectangle bounds, const std::string& labe
return pressed;
}
auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height,
const std::string& label, const Color color, const bool enabled,
auto user_interface::draw_menu_button(const int x,
const int y,
const int width,
const int height,
const std::string& label,
const Color color,
const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_button(bounds, label, color, enabled, font_size);
}
auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color, bool enabled,
auto user_interface::draw_toggle_slider(const Rectangle bounds,
const std::string& off_label,
const std::string& on_label,
int* active,
Color color,
bool enabled,
int font_size) const -> int
{
// Save original styling
@ -308,16 +349,29 @@ auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::strin
return pressed;
}
auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height,
const std::string& off_label, const std::string& on_label, int* active,
const Color color, const bool enabled, const int font_size) const -> int
auto user_interface::draw_menu_toggle_slider(const int x,
const int y,
const int width,
const int height,
const std::string& off_label,
const std::string& on_label,
int* active,
const Color color,
const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size);
}
auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color,
bool enabled, int font_size) const -> int
auto user_interface::draw_spinner(Rectangle bounds,
const std::string& label,
int* value,
int min,
int max,
Color color,
bool enabled,
int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -352,15 +406,26 @@ auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, in
return pressed;
}
auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height,
const std::string& label, int* value, const int min, const int max,
const Color color, const bool enabled, const int font_size) const -> int
auto user_interface::draw_menu_spinner(const int x,
const int y,
const int width,
const int height,
const std::string& label,
int* value,
const int min,
const int max,
const Color color,
const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_spinner(bounds, label, value, min, max, color, enabled, font_size);
}
auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled,
auto user_interface::draw_label(const Rectangle bounds,
const std::string& text,
const Color color,
const bool enabled,
const int font_size) const -> int
{
// Save original styling
@ -391,7 +456,11 @@ auto user_interface::draw_label(const Rectangle bounds, const std::string& text,
return pressed;
}
auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color,
auto user_interface::draw_board_block(const int x,
const int y,
const int width,
const int height,
const Color color,
const bool enabled) const -> bool
{
component_style s = get_component_style(BUTTON);
@ -400,7 +469,7 @@ auto user_interface::draw_board_block(const int x, const int y, const int width,
const Rectangle bounds = board_grid.square_bounds(x, y, width, height);
const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds);
const bool pressed = puzzle::block(x, y, width, height, false).covers(input.sel_x, input.sel_y);
const bool pressed = block(x, y, width, height, false).covers(input.sel_x, input.sel_y);
// Background to make faded colors work
DrawRectangleRec(bounds, RAYWHITE);
@ -438,7 +507,15 @@ auto user_interface::window_open() const -> bool
auto user_interface::draw_menu_header(const Color color) const -> void
{
int preset = static_cast<int>(state.get_current_preset());
draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, static_cast<int>(state.get_preset_count()), color,
draw_menu_spinner(0,
0,
1,
1,
"Preset: ",
&preset,
-1,
static_cast<int>(state.get_preset_count()),
color,
!input.editing);
if (preset > static_cast<int>(state.get_current_preset())) {
input.load_next_preset();
@ -446,11 +523,17 @@ auto user_interface::draw_menu_header(const Color color) const -> void
input.load_previous_preset();
}
draw_menu_button(1, 0, 1, 1, std::format("{}: {}/{} Blocks",
state.was_edited()
? "Modified"
: std::format("\"{}\"", state.get_current_preset_comment().substr(2)),
state.get_current_state().block_count(), puzzle::MAX_BLOCKS), color);
draw_menu_button(1,
0,
1,
1,
std::format("{}: {}/{} Blocks",
state.was_edited()
? "Modified"
: std::format("\"{}\"", state.get_current_preset_comment().substr(2)),
state.get_current_state().block_count(),
puzzle::MAX_BLOCKS),
color);
int editing = input.editing;
draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color);
@ -461,13 +544,22 @@ auto user_interface::draw_menu_header(const Color color) const -> void
auto user_interface::draw_graph_info(const Color color) const -> void
{
draw_menu_button(0, 1, 1, 1, std::format("Found {} States ({} Winning)", state.get_state_count(),
state.get_target_count()), color);
draw_menu_button(0,
1,
1,
1,
std::format("Found {} States ({} Winning)", state.get_state_count(), state.get_target_count()),
color);
draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color);
draw_menu_button(2, 1, 1, 1, std::format("{} Moves to Nearest Solution",
state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), color);
draw_menu_button(2,
1,
1,
1,
std::format("{} Moves to Nearest Solution",
state.get_path_length() > 0 ? state.get_path_length() - 1 : 0),
color);
}
auto user_interface::draw_graph_controls(const Color color) const -> void
@ -504,7 +596,14 @@ auto user_interface::draw_camera_controls(const Color color) const -> void
}
int lock_camera_mass_center = input.camera_mass_center_lock;
draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color,
draw_menu_toggle_slider(1,
3,
1,
1,
"Current Block (U)",
"Graph Center (U)",
&lock_camera_mass_center,
color,
input.camera_lock);
if (lock_camera_mass_center != input.camera_mass_center_lock) {
input.toggle_camera_mass_center_lock();
@ -537,8 +636,12 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void
};
const int visits = state.get_current_visits();
draw_menu_button(0, 4, 1, 1, std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits,
nth(visits)), color);
draw_menu_button(0,
4,
1,
1,
std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits, nth(visits)),
color);
if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) {
input.goto_optimal_next_state();
@ -556,7 +659,12 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void
input.goto_most_distant_state();
}
if (draw_menu_button(2, 5, 1, 1, "Go to Starting State (R)", color,
if (draw_menu_button(2,
5,
1,
1,
"Go to Starting State (R)",
color,
state.get_current_index() != state.get_starting_index())) {
input.goto_starting_state();
}
@ -634,8 +742,13 @@ auto user_interface::draw_save_preset_popup() -> void
}
// Returns the pressed button index
const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", "Ok;Cancel",
preset_comment.data(), 255, nullptr);
const int button = GuiTextInputBox(popup_bounds(),
"Save as Preset",
"Enter Preset Name",
"Ok;Cancel",
preset_comment.data(),
255,
nullptr);
if (button == 1) {
state.save_current_to_preset_file(preset_comment.data());
}
@ -698,7 +811,11 @@ auto user_interface::draw_puzzle_board() -> void
{
const puzzle& current = state.get_current_state();
board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.get_width(),
board_grid.update_bounds(0,
MENU_HEIGHT,
GetScreenWidth() / 2,
GetScreenHeight() - MENU_HEIGHT,
current.get_width(),
current.get_height());
// Draw outer border
@ -706,18 +823,22 @@ auto user_interface::draw_puzzle_board() -> void
DrawRectangleRec(bounds, current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED);
// Draw inner borders
DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING,
DrawRectangle(bounds.x + BOARD_PADDING,
bounds.y + BOARD_PADDING,
bounds.width - 2 * BOARD_PADDING,
bounds.height - 2 * BOARD_PADDING,
current.get_restricted() ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
// Draw target opening
// TODO: Only draw single direction (in corner) if restricted (use target block principal
// direction)
const std::optional<puzzle::block> target_block = current.try_get_target_block();
const std::optional<block> target_block = current.try_get_target_block();
const int target_x = current.get_goal_x();
const int target_y = current.get_goal_y();
if (current.get_goal() && target_block) {
auto [x, y, width, height] = board_grid.square_bounds(target_x, target_y, target_block->get_width(),
auto [x, y, width, height] = board_grid.square_bounds(target_x,
target_y,
target_block->get_width(),
target_block->get_height());
const Color opening_color = Fade(current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3);
@ -764,7 +885,7 @@ auto user_interface::draw_puzzle_board() -> void
}
// Draw blocks
for (const puzzle::block b : current.block_view()) {
for (const block b : current.block_view()) {
Color c = BLOCK_COLOR;
if (b.get_target()) {
c = TARGET_BLOCK_COLOR;
@ -782,16 +903,22 @@ auto user_interface::draw_puzzle_board() -> void
if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && input.hov_y >=
input.block_add_y) {
bool collides = false;
for (const puzzle::block b : current.block_view()) {
if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, false))) {
for (const block b : current.block_view()) {
if (b.collides(block(input.block_add_x,
input.block_add_y,
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1,
false))) {
collides = true;
break;
}
}
if (!collides) {
draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, PURPLE);
draw_board_block(input.block_add_x,
input.block_add_y,
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1,
PURPLE);
}
}
}
@ -803,7 +930,8 @@ auto user_interface::draw_puzzle_board() -> void
// Draw goal boundaries when editing
if (input.editing && current.get_goal() && target_block) {
DrawRectangleLinesEx(
board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), 2.0,
board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()),
2.0,
TARGET_BLOCK_COLOR);
}
}

View File

@ -10,7 +10,7 @@ TEST_CASE("bitmap_is_full all bits set", "[puzzle][board]")
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
uint64_t bitmap = -1;
u64 bitmap = -1;
REQUIRE(p1.bitmap_is_full(bitmap));
REQUIRE(p2.bitmap_is_full(bitmap));
@ -24,7 +24,7 @@ TEST_CASE("bitmap_is_full no bits set", "[puzzle][board]")
puzzle p2(3, 4);
puzzle p3(5, 4);
puzzle p4(3, 7);
uint64_t bitmap = 0;
u64 bitmap = 0;
REQUIRE_FALSE(p1.bitmap_is_full(bitmap));
REQUIRE_FALSE(p2.bitmap_is_full(bitmap));
@ -39,10 +39,10 @@ TEST_CASE("bitmap_is_full necessary bits set", "[puzzle][board]")
puzzle p3(5, 4);
puzzle p4(3, 7);
uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5
uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4
uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4
uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7
u64 bitmap1 = (1ull << 25) - 1; // 5 * 5
u64 bitmap2 = (1ull << 12) - 1; // 3 * 4
u64 bitmap3 = (1ull << 20) - 1; // 5 * 4
u64 bitmap4 = (1ull << 21) - 1; // 3 * 7
REQUIRE(p1.bitmap_is_full(bitmap1));
REQUIRE(p2.bitmap_is_full(bitmap2));
@ -57,10 +57,10 @@ TEST_CASE("bitmap_is_full necessary bits not set", "[puzzle][board]")
puzzle p3(5, 4);
puzzle p4(3, 7);
uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5
uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4
uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4
uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7
u64 bitmap1 = (1ull << 25) - 1; // 5 * 5
u64 bitmap2 = (1ull << 12) - 1; // 3 * 4
u64 bitmap3 = (1ull << 20) - 1; // 5 * 4
u64 bitmap4 = (1ull << 21) - 1; // 3 * 7
bitmap1 &= ~(1ull << 12);
bitmap2 &= ~(1ull << 6);

View File

@ -5,7 +5,7 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
static auto board_mask(const int w, const int h) -> uint64_t
static auto board_mask(const int w, const int h) -> u64
{
const int cells = w * h;
if (cells == 64) {
@ -31,7 +31,7 @@ TEST_CASE("Full board detection respects width*height only", "[puzzle][board]")
puzzle p(w, h);
uint64_t mask = board_mask(w, h);
u64 mask = board_mask(w, h);
int x = -1, y = -1;
@ -51,7 +51,7 @@ TEST_CASE("Single empty cell at various positions", "[puzzle][board]")
auto empty_index = GENERATE_COPY(values<int>({ 0, cells / 2, cells - 1}));
uint64_t bitmap = board_mask(w, h);
u64 bitmap = board_mask(w, h);
bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1;
@ -65,10 +65,10 @@ TEST_CASE("Bits outside board are ignored", "[puzzle][board]")
{
puzzle p(3, 3); // 9 cells
uint64_t mask = board_mask(3, 3);
u64 mask = board_mask(3, 3);
// Board is full
uint64_t bitmap = mask;
u64 bitmap = mask;
// Set extra bits outside 9 cells
bitmap |= (1ULL << 20);
@ -83,7 +83,7 @@ TEST_CASE("First empty found in forward search branch", "[puzzle][branch]")
puzzle p(4, 4); // 16 cells
// Only MSB (within board) set
uint64_t bitmap = (1ULL << 15);
u64 bitmap = (1ULL << 15);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
@ -97,7 +97,7 @@ TEST_CASE("Backward search branch finds gap before MSB cluster", "[puzzle][branc
puzzle p(4, 4); // 16 cells
// Set bits 15,14,13 but leave 12 empty
uint64_t bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13);
u64 bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
@ -112,7 +112,7 @@ TEST_CASE("Rectangular board coordinate mapping", "[puzzle][rect]")
int empty_index = 11;
uint64_t bitmap = board_mask(5, 3);
u64 bitmap = board_mask(5, 3);
bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1;
@ -126,7 +126,7 @@ TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]")
{
puzzle p(7, 8); // 56 cells
uint64_t mask = board_mask(7, 8);
u64 mask = board_mask(7, 8);
// Full board should return false
int x = -1, y = -1;
@ -142,7 +142,7 @@ TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]")
}
// --- Oracle: find first zero bit inside board ---
static auto oracle_find_first_empty(uint64_t bitmap, int w, int h, int& x, int& y) -> bool
static auto oracle_find_first_empty(u64 bitmap, int w, int h, int& x, int& y) -> bool
{
int cells = w * h;
@ -164,13 +164,13 @@ TEST_CASE("Oracle validation across board sizes 3x3 to 8x8", "[puzzle][oracle]")
puzzle p(w, h);
uint64_t mask = board_mask(w, h);
u64 mask = board_mask(w, h);
std::mt19937_64 rng(12345);
std::uniform_int_distribution<uint64_t> dist(0, UINT64_MAX);
std::uniform_int_distribution<u64> dist(0, UINT64_MAX);
for (int iteration = 0; iteration < 200; ++iteration) {
uint64_t bitmap = dist(rng);
u64 bitmap = dist(rng);
int ox = -1, oy = -1;
bool oracle_result = oracle_find_first_empty(bitmap, w, h, ox, oy);
@ -191,7 +191,7 @@ TEST_CASE("Bits set outside board only behaves as empty board", "[puzzle][outsid
{
puzzle p(3, 3); // 9 cells
uint64_t bitmap = (1ULL << 40) | (1ULL << 63);
u64 bitmap = (1ULL << 40) | (1ULL << 63);
int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
@ -207,7 +207,7 @@ TEST_CASE("Last valid cell empty stresses upper bound", "[puzzle][boundary]")
puzzle p(w, h);
int cells = w * h;
uint64_t bitmap = board_mask(w, h);
u64 bitmap = board_mask(w, h);
// Clear last valid bit
bitmap &= ~(1ULL << (cells - 1));
@ -235,7 +235,7 @@ TEST_CASE("Board sizes between 33 and 63 cells", "[puzzle][midrange]")
for (int index : {31, 32, cells - 2}) {
if (index >= cells) continue;
uint64_t bitmap = board_mask(w, h);
u64 bitmap = board_mask(w, h);
bitmap &= ~(1ULL << index);
int x = -1, y = -1;
@ -250,7 +250,7 @@ TEST_CASE("Multiple holes choose lowest index", "[puzzle][multiple]")
{
puzzle p(5, 5);
uint64_t bitmap = board_mask(5, 5);
u64 bitmap = board_mask(5, 5);
// Clear several positions
bitmap &= ~(1ULL << 3);

View File

@ -2,7 +2,7 @@
#include <catch2/catch_template_test_macros.hpp>
#include <cstdint>
#include "util.hpp"
#include "bits.hpp"
// =============================================================================
// Catch2
@ -31,8 +31,8 @@
// 4. TEMPLATE_TEST_CASE(name, tags, Type1, Type2, ...)
// A parameterised test that is instantiated once for each type listed.
// Inside the test body, the alias `TestType` refers to the current type.
// This avoids duplicating identical logic for uint8_t, uint16_t, uint32_t,
// and uint64_t. Catch2 automatically appends the type name to the test name
// This avoids duplicating identical logic for u8, u16, u32,
// and u64. Catch2 automatically appends the type name to the test name
// in the output so you can see which instantiation failed.
//
// 5. Tags (e.g. "[create_mask]", "[round-trip]")
@ -49,7 +49,7 @@
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]",
uint8_t, uint16_t, uint32_t, uint64_t)
u8, u16, u32, u64)
{
SECTION("single bit mask at bit 0") {
auto m = create_mask<TestType>(0, 0);
@ -72,16 +72,16 @@ TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]",
}
SECTION("full-width mask returns all ones") {
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
constexpr u8 last = sizeof(TestType) * 8 - 1;
auto m = create_mask<TestType>(0, last);
REQUIRE(m == static_cast<TestType>(~TestType{0}));
}
}
TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") {
REQUIRE(create_mask<uint32_t>(0, 15) == 0x0000FFFF);
REQUIRE(create_mask<uint32_t>(0, 31) == 0xFFFFFFFF);
REQUIRE(create_mask<uint32_t>(16, 31) == 0xFFFF0000);
REQUIRE(create_mask<u32>(0, 15) == 0x0000FFFF);
REQUIRE(create_mask<u32>(0, 31) == 0xFFFFFFFF);
REQUIRE(create_mask<u32>(16, 31) == 0xFFFF0000);
}
// ---------------------------------------------------------------------------
@ -89,11 +89,11 @@ TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") {
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
u8, u16, u32, u64)
{
SECTION("clear all bits") {
TestType val = static_cast<TestType>(~TestType{0});
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
constexpr u8 last = sizeof(TestType) * 8 - 1;
clear_bits(val, 0, last);
REQUIRE(val == TestType{0});
}
@ -128,55 +128,55 @@ TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]",
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
u8, u16, u32, u64)
{
SECTION("set lower nibble on zero") {
TestType val = TestType{0};
set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0xA));
set_bits(val, u8{0}, u8{3}, static_cast<TestType>(0xA));
REQUIRE(val == static_cast<TestType>(0x0A));
}
SECTION("set upper nibble on zero") {
TestType val = TestType{0};
set_bits(val, uint8_t{4}, uint8_t{7}, static_cast<TestType>(0xB));
set_bits(val, u8{4}, u8{7}, static_cast<TestType>(0xB));
REQUIRE(val == static_cast<TestType>(0xB0));
}
SECTION("set_bits replaces existing bits") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0x5));
set_bits(val, u8{0}, u8{3}, static_cast<TestType>(0x5));
REQUIRE(val == static_cast<TestType>(0xF5));
}
SECTION("set single bit to 1") {
TestType val = TestType{0};
set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(1));
set_bits(val, u8{3}, u8{3}, static_cast<TestType>(1));
REQUIRE(val == static_cast<TestType>(0x08));
}
SECTION("set single bit to 0") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(0));
set_bits(val, u8{3}, u8{3}, static_cast<TestType>(0));
REQUIRE(val == static_cast<TestType>(0xF7));
}
SECTION("setting value 0 clears the range") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{0}, uint8_t{7}, static_cast<TestType>(0));
set_bits(val, u8{0}, u8{7}, static_cast<TestType>(0));
REQUIRE(val == TestType{0});
}
}
TEST_CASE("set_bits with different value type (U != T)", "[set_bits]") {
uint32_t val = 0;
constexpr uint8_t small_val = 0x3F;
set_bits(val, uint8_t{8}, uint8_t{13}, small_val);
REQUIRE(val == (uint32_t{0x3F} << 8));
u32 val = 0;
constexpr u8 small_val = 0x3F;
set_bits(val, u8{8}, u8{13}, small_val);
REQUIRE(val == (u32{0x3F} << 8));
}
TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") {
uint32_t val = 0xDEADBEEF;
set_bits(val, uint8_t{8}, uint8_t{15}, uint32_t{0x42});
u32 val = 0xDEADBEEF;
set_bits(val, u8{8}, u8{15}, u32{0x42});
REQUIRE(val == 0xDEAD42EF);
}
@ -185,53 +185,53 @@ TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") {
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
u8, u16, u32, u64)
{
SECTION("get lower nibble") {
TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, uint8_t{0}, uint8_t{3});
auto result = get_bits(val, u8{0}, u8{3});
REQUIRE(result == TestType{0xB});
}
SECTION("get upper nibble") {
TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, uint8_t{4}, uint8_t{7});
auto result = get_bits(val, u8{4}, u8{7});
REQUIRE(result == TestType{0xA});
}
SECTION("get single bit that is set") {
TestType val = static_cast<TestType>(0x08);
auto result = get_bits(val, uint8_t{3}, uint8_t{3});
auto result = get_bits(val, u8{3}, u8{3});
REQUIRE(result == TestType{1});
}
SECTION("get single bit that is clear") {
TestType val = static_cast<TestType>(0xF7);
auto result = get_bits(val, uint8_t{3}, uint8_t{3});
auto result = get_bits(val, u8{3}, u8{3});
REQUIRE(result == TestType{0});
}
SECTION("get all bits") {
TestType val = static_cast<TestType>(~TestType{0});
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
auto result = get_bits(val, uint8_t{0}, last);
constexpr u8 last = sizeof(TestType) * 8 - 1;
auto result = get_bits(val, u8{0}, last);
REQUIRE(result == val);
}
SECTION("get from zero returns zero") {
TestType val = TestType{0};
auto result = get_bits(val, uint8_t{0}, uint8_t{7});
auto result = get_bits(val, u8{0}, u8{7});
REQUIRE(result == TestType{0});
}
}
TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") {
constexpr uint32_t val = 0xDEADBEEF;
constexpr u32 val = 0xDEADBEEF;
REQUIRE(get_bits(val, uint8_t{0}, uint8_t{7}) == 0xEF);
REQUIRE(get_bits(val, uint8_t{8}, uint8_t{15}) == 0xBE);
REQUIRE(get_bits(val, uint8_t{16}, uint8_t{23}) == 0xAD);
REQUIRE(get_bits(val, uint8_t{24}, uint8_t{31}) == 0xDE);
REQUIRE(get_bits(val, u8{0}, u8{7}) == 0xEF);
REQUIRE(get_bits(val, u8{8}, u8{15}) == 0xBE);
REQUIRE(get_bits(val, u8{16}, u8{23}) == 0xAD);
REQUIRE(get_bits(val, u8{24}, u8{31}) == 0xDE);
}
// ---------------------------------------------------------------------------
@ -239,29 +239,29 @@ TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") {
// ---------------------------------------------------------------------------
TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") {
uint32_t reg = 0;
u32 reg = 0;
set_bits(reg, uint8_t{4}, uint8_t{11}, uint32_t{0xAB});
REQUIRE(get_bits(reg, uint8_t{4}, uint8_t{11}) == 0xAB);
set_bits(reg, u8{4}, u8{11}, u32{0xAB});
REQUIRE(get_bits(reg, u8{4}, u8{11}) == 0xAB);
REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{3}) == 0x0);
REQUIRE(get_bits(reg, uint8_t{12}, uint8_t{31}) == 0x0);
REQUIRE(get_bits(reg, u8{0}, u8{3}) == 0x0);
REQUIRE(get_bits(reg, u8{12}, u8{31}) == 0x0);
}
TEST_CASE("multiple set_bits on different ranges", "[round-trip]") {
uint32_t reg = 0;
u32 reg = 0;
set_bits(reg, uint8_t{0}, uint8_t{7}, uint32_t{0x01});
set_bits(reg, uint8_t{8}, uint8_t{15}, uint32_t{0x02});
set_bits(reg, uint8_t{16}, uint8_t{23}, uint32_t{0x03});
set_bits(reg, uint8_t{24}, uint8_t{31}, uint32_t{0x04});
set_bits(reg, u8{0}, u8{7}, u32{0x01});
set_bits(reg, u8{8}, u8{15}, u32{0x02});
set_bits(reg, u8{16}, u8{23}, u32{0x03});
set_bits(reg, u8{24}, u8{31}, u32{0x04});
REQUIRE(reg == 0x04030201);
}
TEST_CASE("64-bit round-trip", "[round-trip]") {
uint64_t reg = 0;
set_bits(reg, uint8_t{32}, uint8_t{63}, uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, uint8_t{32}, uint8_t{63}) == uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{31}) == uint64_t{0});
u64 reg = 0;
set_bits(reg, u8{32}, u8{63}, u64{0xCAFEBABE});
REQUIRE(get_bits(reg, u8{32}, u8{63}) == u64{0xCAFEBABE});
REQUIRE(get_bits(reg, u8{0}, u8{31}) == u64{0});
}

View File

@ -12,7 +12,7 @@
TEST_CASE("Block creation and field access", "[block]")
{
puzzle::block b(1, 2, 3, 4, true, false);
block b(1, 2, 3, 4, true, false);
CHECK(b.get_x() == 1);
CHECK(b.get_y() == 2);
@ -25,16 +25,16 @@ TEST_CASE("Block creation and field access", "[block]")
TEST_CASE("Block invalid by default", "[block]")
{
puzzle::block b;
block b;
CHECK_FALSE(b.valid());
}
TEST_CASE("Block comparison ordering", "[block]")
{
// Row-major: (0,0) < (1,0) < (0,1)
puzzle::block a(0, 0, 1, 1);
puzzle::block b(1, 0, 1, 1);
puzzle::block c(0, 1, 1, 1);
block a(0, 0, 1, 1);
block b(1, 0, 1, 1);
block c(0, 1, 1, 1);
CHECK(a < b);
CHECK(b < c);
@ -43,7 +43,7 @@ TEST_CASE("Block comparison ordering", "[block]")
TEST_CASE("Block principal_dirs for horizontal block", "[block]")
{
puzzle::block b(0, 0, 3, 1); // wider than tall
block b(0, 0, 3, 1); // wider than tall
CHECK((b.principal_dirs() & eas));
CHECK((b.principal_dirs() & wes));
CHECK_FALSE((b.principal_dirs() & nor));
@ -52,7 +52,7 @@ TEST_CASE("Block principal_dirs for horizontal block", "[block]")
TEST_CASE("Block principal_dirs for vertical block", "[block]")
{
puzzle::block b(0, 0, 1, 3); // taller than wide
block b(0, 0, 1, 3); // taller than wide
CHECK((b.principal_dirs() & nor));
CHECK((b.principal_dirs() & sou));
CHECK_FALSE((b.principal_dirs() & eas));
@ -61,7 +61,7 @@ TEST_CASE("Block principal_dirs for vertical block", "[block]")
TEST_CASE("Block principal_dirs for square block", "[block]")
{
puzzle::block b(0, 0, 2, 2);
block b(0, 0, 2, 2);
CHECK((b.principal_dirs() & nor));
CHECK((b.principal_dirs() & sou));
CHECK((b.principal_dirs() & eas));
@ -70,7 +70,7 @@ TEST_CASE("Block principal_dirs for square block", "[block]")
TEST_CASE("Block covers", "[block]")
{
puzzle::block b(1, 2, 3, 2);
block b(1, 2, 3, 2);
// Covers (1,2) to (3,3)
CHECK(b.covers(1, 2));
CHECK(b.covers(3, 3));
@ -83,10 +83,10 @@ TEST_CASE("Block covers", "[block]")
TEST_CASE("Block collides", "[block]")
{
puzzle::block a(0, 0, 2, 2);
puzzle::block b(1, 1, 2, 2);
puzzle::block c(2, 2, 1, 1);
puzzle::block d(3, 0, 1, 1);
block a(0, 0, 2, 2);
block b(1, 1, 2, 2);
block c(2, 2, 1, 1);
block d(3, 0, 1, 1);
CHECK(a.collides(b));
CHECK(b.collides(a));
@ -114,7 +114,7 @@ TEST_CASE("Puzzle creation and meta access", "[puzzle]")
TEST_CASE("Puzzle add and remove block", "[puzzle]")
{
puzzle p(4, 4, 0, 0, false, false);
puzzle::block b(0, 0, 2, 1);
block b(0, 0, 2, 1);
auto p2 = p.try_add_block(b);
REQUIRE(p2.has_value());
@ -127,8 +127,8 @@ TEST_CASE("Puzzle add and remove block", "[puzzle]")
TEST_CASE("Puzzle meta roundtrip for all valid sizes", "[puzzle]")
{
for (uint8_t w = 3; w <= 8; ++w) {
for (uint8_t h = 3; h <= 8; ++h) {
for (u8 w = 3; w <= 8; ++w) {
for (u8 h = 3; h <= 8; ++h) {
puzzle p(w, h, 0, 0, true, false);
CHECK(p.get_width() == w);
CHECK(p.get_height() == h);
@ -151,11 +151,11 @@ TEST_CASE("Puzzle block_count", "[puzzle]")
puzzle p(4, 4, 0, 0, false, false);
CHECK(p.block_count() == 0);
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
auto p2 = p.try_add_block(block(0, 0, 1, 1));
REQUIRE(p2);
CHECK(p2->block_count() == 1);
auto p3 = p2->try_add_block(puzzle::block(2, 2, 1, 1));
auto p3 = p2->try_add_block(block(2, 2, 1, 1));
REQUIRE(p3);
CHECK(p3->block_count() == 2);
}
@ -163,18 +163,18 @@ TEST_CASE("Puzzle block_count", "[puzzle]")
TEST_CASE("Puzzle cannot add overlapping block", "[puzzle]")
{
puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 2));
auto p2 = p.try_add_block(block(0, 0, 2, 2));
REQUIRE(p2);
// Overlapping
auto p3 = p2->try_add_block(puzzle::block(1, 1, 2, 2));
auto p3 = p2->try_add_block(block(1, 1, 2, 2));
CHECK_FALSE(p3.has_value());
}
TEST_CASE("Puzzle cannot add block outside board", "[puzzle]")
{
puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(3, 3, 2, 2));
auto p2 = p.try_add_block(block(3, 3, 2, 2));
CHECK_FALSE(p2.has_value());
}
@ -184,17 +184,17 @@ TEST_CASE("Puzzle cannot add block outside board", "[puzzle]")
TEST_CASE("bitmap_set_bit and bitmap_get_bit roundtrip", "[bitmap]")
{
for (uint8_t w = 3; w <= 8; ++w) {
uint64_t bm = 0;
for (u8 w = 3; w <= 8; ++w) {
u64 bm = 0;
// Set every cell on a w x w board
for (uint8_t y = 0; y < w; ++y) {
for (uint8_t x = 0; x < w; ++x) {
for (u8 y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) {
puzzle::bitmap_set_bit(bm, w, x, y);
}
}
// Verify all set
for (uint8_t y = 0; y < w; ++y) {
for (uint8_t x = 0; x < w; ++x) {
for (u8 y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) {
CHECK(puzzle::bitmap_get_bit(bm, w, x, y));
}
}
@ -203,12 +203,12 @@ TEST_CASE("bitmap_set_bit and bitmap_get_bit roundtrip", "[bitmap]")
TEST_CASE("bitmap_set_bit sets only the target bit", "[bitmap]")
{
for (uint8_t w = 3; w <= 8; ++w) {
for (uint8_t y = 0; y < w; ++y) {
for (uint8_t x = 0; x < w; ++x) {
uint64_t bm = 0;
for (u8 w = 3; w <= 8; ++w) {
for (u8 y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) {
u64 bm = 0;
puzzle::bitmap_set_bit(bm, w, x, y);
CHECK(bm == (uint64_t(1) << (y * w + x)));
CHECK(bm == (u64(1) << (y * w + x)));
}
}
}
@ -216,11 +216,11 @@ TEST_CASE("bitmap_set_bit sets only the target bit", "[bitmap]")
TEST_CASE("bitmap_clear_bit clears only the target bit", "[bitmap]")
{
uint8_t w = 6;
uint64_t bm = 0;
u8 w = 6;
u64 bm = 0;
puzzle::bitmap_set_bit(bm, w, 2, 3);
puzzle::bitmap_set_bit(bm, w, 4, 1);
uint64_t before = bm;
u64 before = bm;
puzzle::bitmap_clear_bit(bm, w, 2, 3);
CHECK_FALSE(puzzle::bitmap_get_bit(bm, w, 2, 3));
@ -230,10 +230,10 @@ TEST_CASE("bitmap_clear_bit clears only the target bit", "[bitmap]")
TEST_CASE("blocks_bitmap for single block", "[bitmap]")
{
puzzle p(6, 6, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(1, 2, 3, 1));
auto p2 = p.try_add_block(block(1, 2, 3, 1));
REQUIRE(p2);
uint64_t bm = p2->blocks_bitmap();
u64 bm = p2->blocks_bitmap();
// Block at (1,2) width 3 height 1 on a 6-wide board
// Bits: row 2, cols 1,2,3 -> positions 2*6+1=13, 14, 15
CHECK(puzzle::bitmap_get_bit(bm, 6, 1, 2));
@ -248,10 +248,10 @@ TEST_CASE("blocks_bitmap for single block", "[bitmap]")
TEST_CASE("blocks_bitmap for 2x2 block", "[bitmap]")
{
puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2));
auto p2 = p.try_add_block(block(1, 1, 2, 2));
REQUIRE(p2);
uint64_t bm = p2->blocks_bitmap();
u64 bm = p2->blocks_bitmap();
CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 1));
CHECK(puzzle::bitmap_get_bit(bm, 5, 2, 1));
CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 2));
@ -264,13 +264,13 @@ TEST_CASE("blocks_bitmap_h only includes horizontal/square blocks", "[bitmap]")
{
puzzle p(6, 6, 0, 0, true, false);
// Horizontal block (2x1) -> principal_dirs has eas
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
auto p2 = p.try_add_block(block(0, 0, 2, 1));
REQUIRE(p2);
// Vertical block (1x2) -> principal_dirs has sou, not eas
auto p3 = p2->try_add_block(puzzle::block(4, 0, 1, 2));
auto p3 = p2->try_add_block(block(4, 0, 1, 2));
REQUIRE(p3);
uint64_t bm_h = p3->blocks_bitmap_h();
u64 bm_h = p3->blocks_bitmap_h();
// Horizontal block should be in bitmap_h
CHECK(puzzle::bitmap_get_bit(bm_h, 6, 0, 0));
CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 0));
@ -283,13 +283,13 @@ TEST_CASE("blocks_bitmap_v only includes vertical/square blocks", "[bitmap]")
{
puzzle p(6, 6, 0, 0, true, false);
// Vertical block (1x2)
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 2));
auto p2 = p.try_add_block(block(0, 0, 1, 2));
REQUIRE(p2);
// Horizontal block (2x1)
auto p3 = p2->try_add_block(puzzle::block(4, 0, 2, 1));
auto p3 = p2->try_add_block(block(4, 0, 2, 1));
REQUIRE(p3);
uint64_t bm_v = p3->blocks_bitmap_v();
u64 bm_v = p3->blocks_bitmap_v();
// Vertical block should be in bitmap_v
CHECK(puzzle::bitmap_get_bit(bm_v, 6, 0, 0));
CHECK(puzzle::bitmap_get_bit(bm_v, 6, 0, 1));
@ -301,11 +301,11 @@ TEST_CASE("blocks_bitmap_v only includes vertical/square blocks", "[bitmap]")
TEST_CASE("blocks_bitmap_h and blocks_bitmap_v both include square blocks", "[bitmap]")
{
puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2));
auto p2 = p.try_add_block(block(1, 1, 2, 2));
REQUIRE(p2);
uint64_t bm_h = p2->blocks_bitmap_h();
uint64_t bm_v = p2->blocks_bitmap_v();
u64 bm_h = p2->blocks_bitmap_h();
u64 bm_v = p2->blocks_bitmap_v();
// Square block should appear in both
CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 1));
@ -320,12 +320,12 @@ TEST_CASE("bitmap_newly_occupied north - single vertical block with space above"
{
// 4x4 board, vertical 1x2 block at (1,2)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
auto p2 = p.try_add_block(block(1, 2, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, vert, nor);
@ -346,12 +346,12 @@ TEST_CASE("bitmap_newly_occupied south - single vertical block with space below"
{
// 4x4 board, vertical 1x2 block at (1,0)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
auto p2 = p.try_add_block(block(1, 0, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, vert, sou);
@ -366,12 +366,12 @@ TEST_CASE("bitmap_newly_occupied east - single horizontal block with space right
{
// 5x4 board, horizontal 2x1 block at (0,1)
puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
auto p2 = p.try_add_block(block(0, 1, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas);
@ -386,12 +386,12 @@ TEST_CASE("bitmap_newly_occupied west - single horizontal block with space left"
{
// 5x4 board, horizontal 2x1 block at (2,1)
puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1));
auto p2 = p.try_add_block(block(2, 1, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes);
@ -406,12 +406,12 @@ TEST_CASE("bitmap_newly_occupied does not wrap east across rows", "[bitmap_move]
{
// 4x4 board, horizontal 2x1 block at (2,0) - rightmost position
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(2, 0, 2, 1));
auto p2 = p.try_add_block(block(2, 0, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas);
@ -424,12 +424,12 @@ TEST_CASE("bitmap_newly_occupied does not wrap west across rows", "[bitmap_move]
{
// 4x4 board, horizontal 2x1 block at (0,1)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
auto p2 = p.try_add_block(block(0, 1, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes);
@ -441,14 +441,14 @@ TEST_CASE("bitmap_newly_occupied blocked by another block", "[bitmap_move]")
{
// 6x4 board, two horizontal blocks side by side
puzzle p(6, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
auto p2 = p.try_add_block(block(0, 0, 2, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(2, 0, 2, 1));
auto p3 = p2->try_add_block(block(2, 0, 2, 1));
REQUIRE(p3);
uint64_t combined = p3->blocks_bitmap();
uint64_t horiz = p3->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p3->blocks_bitmap();
u64 horiz = p3->blocks_bitmap_h();
u64 bm = combined;
p3->bitmap_newly_occupied_after_move(bm, horiz, eas);
@ -468,11 +468,11 @@ TEST_CASE("restricted_bitmap_get_moves north - returns correct source cell", "[g
{
// 4x4 board, vertical 1x2 block at (1,2)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
auto p2 = p.try_add_block(block(1, 2, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor);
@ -487,11 +487,11 @@ TEST_CASE("restricted_bitmap_get_moves south - returns correct source cell", "[g
{
// 4x4 board, vertical 1x2 block at (1,0)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
auto p2 = p.try_add_block(block(1, 0, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, sou);
@ -511,11 +511,11 @@ TEST_CASE("restricted_bitmap_get_moves east - returns correct source cell", "[ge
{
// 5x4 board, horizontal 2x1 block at (0,1)
puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
auto p2 = p.try_add_block(block(0, 1, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
auto moves = p2->restricted_bitmap_get_moves(combined, horiz, eas);
@ -530,11 +530,11 @@ TEST_CASE("restricted_bitmap_get_moves west - returns correct source cell", "[ge
{
// 5x4 board, horizontal 2x1 block at (2,1)
puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1));
auto p2 = p.try_add_block(block(2, 1, 2, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
auto moves = p2->restricted_bitmap_get_moves(combined, horiz, wes);
@ -549,11 +549,11 @@ TEST_CASE("restricted_bitmap_get_moves returns empty when blocked", "[get_moves]
{
// 4x4 board, vertical 1x2 block at (1,0) - can't move north (at top edge)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
auto p2 = p.try_add_block(block(1, 0, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor);
@ -569,11 +569,11 @@ TEST_CASE("restricted_bitmap_get_moves with width 6 board", "[get_moves]")
{
// 6x6 board, vertical 1x2 block at (2,3)
puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(2, 3, 1, 2));
auto p2 = p.try_add_block(block(2, 3, 1, 2));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t vert = p2->blocks_bitmap_v();
u64 combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor);
@ -588,20 +588,20 @@ TEST_CASE("restricted_bitmap_get_moves multiple blocks can move", "[get_moves]")
{
// 6x6 board, two vertical blocks with space above
puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
auto p2 = p.try_add_block(block(1, 2, 1, 2));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(4, 3, 1, 2));
auto p3 = p2->try_add_block(block(4, 3, 1, 2));
REQUIRE(p3);
uint64_t combined = p3->blocks_bitmap();
uint64_t vert = p3->blocks_bitmap_v();
u64 combined = p3->blocks_bitmap();
u64 vert = p3->blocks_bitmap_v();
auto moves = p3->restricted_bitmap_get_moves(combined, vert, nor);
// Both blocks can move north
// Block 1: newly occupied (1,1), source (1,2) = 1+2*6 = 13
// Block 2: newly occupied (4,2), source (4,3) = 4+3*6 = 22
std::set<uint8_t> move_set;
std::set<u8> move_set;
for (int i = 0; i < puzzle::MAX_BLOCKS && moves[i] != 0xFF; ++i) {
move_set.insert(moves[i]);
}
@ -617,7 +617,7 @@ TEST_CASE("restricted_bitmap_get_moves multiple blocks can move", "[get_moves]")
TEST_CASE("try_move_block_at basic moves", "[move]")
{
puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
auto p2 = p.try_add_block(block(1, 1, 1, 1));
REQUIRE(p2);
auto north = p2->try_move_block_at(1, 1, nor);
@ -640,7 +640,7 @@ TEST_CASE("try_move_block_at basic moves", "[move]")
TEST_CASE("try_move_block_at blocked by edge", "[move]")
{
puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
auto p2 = p.try_add_block(block(0, 0, 1, 1));
REQUIRE(p2);
CHECK_FALSE(p2->try_move_block_at(0, 0, nor).has_value());
@ -650,9 +650,9 @@ TEST_CASE("try_move_block_at blocked by edge", "[move]")
TEST_CASE("try_move_block_at blocked by collision", "[move]")
{
puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
auto p2 = p.try_add_block(block(0, 0, 1, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(1, 0, 1, 1));
auto p3 = p2->try_add_block(block(1, 0, 1, 1));
REQUIRE(p3);
CHECK_FALSE(p3->try_move_block_at(0, 0, eas).has_value());
@ -661,14 +661,14 @@ TEST_CASE("try_move_block_at blocked by collision", "[move]")
TEST_CASE("try_move_block_at_fast matches try_move_block_at", "[move]")
{
puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1));
auto p2 = p.try_add_block(block(1, 1, 2, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(0, 3, 1, 2));
auto p3 = p2->try_add_block(block(0, 3, 1, 2));
REQUIRE(p3);
uint64_t bm = p3->blocks_bitmap();
u64 bm = p3->blocks_bitmap();
for (const direction d : {nor, eas, sou, wes}) {
for (const dir d : {nor, eas, sou, wes}) {
// Block 0 is the first in sorted order
auto slow = p3->try_move_block_at(1, 1, d);
auto fast = p3->try_move_block_at_fast(bm, 0, d);
@ -686,7 +686,7 @@ TEST_CASE("try_move_block_at restricted mode respects principal dirs", "[move]")
{
puzzle p(5, 5, 0, 0, true, false); // restricted
// Horizontal block 2x1 at (1,2)
auto p2 = p.try_add_block(puzzle::block(1, 2, 2, 1));
auto p2 = p.try_add_block(block(1, 2, 2, 1));
REQUIRE(p2);
// In restricted mode, horizontal block can only move east/west
@ -760,16 +760,16 @@ static auto collect_adjacent_simple(const puzzle& p) -> std::set<size_t>
// Helper: collect all adjacent states using for_each_adjacent_restricted
static auto collect_adjacent_restricted(const puzzle& p) -> std::set<size_t>
{
const uint8_t w = p.get_width();
std::array<uint8_t, 64> bitmap_block_indices{};
const u8 w = p.get_width();
std::array<u8, 64> bitmap_block_indices{};
for (size_t i = 0; i < puzzle::MAX_BLOCKS; ++i) {
const puzzle::block b(p.repr_view().data()[i]);
const block b(p.repr_view().data()[i]);
if (i >= static_cast<size_t>(p.block_count())) {
break;
}
const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr();
for (uint8_t x = bx; x < bx + bw; ++x) {
for (uint8_t y = by; y < by + bh; ++y) {
for (u8 x = bx; x < bx + bw; ++x) {
for (u8 y = by; y < by + bh; ++y) {
bitmap_block_indices[y * w + x] = i;
}
}
@ -786,7 +786,7 @@ static auto collect_adjacent_restricted(const puzzle& p) -> std::set<size_t>
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - single block", "[cross_validate]")
{
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
auto p2 = p.try_add_block(block(1, 1, 1, 1));
REQUIRE(p2);
auto simple = collect_adjacent_simple(*p2);
@ -798,7 +798,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - single block
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - horizontal block", "[cross_validate]")
{
puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1));
auto p2 = p.try_add_block(block(1, 1, 2, 1));
REQUIRE(p2);
auto simple = collect_adjacent_simple(*p2);
@ -810,7 +810,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - horizontal b
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - vertical block", "[cross_validate]")
{
puzzle p(4, 5, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 2));
auto p2 = p.try_add_block(block(1, 1, 1, 2));
REQUIRE(p2);
auto simple = collect_adjacent_simple(*p2);
@ -822,9 +822,9 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - vertical blo
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - two blocks", "[cross_validate]")
{
puzzle p(5, 5, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); // horizontal
auto p2 = p.try_add_block(block(0, 0, 2, 1)); // horizontal
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(4, 1, 1, 2)); // vertical
auto p3 = p2->try_add_block(block(4, 1, 1, 2)); // vertical
REQUIRE(p3);
auto simple = collect_adjacent_simple(*p3);
@ -836,13 +836,13 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - two blocks",
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - crowded board", "[cross_validate]")
{
puzzle p(5, 5, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
auto p2 = p.try_add_block(block(0, 0, 2, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2));
auto p3 = p2->try_add_block(block(3, 0, 1, 2));
REQUIRE(p3);
auto p4 = p3->try_add_block(puzzle::block(0, 2, 1, 3));
auto p4 = p3->try_add_block(block(0, 2, 1, 3));
REQUIRE(p4);
auto p5 = p4->try_add_block(puzzle::block(2, 3, 2, 1));
auto p5 = p4->try_add_block(block(2, 3, 2, 1));
REQUIRE(p5);
auto simple = collect_adjacent_simple(*p5);
@ -854,13 +854,13 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - crowded boar
TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - 6x6 board", "[cross_validate]")
{
puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 3, 1)); // horizontal
auto p2 = p.try_add_block(block(0, 0, 3, 1)); // horizontal
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(5, 0, 1, 2)); // vertical
auto p3 = p2->try_add_block(block(5, 0, 1, 2)); // vertical
REQUIRE(p3);
auto p4 = p3->try_add_block(puzzle::block(2, 2, 1, 2)); // vertical
auto p4 = p3->try_add_block(block(2, 2, 1, 2)); // vertical
REQUIRE(p4);
auto p5 = p4->try_add_block(puzzle::block(3, 2, 2, 1)); // horizontal
auto p5 = p4->try_add_block(block(3, 2, 2, 1)); // horizontal
REQUIRE(p5);
auto simple = collect_adjacent_simple(*p5);
@ -889,7 +889,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - the main puz
TEST_CASE("explore_state_space simple case - single 1x1 block", "[explore]")
{
puzzle p(3, 3, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
auto p2 = p.try_add_block(block(1, 1, 1, 1));
REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space();
@ -901,7 +901,7 @@ TEST_CASE("explore_state_space simple case - single 1x1 block", "[explore]")
TEST_CASE("explore_state_space restricted - vertical block", "[explore]")
{
puzzle p(3, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
auto p2 = p.try_add_block(block(1, 0, 1, 2));
REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space();
@ -914,7 +914,7 @@ TEST_CASE("explore_state_space restricted - vertical block", "[explore]")
TEST_CASE("explore_state_space restricted - horizontal block", "[explore]")
{
puzzle p(4, 3, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
auto p2 = p.try_add_block(block(0, 1, 2, 1));
REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space();
@ -927,7 +927,7 @@ TEST_CASE("explore_state_space restricted - horizontal block", "[explore]")
TEST_CASE("explore_state_space restricted - square block", "[explore]")
{
puzzle p(3, 3, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
auto p2 = p.try_add_block(block(0, 0, 1, 1));
REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space();
@ -939,7 +939,7 @@ TEST_CASE("explore_state_space restricted - square block", "[explore]")
TEST_CASE("explore_state_space preserves board metadata", "[explore]")
{
puzzle p(5, 4, 2, 1, true, true);
auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1, true));
auto p2 = p.try_add_block(block(0, 0, 1, 1, true));
REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space();
@ -959,9 +959,9 @@ TEST_CASE("explore_state_space two blocks restricted", "[explore]")
// 4x4 board, restricted
// Horizontal 2x1 at (0,0) and vertical 1x2 at (3,0)
puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
auto p2 = p.try_add_block(block(0, 0, 2, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2));
auto p3 = p2->try_add_block(block(3, 0, 1, 2));
REQUIRE(p3);
auto [states, transitions] = p3->explore_state_space();
@ -988,15 +988,15 @@ TEST_CASE("explore_state_space two blocks restricted", "[explore]")
TEST_CASE("East move does not wrap for various board widths", "[bitmap_move]")
{
for (uint8_t w = 3; w <= 8; ++w) {
for (u8 w = 3; w <= 8; ++w) {
puzzle p(w, 3, 0, 0, true, false);
// Place a 1x1 block at rightmost column, row 0
auto p2 = p.try_add_block(puzzle::block(w - 1, 0, 1, 1));
auto p2 = p.try_add_block(block(w - 1, 0, 1, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas);
@ -1009,15 +1009,15 @@ TEST_CASE("East move does not wrap for various board widths", "[bitmap_move]")
TEST_CASE("West move does not wrap for various board widths", "[bitmap_move]")
{
for (uint8_t w = 3; w <= 8; ++w) {
for (u8 w = 3; w <= 8; ++w) {
puzzle p(w, 3, 0, 0, true, false);
// Place a 1x1 block at leftmost column, row 1
auto p2 = p.try_add_block(puzzle::block(0, 1, 1, 1));
auto p2 = p.try_add_block(block(0, 1, 1, 1));
REQUIRE(p2);
uint64_t combined = p2->blocks_bitmap();
uint64_t horiz = p2->blocks_bitmap_h();
uint64_t bm = combined;
u64 combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h();
u64 bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes);
@ -1034,34 +1034,34 @@ TEST_CASE("West move does not wrap for various board widths", "[bitmap_move]")
TEST_CASE("bitmap_check_collision detects collision", "[collision]")
{
puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(2, 2, 1, 1));
auto p2 = p.try_add_block(block(2, 2, 1, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(0, 0, 2, 1));
auto p3 = p2->try_add_block(block(0, 0, 2, 1));
REQUIRE(p3);
uint64_t bm = p3->blocks_bitmap();
u64 bm = p3->blocks_bitmap();
// Check if a hypothetical block at (1,2) 2x1 collides
puzzle::block hyp(1, 2, 2, 1);
block hyp(1, 2, 2, 1);
CHECK(p3->bitmap_check_collision(bm, hyp)); // overlaps with (2,2)
// Check if a hypothetical block at (3,3) 1x1 collides
puzzle::block hyp2(3, 3, 1, 1);
block hyp2(3, 3, 1, 1);
CHECK_FALSE(p3->bitmap_check_collision(bm, hyp2));
}
TEST_CASE("bitmap_check_collision directional", "[collision]")
{
puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(puzzle::block(2, 0, 1, 1));
auto p2 = p.try_add_block(block(2, 0, 1, 1));
REQUIRE(p2);
auto p3 = p2->try_add_block(puzzle::block(2, 1, 1, 1));
auto p3 = p2->try_add_block(block(2, 1, 1, 1));
REQUIRE(p3);
uint64_t bm = p3->blocks_bitmap();
u64 bm = p3->blocks_bitmap();
// Clear block at (2,1) from bitmap to check if (2,1) can move north
puzzle::block b(2, 1, 1, 1);
block b(2, 1, 1, 1);
p3->bitmap_clear_block(bm, b);
// (2,1) moving north -> check (2,0) which has a block