Compare commits

..

1 Commits

Author SHA1 Message Date
3d1efd6735 Fix conditional threadpool include 2026-03-04 22:16:57 +01:00
39 changed files with 1891 additions and 2921 deletions

2
.gitignore vendored
View File

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

View File

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

View File

@ -1,11 +1,6 @@
// ReSharper disable CppTooWideScope
// ReSharper disable CppDFAUnreadVariable
#include "puzzle.hpp" #include "puzzle.hpp"
#include <random>
#include <unordered_set>
#include <benchmark/benchmark.h> #include <benchmark/benchmark.h>
#include <boost/unordered/unordered_flat_map.hpp>
static std::vector<std::string> puzzles = { static std::vector<std::string> puzzles = {
// 0: RushHour 1 // 0: RushHour 1
@ -40,113 +35,6 @@ static std::vector<std::string> puzzles = {
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]", "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 static auto explore_state_space(benchmark::State& state) -> void
{ {
const puzzle p = puzzle(puzzles[state.range(0)]); const puzzle p = puzzle(puzzles[state.range(0)]);
@ -160,52 +48,43 @@ static auto explore_state_space(benchmark::State& state) -> void
static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void
{ {
constexpr u8 max_blocks = 5; // ReSharper disable once CppTooWideScope
constexpr uint8_t max_blocks = 5;
constexpr u8 board_width = 4; constexpr uint8_t board_width = 4;
constexpr u8 board_height = 5; constexpr uint8_t board_height = 5;
constexpr u8 goal_x = board_width - 1; constexpr uint8_t goal_x = board_width - 1;
constexpr u8 goal_y = 2; constexpr uint8_t goal_y = 2;
constexpr bool restricted = true; constexpr bool restricted = true;
const blockset2 permitted_blocks = { const boost::unordered_flat_set<puzzle::block, block_hasher2, block_equal2> permitted_blocks = {
block(0, 0, 2, 1, false, false), puzzle::block(0, 0, 2, 1, false, false),
block(0, 0, 3, 1, false, false), puzzle::block(0, 0, 3, 1, false, false),
block(0, 0, 1, 2, false, false), puzzle::block(0, 0, 1, 2, false, false),
block(0, 0, 1, 3, false, false) puzzle::block(0, 0, 1, 3, false, false)
}; };
const block target_block = block(0, 0, 2, 1, true, false); const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false);
constexpr std::tuple<u8, u8, u8, u8> target_block_pos_range = { constexpr std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> target_block_pos_range = {
0, 0,
goal_y, goal_y,
goal_x, board_width - 1,
goal_y goal_y
}; };
const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true); const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true);
for (auto _ : state) { for (auto _ : state) {
puzzleset result = p.explore_puzzle_space( boost::unordered_flat_set<puzzle, puzzle_hasher> result = p.explore_puzzle_space(
permitted_blocks, permitted_blocks,
target_block, target_block,
target_block_pos_range, target_block_pos_range,
max_blocks, max_blocks,
0,
std::nullopt); std::nullopt);
benchmark::DoNotOptimize(result); 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_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond);
BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond); BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond);

6
flake.lock generated
View File

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

115
flake.nix
View File

@ -111,9 +111,9 @@ rec {
abbr -a release "${buildRelease} && ./cmake-build-release/masssprings" abbr -a release "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a run "${buildRelease} && ./cmake-build-release/masssprings" abbr -a run "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a runclusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --moves=10 --blocks=4" 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 runtests "${buildDebug} && ./cmake-build-debug/tests" abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests"
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 runbenchs "${buildRelease} && sudo cpupower frequency-set --governor performance && ./cmake-build-release/benchmarks; sudo cpupower frequency-set --governor powersave"
abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings" 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 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" abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data"
@ -135,7 +135,7 @@ rec {
# Define custom dependencies # Define custom dependencies
# =========================================================================================== # ===========================================================================================
raygui = stdenv.mkDerivation rec { raygui = stdenv.mkDerivation (finalAttrs: {
pname = "raygui"; pname = "raygui";
version = "4.0-unstable-2026-02-24"; version = "4.0-unstable-2026-02-24";
@ -163,13 +163,13 @@ rec {
Name: raygui Name: raygui
Description: Simple and easy-to-use immediate-mode gui library Description: Simple and easy-to-use immediate-mode gui library
URL: https://github.com/raysan5/raygui URL: https://github.com/raysan5/raygui
Version: ${version} Version: ${finalAttrs.version}
Cflags: -I"{includedir}" Cflags: -I"{includedir}"
EOF EOF
runHook postInstall runHook postInstall
''; '';
}; });
thread-pool = stdenv.mkDerivation { thread-pool = stdenv.mkDerivation {
pname = "thread-pool"; pname = "thread-pool";
@ -185,69 +185,8 @@ rec {
# Header-only library # Header-only library
dontBuild = true; dontBuild = true;
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out mkdir -p $out
cp -rv ./include $out/include mv ./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
''; '';
}; };
@ -285,13 +224,11 @@ rec {
# C/C++: # C/C++:
raylib raylib
raygui raygui
glew
thread-pool thread-pool
libmorton
boost boost
# Debugging/Testing/Profiling # Debugging/Testing/Profiling
tracy_0_13 tracy-wayland
backward-cpp backward-cpp
libbfd libbfd
catch2_3 catch2_3
@ -331,29 +268,15 @@ rec {
''; '';
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out/lib mkdir -p $out/lib
cp ./${pname} $out/lib/ cp ./${pname} $out/lib/
cp -rv $src/default.puzzle $out/lib/ cp $src/default.puzzle $out/lib/
cp -rv $src/fonts $out/lib/fonts cp -r $src/fonts $out/lib/fonts
cp -rv $src/shader $out/lib/shader cp -r $src/shader $out/lib/shader
# The wrapper enters the correct working dir, so fonts/shaders/presets are available # The wrapper enters the correct working dir, so fonts/shaders/presets are available
mkdir -p $out/bin mkdir -p $out/bin
makeWrapper $out/lib/${pname} $out/bin/${pname} --chdir "$out/lib" makeWrapper $out/lib/${pname} $out/bin/${pname} --chdir "${placeholder "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
''; '';
}; };
@ -371,9 +294,7 @@ rec {
buildInputs = with windowsPkgs; [ buildInputs = with windowsPkgs; [
raylib raylib
raygui raygui
glew-windows
thread-pool thread-pool
libmorton
# Disable stacktrace since that's platform dependant and won't cross compile to windows # 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 # https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/libraries/boost/generic.nix#L43
@ -395,15 +316,11 @@ rec {
]; ];
installPhase = '' installPhase = ''
runHook preInstall
mkdir -p $out/bin mkdir -p $out/bin
cp -rv ./${pname}.exe $out/bin/ cp ./${pname}.exe $out/bin/
cp -rv $src/default.puzzle $out/bin/ cp $src/default.puzzle $out/bin/
cp -rv $src/fonts $out/bin/fonts cp -r $src/fonts $out/bin/fonts
cp -rv $src/shader $out/bin/shader cp -r $src/shader $out/bin/shader
runHook postInstall
''; '';
}; };
in rec { in rec {
@ -447,9 +364,7 @@ rec {
# C/C++: # C/C++:
raylib raylib
raygui raygui
glew
thread-pool thread-pool
libmorton
boost boost
# Debugging/Testing/Profiling # Debugging/Testing/Profiling

View File

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

View File

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

View File

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

View File

@ -0,0 +1,63 @@
#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,15 +1,11 @@
#ifndef OCTREE_HPP_ #ifndef OCTREE_HPP_
#define OCTREE_HPP_ #define OCTREE_HPP_
#include "util.hpp"
#include "config.hpp"
#include <array> #include <array>
#include <vector> #include <vector>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <libmorton/morton.h>
class octree class octree
{ {
@ -18,193 +14,38 @@ class octree
public: public:
Vector3 mass_center = Vector3Zero(); Vector3 mass_center = Vector3Zero();
float mass_total = 0.0; float mass_total = 0.0;
u8 depth = 0; Vector3 box_min = Vector3Zero(); // area start
float size = 0.0f; // Because our octree cells are cubic we don't need to store the bounds Vector3 box_max = Vector3Zero(); // area end
std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1}; std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1};
int mass_id = -1; int mass_id = -1;
bool leaf = true; bool leaf = true;
public:
[[nodiscard]] auto child_count() const -> int;
}; };
private: public:
// 21 * 3 = 63, fits in u64 for combined x/y/z morton-code static constexpr int MAX_DEPTH = 20;
static constexpr int MAX_DEPTH = 21;
std::vector<node> nodes; 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: public:
octree() = default; octree() = default;
// Required for async octree // octree(const octree& copy) = delete;
// NO_COPY_NO_MOVE(octree); // auto operator=(const octree& copy) -> octree& = delete;
// octree(octree&& move) = delete;
private: // auto operator=(octree&& move) -> octree& = delete;
[[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: public:
auto clear() -> void; [[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int;
auto reserve(size_t count) -> void; [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
[[nodiscard]] auto empty() const -> bool; -> std::pair<Vector3, Vector3>;
[[nodiscard]] auto root() const -> const node&; 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;
// Morton/linear octree implementation [[nodiscard]] auto calculate_force(int node_idx, const Vector3& pos) const -> Vector3;
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 #endif

View File

@ -24,7 +24,8 @@ public:
auto pan(Vector2 last_mouse, Vector2 mouse) -> void; 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 #endif

File diff suppressed because it is too large Load Diff

View File

@ -14,73 +14,55 @@ class renderer
{ {
private: private:
const state_manager& state; const state_manager& state;
input_handler& input; const input_handler& input;
user_interface& gui; user_interface& gui;
const orbit_camera& camera; 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 // 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); RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Edges // Batching
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; std::vector<std::pair<Vector3, Vector3>> connections;
// Vertex instancing // Instancing
static constexpr int INSTANCE_COLOR_ATTR = 5; static constexpr int INSTANCE_COLOR_ATTR = 5;
std::vector<Matrix> transforms; std::vector<Matrix> transforms;
std::vector<Color> colors; std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault(); Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); 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; unsigned int color_vbo_id = 0;
public: public:
// TODO: I am allocating HUGE vertex buffers instead of resizing dynamically... renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input,
// Edges: 5'000'000 * 2 * 12 Byte ~= 115 MB user_interface& _gui)
// 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) : 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_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib( instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
instancing_shader, GetShaderLocationAttrib(instancing_shader, "instanceTransform");
"instanceTransform"); instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] =
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos"); 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; vertex_mat.shader = instancing_shader;
transforms.reserve(DRAW_VERTICES_LIMIT); transforms.reserve(DRAW_VERTICES_LIMIT);
colors.reserve(DRAW_VERTICES_LIMIT); colors.reserve(DRAW_VERTICES_LIMIT);
color_vbo_id = rlLoadVertexBuffer(nullptr, DRAW_VERTICES_LIMIT * sizeof(Color), true); color_vbo_id = rlLoadVertexBuffer(colors.data(), DRAW_VERTICES_LIMIT * sizeof(Color), true);
rlEnableVertexArray(cube_instance.vaoId); rlEnableVertexArray(cube_instance.vaoId);
rlEnableVertexBuffer(color_vbo_id); rlEnableVertexBuffer(color_vbo_id);
@ -92,7 +74,10 @@ public:
rlDisableVertexArray(); rlDisableVertexArray();
} }
NO_COPY_NO_MOVE(renderer); renderer(const renderer& copy) = delete;
auto operator=(const renderer& copy) -> renderer& = delete;
renderer(renderer&& move) = delete;
auto operator=(renderer&& move) -> renderer& = delete;
~renderer() ~renderer()
{ {
@ -100,19 +85,12 @@ public:
UnloadRenderTexture(klotski_target); UnloadRenderTexture(klotski_target);
UnloadRenderTexture(menu_target); UnloadRenderTexture(menu_target);
// Edges
rlUnloadVertexArray(edge_vao_id);
rlUnloadVertexBuffer(edge_vbo_id);
UnloadShader(edge_shader);
// Instancing // Instancing
UnloadMaterial(vertex_mat); UnloadMaterial(vertex_mat);
UnloadMesh(cube_instance); UnloadMesh(cube_instance);
// I think the shader already gets unloaded with the material? // I think the shader already gets unloaded with the material?
// UnloadShader(instancing_shader); // UnloadShader(instancing_shader);
rlUnloadVertexBuffer(color_vbo_id);
} }
private: private:
@ -124,7 +102,8 @@ private:
auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void; auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
public: 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 #endif

View File

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

View File

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

View File

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

View File

@ -1,67 +1,13 @@
#ifndef UTIL_HPP_ #ifndef UTIL_HPP_
#define UTIL_HPP_ #define UTIL_HPP_
#include <vector>
#include <iostream> #include <iostream>
#include <raylib.h> #include <raylib.h>
#define INLINE __attribute__((always_inline)) #define INLINE __attribute__((always_inline))
#define PACKED __attribute__((packed)) #define PACKED __attribute__((packed))
#define STARTTIME const auto start = std::chrono::high_resolution_clock::now() enum ctrl
#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, reset = 0,
bold_bright = 1, bold_bright = 1,
@ -72,40 +18,120 @@ enum class ctrl : u8
inverse_off = 27 inverse_off = 27
}; };
enum class fg : u8 enum fg
{ {
black = 30, fg_black = 30,
red = 31, fg_red = 31,
green = 32, fg_green = 32,
yellow = 33, fg_yellow = 33,
blue = 34, fg_blue = 34,
magenta = 35, fg_magenta = 35,
cyan = 36, fg_cyan = 36,
white = 37 fg_white = 37
}; };
enum class bg : u8 enum bg
{ {
black = 40, bg_black = 40,
red = 41, bg_red = 41,
green = 42, bg_green = 42,
yellow = 43, bg_yellow = 43,
blue = 44, bg_blue = 44,
magenta = 45, bg_magenta = 45,
cyan = 46, bg_cyan = 46,
white = 47 bg_white = 47
}; };
inline auto ansi_bold_fg(const fg color) -> std::string inline auto ansi_bold_fg(const fg color) -> std::string
{ {
return std::format("\033[{};{}m", static_cast<int>(ctrl::bold_bright), static_cast<int>(color)); return std::format("\033[{};{}m", static_cast<int>(bold_bright), static_cast<int>(color));
} }
inline auto ansi_reset() -> std::string inline auto ansi_reset() -> std::string
{ {
return std::format("\033[{}m", static_cast<int>(ctrl::reset)); return std::format("\033[{}m", static_cast<int>(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 // Output
inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream&
@ -124,33 +150,44 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
template <typename... Args> template <typename... Args>
auto traceln(std::format_string<Args...> fmt, Args&&... args) -> void auto traceln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}TRACE{}]: ", ansi_bold_fg(fg::cyan), ansi_reset()) << std::format( std::cout << std::format("[{}TRACE{}]: ", ansi_bold_fg(fg_cyan), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
template <typename... Args> template <typename... Args>
auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg::blue), ansi_reset()) << std::format( std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
template <typename... Args> template <typename... Args>
auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg::yellow), ansi_reset()) << std::format( std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
std::forward<Args>(args)...) << std::endl;
} }
template <typename... Args> template <typename... Args>
auto errln(std::format_string<Args...> fmt, Args&&... args) -> void auto errln(std::format_string<Args...> fmt, Args&&... args) -> void
{ {
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg::red), ansi_reset()) << std::format( std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) << std::format(
fmt, fmt, std::forward<Args>(args)...) << std::endl;
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, '='));
} }
#endif #endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

After

Width:  |  Height:  |  Size: 326 KiB

View File

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

View File

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

View File

@ -1,15 +0,0 @@
#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

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

View File

@ -18,7 +18,6 @@ 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::add_block);
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_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::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::camera_start_rotate);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block);
@ -43,7 +42,6 @@ auto input_handler::init_handlers() -> void
register_key_pressed_handler(KEY_C, &input_handler::clear_graph); 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_I, &input_handler::toggle_mark_solutions);
register_key_pressed_handler(KEY_O, &input_handler::toggle_connect_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_TAB, &input_handler::toggle_editing);
register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement); register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement);
@ -144,17 +142,12 @@ auto input_handler::camera_zoom() const -> void
auto input_handler::camera_fov() const -> void auto input_handler::camera_fov() const -> void
{ {
if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL)) { if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_LEFT_SHIFT)) {
return; return;
} }
const float wheel = GetMouseWheelMove(); 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 auto input_handler::select_block() -> void
@ -208,7 +201,7 @@ auto input_handler::add_block() -> void
has_block_add_xy = false; has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) { } 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( const std::optional<puzzle>& next = current.try_add_block(
block(block_add_x, block_add_y, block_add_width, block_add_height, false)); puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) { if (next) {
sel_x = block_add_x; sel_x = block_add_x;
@ -224,7 +217,7 @@ auto input_handler::add_block() -> void
auto input_handler::remove_block() -> void auto input_handler::remove_block() -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
const std::optional<block>& b = current.try_get_block(hov_x, hov_y); const std::optional<puzzle::block>& b = current.try_get_block(hov_x, hov_y);
if (!editing || has_block_add_xy || !b) { if (!editing || has_block_add_xy || !b) {
return; return;
} }
@ -246,7 +239,7 @@ auto input_handler::remove_block() -> void
auto input_handler::place_goal() const -> void auto input_handler::place_goal() const -> void
{ {
const puzzle& current = state.get_current_state(); const puzzle& current = state.get_current_state();
if (!editing || !mouse_in_board_pane() || !current.covers(hov_x, hov_y)) { if (!editing || !current.covers(hov_x, hov_y)) {
return; return;
} }
@ -258,16 +251,6 @@ auto input_handler::place_goal() const -> void
state.edit_starting_state(*next); 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 auto input_handler::toggle_camera_lock() -> void
{ {
if (!camera_lock) { if (!camera_lock) {
@ -424,11 +407,6 @@ auto input_handler::toggle_connect_solutions() -> void
connect_solutions = !connect_solutions; 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 auto input_handler::toggle_mark_path() -> void
{ {
mark_path = !mark_path; mark_path = !mark_path;

View File

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

View File

@ -1,50 +1,26 @@
#include <chrono>
#include <mutex>
#include <raylib.h>
#include "config.hpp" #include "config.hpp"
#include "input_handler.hpp" #include "input_handler.hpp"
#include "cpu_layout_engine.hpp" #include "threaded_physics.hpp"
#include "renderer.hpp" #include "renderer.hpp"
#include "state_manager.hpp" #include "state_manager.hpp"
#include "user_interface.hpp" #include "user_interface.hpp"
#include <chrono>
#include <mutex>
#include <GL/glew.h>
#include <raylib.h>
#include <filesystem> #include <filesystem>
#if not defined(_WIN32) #ifndef WIN32
#include <boost/program_options.hpp> #include <boost/program_options.hpp>
namespace po = boost::program_options; namespace po = boost::program_options;
#endif #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: Export cluster to graphviz
// TODO: Fix naming: // TODO: Fix naming:
// - Target: The block that has to leave the board to win // - Target: The block that has to leave the board to win
@ -57,6 +33,29 @@ std::tuple<u8, u8, u8, u8> target_block_pos_range;
// TODO: Add state space generation time to debug overlay // 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: 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 auto ui_mode() -> int
{ {
// RayLib window setup // RayLib window setup
@ -67,15 +66,8 @@ auto ui_mode() -> int
SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN);
InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); 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 // Game setup
cpu_layout_engine physics(thread_pool); threaded_physics physics(thread_pool);
state_manager state(physics, preset_file); state_manager state(physics, preset_file);
orbit_camera camera; orbit_camera camera;
input_handler input(state, camera); input_handler input(state, camera);
@ -169,87 +161,51 @@ auto ui_mode() -> int
return 0; return 0;
} }
auto rush_hour_puzzle_space() -> void auto rush_hour_puzzle_space() -> int
{ {
board_width = 6; const boost::unordered_flat_set<puzzle::block, block_hasher2, block_equal2> permitted_blocks = {
board_height = 6; puzzle::block(0, 0, 2, 1, false, false),
goal_x = 4; puzzle::block(0, 0, 3, 1, false, false),
goal_y = 2; puzzle::block(0, 0, 1, 2, false, false),
restricted = true; puzzle::block(0, 0, 1, 3, false, false)
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)
}; };
target_block = block(0, 0, 2, 1, true, false); const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false);
target_block_pos_range = {0, goal_y, board_width - target_block.get_width(), goal_y}; const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> target_block_pos_range = {0, goal_y, board_width - 1, goal_y};
}
auto klotski_puzzle_space() -> void infoln("Exploring Rush-Hour puzzle space:");
{
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("- Size: {}x{}", board_width, board_height);
infoln("- Goal: {},{}", goal_x, goal_y); infoln("- Goal: {},{}", goal_x, goal_y);
infoln("- Restricted: {}", restricted); infoln("- Restricted: {}", restricted);
infoln("- Max Blocks: {}", max_blocks); infoln("- Max Blocks: {}", max_blocks);
infoln("- Min Moves: {}", min_moves);
infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height()); infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height());
infoln("- Max Sets: {}", upper_set_count);
infoln("- Permitted block sizes:"); infoln("- Permitted block sizes:");
std::cout << " "; std::cout << " ";
for (const block b : permitted_blocks) { for (const puzzle::block b : permitted_blocks) {
std::cout << std::format(" {}x{},", b.get_width(), b.get_height()); std::cout << std::format(" {}x{},", b.get_width(), b.get_height());
} }
std::cout << std::endl; 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 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, permitted_blocks,
target_block, target_block,
target_block_pos_range, target_block_pos_range,
max_blocks, max_blocks,
min_moves,
thread_pool); thread_pool);
ENDTIME(std::format("Found {} different clusters", result.size()), std::chrono::seconds, "s");
// TODO: The exported clusters are the numerically smallest state of the cluster. const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
// Not the state with the longest path. infoln("Found {} different clusters. Took {}s.",
result.size(),
std::chrono::duration_cast<std::chrono::seconds>(end - start).count());
infoln("Sorting clusters..."); infoln("Sorting clusters...");
std::vector<puzzle> result_sorted{result.begin(), result.end()}; std::vector<puzzle> result_sorted{result.begin(), result.end()};
std::ranges::sort(result_sorted, std::ranges::greater{}); 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 i = 0;
size_t success = 0; size_t success = 0;
std::filesystem::remove(output_file); std::filesystem::remove(output_file);
@ -270,32 +226,26 @@ auto puzzle_space() -> int
enum class runmode enum class runmode
{ {
USER_INTERFACE, USER_INTERFACE, RUSH_HOUR_PUZZLE_SPACE, EXIT,
RUSH_HOUR_PUZZLE_SPACE,
KLOTSKI_PUZZLE_SPACE,
EXIT,
}; };
auto argparse(const int argc, char* argv[]) -> runmode auto argparse(const int argc, char* argv[]) -> runmode
{ {
#if not defined(_WIN32) #ifndef WIN32
po::options_description desc("Allowed options"); po::options_description desc("Allowed options");
desc.add_options() // desc.add_options() //
("help", "produce help message") // ("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") // ("presets", po::value<std::string>()->default_value("default.puzzle"), "load presets from file") //
("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") // ("output", po::value<std::string>()->default_value("clusters.puzzle"), "output file for generated clusters") //
// ("h", po::value<int>()->default_value(board_height)->value_name("[3, 8"), "board height") // ("space", po::value<std::string>()->value_name("rh|klotski"), "generate puzzle space with ruleset") //
// ("gx", po::value<int>()->default_value(goal_x)->value_name("[0, w-1]"), "board goal horizontal position") // ("w", po::value<int>()->default_value(6), "board width") //
// ("gy", po::value<int>()->default_value(goal_y)->value_name("[0, h-1]"), "board goal vertical position") // ("h", po::value<int>()->default_value(6), "board height") //
// ("free", "allow free block movement") // ("gx", po::value<int>()->default_value(4), "board goal horizontal position") //
("blocks", ("gy", po::value<int>()->default_value(2), "board goal vertical position") //
po::value<int>()->default_value(max_blocks)->value_name("[1, 15]"), ("free", "allow free block movement") //
"block limit for puzzle space generation") // ("blocks", po::value<int>()->default_value(5), "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; po::positional_options_description positional;
@ -309,53 +259,56 @@ auto argparse(const int argc, char* argv[]) -> runmode
std::cout << desc << std::endl; std::cout << desc << std::endl;
return runmode::EXIT; return runmode::EXIT;
} }
if (vm.contains("output")) { if (vm.contains("output")) {
output_file = vm["output"].as<std::string>(); output_file = vm["output"].as<std::string>();
} }
// if (vm.contains("w")) { if (vm.contains("w")) {
// board_width = vm["w"].as<int>(); board_width = vm["w"].as<int>();
// board_width = std::max(static_cast<int>(puzzle::MIN_WIDTH), board_width = std::max(static_cast<int>(puzzle::MIN_WIDTH),
// std::min(board_width, static_cast<int>(puzzle::MAX_WIDTH))); std::min(board_width, static_cast<int>(puzzle::MAX_WIDTH)));
// } }
// if (vm.contains("h")) {
// board_height = vm["h"].as<int>(); if (vm.contains("h")) {
// board_height = std::max(static_cast<int>(puzzle::MIN_HEIGHT), board_height = vm["h"].as<int>();
// std::min(board_height, static_cast<int>(puzzle::MAX_HEIGHT))); 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("gx")) {
// } goal_x = vm["gx"].as<int>();
// if (vm.contains("gy")) { goal_x = std::max(0, std::min(goal_x, static_cast<int>(puzzle::MAX_WIDTH) - 1));
// 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("gy")) {
// if (vm.contains("free")) { goal_y = vm["gy"].as<int>();
// restricted = false; 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")) { if (vm.contains("blocks")) {
max_blocks = vm["blocks"].as<int>(); max_blocks = vm["blocks"].as<int>();
max_blocks = std::max(1, std::min(max_blocks, static_cast<int>(puzzle::MAX_BLOCKS))); 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")) { if (vm.contains("space")) {
const std::string ruleset = vm["space"].as<std::string>(); const std::string ruleset = vm["space"].as<std::string>();
if (ruleset == "rh") { if (ruleset == "rh") {
return runmode::RUSH_HOUR_PUZZLE_SPACE; return runmode::RUSH_HOUR_PUZZLE_SPACE;
} }
if (ruleset == "klotski") { if (ruleset == "klotski") {
return runmode::KLOTSKI_PUZZLE_SPACE; throw std::runtime_error("Not implemented");
} }
} }
if (vm.contains("presets")) { if (vm.contains("presets")) {
preset_file = vm["presets"].as<std::string>(); preset_file = vm["presets"].as<std::string>();
} }
#endif #endif
return runmode::USER_INTERFACE; return runmode::USER_INTERFACE;
} }
@ -375,13 +328,7 @@ auto main(const int argc, char* argv[]) -> int
#endif #endif
infoln("Using background thread for physics."); infoln("Using background thread for physics.");
infoln("Using linear octree + Barnes-Hut for graph layout."); infoln("Using octree-barnes-hut for graph layout.");
#ifdef ASYNC_OCTREE
infoln("Using asynchronous octree build.");
#else
infoln("Using synchronous octree build.");
#endif
#ifdef THREADPOOL #ifdef THREADPOOL
infoln("Additional thread-pool enabled ({} threads).", threads.get_thread_count()); infoln("Additional thread-pool enabled ({} threads).", threads.get_thread_count());
@ -393,14 +340,10 @@ auto main(const int argc, char* argv[]) -> int
case runmode::USER_INTERFACE: case runmode::USER_INTERFACE:
return ui_mode(); return ui_mode();
case runmode::RUSH_HOUR_PUZZLE_SPACE: case runmode::RUSH_HOUR_PUZZLE_SPACE:
rush_hour_puzzle_space(); return rush_hour_puzzle_space();
return puzzle_space();
case runmode::KLOTSKI_PUZZLE_SPACE:
klotski_puzzle_space();
return puzzle_space();
case runmode::EXIT: case runmode::EXIT:
return 0; return 0;
}; };
return 1; return 1;
} }

View File

@ -1,20 +1,69 @@
#include "cpu_spring_system.hpp" #include "mass_spring_system.hpp"
#include "config.hpp" #include "config.hpp"
#include <cfloat> #include <cfloat>
#include <cstring> #include <cstring>
auto cpu_spring_system::clear() -> void 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
{ {
positions.clear(); positions.clear();
previous_positions.clear(); previous_positions.clear();
velocities.clear(); velocities.clear();
forces.clear(); forces.clear();
springs.clear(); springs.clear();
tree.clear(); tree.nodes.clear();
} }
auto cpu_spring_system::add_mass() -> void auto mass_spring_system::add_mass() -> void
{ {
// Adding all positions to (0, 0, 0) breaks the octree // Adding all positions to (0, 0, 0) breaks the octree
@ -31,24 +80,21 @@ auto cpu_spring_system::add_mass() -> void
forces.emplace_back(Vector3Zero()); forces.emplace_back(Vector3Zero());
} }
auto cpu_spring_system::add_spring(size_t a, size_t b) -> void auto mass_spring_system::add_spring(size_t a, size_t b) -> void
{ {
// Update masses to be located along a random walk when adding the springs // Update masses to be located along a random walk when adding the springs
const Vector3& mass_a = positions[a]; const Vector3& mass_a = positions[a];
const Vector3& mass_b = positions[b]; const Vector3& mass_b = positions[b];
Vector3 offset{ Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100))};
static_cast<float>(GetRandomValue(-100, 100)) offset = Vector3Normalize(offset) * REST_LENGTH;
};
// 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 the offset moves the mass closer to the current center of mass, flip it
if (!tree.empty()) { if (!tree.nodes.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(positions[a], tree.root().mass_center); const Vector3 mass_center_direction =
Vector3Subtract(positions[a], tree.nodes[0].mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction); const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) { if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
@ -66,53 +112,28 @@ auto cpu_spring_system::add_spring(size_t a, size_t b) -> void
springs.emplace_back(a, b); springs.emplace_back(a, b);
} }
auto cpu_spring_system::clear_forces() -> void auto mass_spring_system::clear_forces() -> void
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
memset(forces.data(), 0, forces.size() * sizeof(Vector3)); memset(forces.data(), 0, forces.size() * sizeof(Vector3));
} }
auto cpu_spring_system::calculate_spring_force(const size_t s) -> void auto mass_spring_system::calculate_spring_forces(
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
{ {
const spring _s = springs[s]; #ifdef TRACY
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 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; ZoneScoped;
#endif #endif
const auto solve_spring_force = [&](const int i) const auto solve_spring_force = [&](const int i) { calculate_spring_force(i); };
{
calculate_spring_force(i);
};
if (thread_pool) { 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 { } else {
for (size_t i = 0; i < springs.size(); ++i) { for (size_t i = 0; i < springs.size(); ++i) {
solve_spring_force(i); solve_spring_force(i);
@ -120,21 +141,24 @@ auto cpu_spring_system::calculate_spring_forces(const threadpool thread_pool) ->
} }
} }
auto cpu_spring_system::calculate_repulsion_forces(const threadpool thread_pool) -> void auto mass_spring_system::calculate_repulsion_forces(
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
const auto solve_octree = [&](const int i) const auto solve_octree = [&](const int i)
{ {
const Vector3 force = tree.calculate_force_morton(0, positions[i], i); const Vector3 force = tree.calculate_force(0, positions[i]);
forces[i] += force; forces[i] += force;
}; };
// Calculate forces using Barnes-Hut // Calculate forces using Barnes-Hut
if (thread_pool) { 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 { } else {
for (size_t i = 0; i < positions.size(); ++i) { for (size_t i = 0; i < positions.size(); ++i) {
solve_octree(i); solve_octree(i);
@ -142,40 +166,14 @@ auto cpu_spring_system::calculate_repulsion_forces(const threadpool thread_pool)
} }
} }
auto cpu_spring_system::integrate_velocity(const size_t m, const float dt) -> void auto mass_spring_system::update(const float dt,
const std::optional<BS::thread_pool<>* const> thread_pool) -> void
{ {
const Vector3 acc = forces[m] / MASS; #ifdef TRACY
velocities[m] += acc * 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; ZoneScoped;
#endif #endif
const auto update = [&](const int i) const auto update = [&](const int i) { verlet_update(i, dt); };
{
verlet_update(i, dt);
};
if (thread_pool) { if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait(); (*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait();
@ -186,7 +184,8 @@ auto cpu_spring_system::update(const float dt, const threadpool thread_pool) ->
} }
} }
auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void auto mass_spring_system::center_masses(const std::optional<BS::thread_pool<>* const> thread_pool)
-> void
{ {
Vector3 mean = Vector3Zero(); Vector3 mean = Vector3Zero();
for (const Vector3& pos : positions) { for (const Vector3& pos : positions) {
@ -194,10 +193,7 @@ auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void
} }
mean /= static_cast<float>(positions.size()); mean /= static_cast<float>(positions.size());
const auto center_mass = [&](const int i) const auto center_mass = [&](const int i) { positions[i] -= mean; };
{
positions[i] -= mean;
};
if (thread_pool) { if (thread_pool) {
(*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait(); (*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait();
@ -206,4 +202,4 @@ auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void
center_mass(i); center_mass(i);
} }
} }
} }

View File

@ -4,332 +4,224 @@
#include <cfloat> #include <cfloat>
#include <raymath.h> #include <raymath.h>
auto octree::clear() -> void auto octree::node::child_count() const -> int
{ {
nodes.clear(); int child_count = 0;
for (const int child : children) {
if (child != -1) {
++child_count;
}
}
return child_count;
} }
auto octree::reserve(const size_t count) -> void auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
{ {
nodes.reserve(count); 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;
} }
auto octree::empty() const -> bool auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3>
{ {
return nodes.empty(); 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);
} }
auto octree::root() const -> const node& auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int
{ {
return nodes[0]; node n;
n.box_min = box_min;
n.box_max = box_max;
nodes.emplace_back(n);
return static_cast<int>(nodes.size() - 1);
} }
// Replaced the 50 line recursive octree insertion with this morton bitch to gain 5 UPS, FML auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
auto octree::build_octree_morton(octree& t, const int depth) -> void
const std::vector<Vector3>& positions, {
const std::optional<BS::thread_pool<>*>& thread_pool) -> 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
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;
#endif #endif
t.clear(); t.nodes.clear();
if (positions.empty()) { t.nodes.reserve(positions.size() * 2);
return;
}
// Compute bounding box around all masses // Compute bounding box around all masses
Vector3 root_min{FLT_MAX, FLT_MAX, FLT_MAX}; Vector3 min{FLT_MAX, FLT_MAX, FLT_MAX};
Vector3 root_max{-FLT_MAX, -FLT_MAX, -FLT_MAX}; Vector3 max{-FLT_MAX, -FLT_MAX, -FLT_MAX};
for (const auto& [x, y, z] : positions) { for (const auto& [x, y, z] : positions) {
root_min.x = std::min(root_min.x, x); min.x = std::min(min.x, x);
root_max.x = std::max(root_max.x, x); max.x = std::max(max.x, x);
root_min.y = std::min(root_min.y, y); min.y = std::min(min.y, y);
root_max.y = std::max(root_max.y, y); max.y = std::max(max.y, y);
root_min.z = std::min(root_min.z, z); min.z = std::min(min.z, z);
root_max.z = std::max(root_max.z, z); max.z = std::max(max.z, z);
} }
constexpr float pad = 1.0f; // Pad the bounding box
root_min = Vector3Subtract(root_min, Vector3Scale(Vector3One(), pad)); constexpr float pad = 1.0;
root_max = Vector3Add(root_max, Vector3Scale(Vector3One(), pad)); min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad));
max = Vector3Add(max, Vector3Scale(Vector3One(), pad));
const float max_extent = std::max({root_max.x - root_min.x, root_max.y - root_min.y, root_max.z - root_min.z}); // Make it cubic (so subdivisions are balanced)
root_max = Vector3Add(root_min, Vector3Scale(Vector3One(), max_extent)); 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 root_extent = root_max.x - root_min.x; // cubic // Root node spans the entire area
const int root = t.create_empty_leaf(min, max);
// Container for building the particle list before sorting by morton code for (size_t i = 0; i < positions.size(); ++i) {
struct sort_node t.insert(root, static_cast<int>(i), positions[i], MASS, 0);
{
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_morton(const int node_idx, const Vector3& pos, const int self_id) const -> Vector3 auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Vector3
{ {
if (node_idx < 0) { if (node_idx < 0) {
return Vector3Zero(); return Vector3Zero();
} }
// Force accumulator const node& n = nodes[node_idx];
float fx = 0.0f; if (std::abs(n.mass_total) <= 0.001f) {
float fy = 0.0f; return Vector3Zero();
float fz = 0.0f; }
std::vector<int> stack; const Vector3 diff = Vector3Subtract(pos, n.mass_center);
stack.reserve(512); float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
stack.push_back(node_idx);
constexpr float theta2 = THETA * THETA; // Softening
dist_sq += SOFTENING;
while (!stack.empty()) { // Barnes-Hut
const int idx = stack.back(); const float size = n.box_max.x - n.box_min.x;
stack.pop_back(); 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;
const node& n = nodes[idx]; return Vector3Scale(diff, force_mag / dist);
}
// No self-force for single-particle leafs // Collect child forces
if (n.leaf && n.mass_id == self_id) { Vector3 force = Vector3Zero();
continue; for (const int child : n.children) {
} if (child >= 0) {
const Vector3 child_force = calculate_force(child, pos);
const float dx = pos.x - n.mass_center.x; force = Vector3Add(force, child_force);
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 Vector3{fx, fy, fz}; return force;
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <rlgl.h> #include <rlgl.h>
#include <GL/glew.h>
auto renderer::update_texture_sizes() -> void auto renderer::update_texture_sizes() -> void
{ {
@ -35,24 +34,10 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
return; return;
} }
// Prepare edge buffer // Prepare connection batching
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(prepare_edge_buffers, "PrepareEdgeBuffers", true); ZoneNamedN(prepare_connections, "PrepareConnectionsBatching", 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 #endif
connections.clear(); connections.clear();
@ -62,6 +47,7 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
const Vector3& current_mass = masses[state.get_current_index()]; const Vector3& current_mass = masses[state.get_current_index()];
const Vector3& winning_mass = masses[_state]; const Vector3& winning_mass = masses[_state];
connections.emplace_back(current_mass, winning_mass); connections.emplace_back(current_mass, winning_mass);
DrawLine3D(current_mass, winning_mass, Fade(TARGET_BLOCK_COLOR, 0.5));
} }
} }
} }
@ -77,67 +63,6 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
transforms.clear(); transforms.clear();
colors.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; size_t mass = 0;
for (const auto& [x, y, z] : masses) { for (const auto& [x, y, z] : masses) {
transforms.emplace_back(MatrixTranslate(x, y, z)); transforms.emplace_back(MatrixTranslate(x, y, z));
@ -156,14 +81,9 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
} else if (state.get_visit_counts().at(mass) > 0) { } else if (state.get_visit_counts().at(mass) > 0) {
// Visited vertex // Visited vertex
c = VERTEX_VISITED_COLOR; 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); colors.emplace_back(c);
++mass; ++mass;
} }
@ -176,53 +96,23 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
BeginMode3D(camera.camera); BeginMode3D(camera.camera);
rlDrawRenderBatchActive(); // Draw springs (batched)
// Draw edges
{ {
#ifdef TRACY #ifdef TRACY
ZoneNamedN(draw_springs, "DrawSprings", true); ZoneNamedN(draw_springs, "DrawSprings", true);
#endif #endif
rlEnableShader(edge_shader.id); rlBegin(RL_LINES);
for (const auto& [from, to] : state.get_links()) {
Matrix modelview = rlGetMatrixModelview(); if (masses.size() > from && masses.size() > to) {
Matrix projection = rlGetMatrixProjection(); const auto& [ax, ay, az] = masses[from];
Matrix mvp = MatrixMultiply(modelview, projection); const auto& [bx, by, bz] = masses[to];
rlSetUniformMatrix(edge_shader.locs[SHADER_LOC_MATRIX_MVP], mvp); rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a);
rlVertex3f(ax, ay, az);
const std::array<float, 4> edge_color = { rlVertex3f(bx, by, bz);
EDGE_COLOR.r / 255.0f, }
EDGE_COLOR.g / 255.0f, }
EDGE_COLOR.b / 255.0f, rlEnd();
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) // Draw masses (instanced)
@ -291,47 +181,32 @@ auto renderer::draw_menu() const -> void
EndTextureMode(); EndTextureMode();
} }
auto renderer::draw_textures(const int fps, auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
const int ups,
const size_t mass_count,
const size_t spring_count) const -> void const size_t spring_count) const -> void
{ {
BeginDrawing(); BeginDrawing();
DrawTextureRec(menu_target.texture, DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height), Vector2(0, 0), WHITE);
Vector2(0, 0),
WHITE);
DrawTextureRec(klotski_target.texture, DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height), Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), Vector2(0, MENU_HEIGHT), WHITE);
WHITE); DrawTextureRec(graph_target.texture, Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height),
DrawTextureRec(graph_target.texture, Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height),
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT),
WHITE);
// Draw borders // Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK); DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK);
DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
1.0f,
BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f,
MENU_HEIGHT,
GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT),
1.0f,
BLACK); 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); gui.draw(fps, ups, mass_count, spring_count);
EndDrawing(); EndDrawing();
} }
auto renderer::render(const std::vector<Vector3>& masses, auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups, const size_t mass_count,
const int fps,
const int ups,
const size_t mass_count,
const size_t spring_count) -> void const size_t spring_count) -> void
{ {
update_texture_sizes(); 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, auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<spring>& _links) -> void const std::vector<std::pair<size_t, size_t>>& _links) -> void
{ {
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared"); 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(); return winning_path.size();
} }
auto state_manager::get_links() const -> const std::vector<spring>& auto state_manager::get_links() const -> const std::vector<std::pair<size_t, size_t>>&
{ {
return links; return links;
} }
@ -429,11 +429,6 @@ auto state_manager::has_distances() const -> bool
return !node_target_distances.empty(); 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 auto state_manager::get_total_moves() const -> size_t
{ {
return total_moves; return total_moves;

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp> #include <catch2/generators/catch_generators.hpp>
static auto board_mask(const int w, const int h) -> u64 static auto board_mask(const int w, const int h) -> uint64_t
{ {
const int cells = w * h; const int cells = w * h;
if (cells == 64) { if (cells == 64) {
@ -31,7 +31,7 @@ TEST_CASE("Full board detection respects width*height only", "[puzzle][board]")
puzzle p(w, h); puzzle p(w, h);
u64 mask = board_mask(w, h); uint64_t mask = board_mask(w, h);
int x = -1, y = -1; 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})); auto empty_index = GENERATE_COPY(values<int>({ 0, cells / 2, cells - 1}));
u64 bitmap = board_mask(w, h); uint64_t bitmap = board_mask(w, h);
bitmap &= ~(1ULL << empty_index); bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1; int x = -1, y = -1;
@ -65,10 +65,10 @@ TEST_CASE("Bits outside board are ignored", "[puzzle][board]")
{ {
puzzle p(3, 3); // 9 cells puzzle p(3, 3); // 9 cells
u64 mask = board_mask(3, 3); uint64_t mask = board_mask(3, 3);
// Board is full // Board is full
u64 bitmap = mask; uint64_t bitmap = mask;
// Set extra bits outside 9 cells // Set extra bits outside 9 cells
bitmap |= (1ULL << 20); bitmap |= (1ULL << 20);
@ -83,7 +83,7 @@ TEST_CASE("First empty found in forward search branch", "[puzzle][branch]")
puzzle p(4, 4); // 16 cells puzzle p(4, 4); // 16 cells
// Only MSB (within board) set // Only MSB (within board) set
u64 bitmap = (1ULL << 15); uint64_t bitmap = (1ULL << 15);
int x = -1, y = -1; int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); 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 puzzle p(4, 4); // 16 cells
// Set bits 15,14,13 but leave 12 empty // Set bits 15,14,13 but leave 12 empty
u64 bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13); uint64_t bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13);
int x = -1, y = -1; int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); 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; int empty_index = 11;
u64 bitmap = board_mask(5, 3); uint64_t bitmap = board_mask(5, 3);
bitmap &= ~(1ULL << empty_index); bitmap &= ~(1ULL << empty_index);
int x = -1, y = -1; 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 puzzle p(7, 8); // 56 cells
u64 mask = board_mask(7, 8); uint64_t mask = board_mask(7, 8);
// Full board should return false // Full board should return false
int x = -1, y = -1; 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 --- // --- Oracle: find first zero bit inside board ---
static auto oracle_find_first_empty(u64 bitmap, int w, int h, int& x, int& y) -> bool static auto oracle_find_first_empty(uint64_t bitmap, int w, int h, int& x, int& y) -> bool
{ {
int cells = w * h; int cells = w * h;
@ -164,13 +164,13 @@ TEST_CASE("Oracle validation across board sizes 3x3 to 8x8", "[puzzle][oracle]")
puzzle p(w, h); puzzle p(w, h);
u64 mask = board_mask(w, h); uint64_t mask = board_mask(w, h);
std::mt19937_64 rng(12345); std::mt19937_64 rng(12345);
std::uniform_int_distribution<u64> dist(0, UINT64_MAX); std::uniform_int_distribution<uint64_t> dist(0, UINT64_MAX);
for (int iteration = 0; iteration < 200; ++iteration) { for (int iteration = 0; iteration < 200; ++iteration) {
u64 bitmap = dist(rng); uint64_t bitmap = dist(rng);
int ox = -1, oy = -1; int ox = -1, oy = -1;
bool oracle_result = oracle_find_first_empty(bitmap, w, h, ox, oy); 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 puzzle p(3, 3); // 9 cells
u64 bitmap = (1ULL << 40) | (1ULL << 63); uint64_t bitmap = (1ULL << 40) | (1ULL << 63);
int x = -1, y = -1; int x = -1, y = -1;
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); 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); puzzle p(w, h);
int cells = w * h; int cells = w * h;
u64 bitmap = board_mask(w, h); uint64_t bitmap = board_mask(w, h);
// Clear last valid bit // Clear last valid bit
bitmap &= ~(1ULL << (cells - 1)); 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}) { for (int index : {31, 32, cells - 2}) {
if (index >= cells) continue; if (index >= cells) continue;
u64 bitmap = board_mask(w, h); uint64_t bitmap = board_mask(w, h);
bitmap &= ~(1ULL << index); bitmap &= ~(1ULL << index);
int x = -1, y = -1; int x = -1, y = -1;
@ -250,7 +250,7 @@ TEST_CASE("Multiple holes choose lowest index", "[puzzle][multiple]")
{ {
puzzle p(5, 5); puzzle p(5, 5);
u64 bitmap = board_mask(5, 5); uint64_t bitmap = board_mask(5, 5);
// Clear several positions // Clear several positions
bitmap &= ~(1ULL << 3); bitmap &= ~(1ULL << 3);

View File

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

View File

@ -12,7 +12,7 @@
TEST_CASE("Block creation and field access", "[block]") TEST_CASE("Block creation and field access", "[block]")
{ {
block b(1, 2, 3, 4, true, false); puzzle::block b(1, 2, 3, 4, true, false);
CHECK(b.get_x() == 1); CHECK(b.get_x() == 1);
CHECK(b.get_y() == 2); CHECK(b.get_y() == 2);
@ -25,16 +25,16 @@ TEST_CASE("Block creation and field access", "[block]")
TEST_CASE("Block invalid by default", "[block]") TEST_CASE("Block invalid by default", "[block]")
{ {
block b; puzzle::block b;
CHECK_FALSE(b.valid()); CHECK_FALSE(b.valid());
} }
TEST_CASE("Block comparison ordering", "[block]") TEST_CASE("Block comparison ordering", "[block]")
{ {
// Row-major: (0,0) < (1,0) < (0,1) // Row-major: (0,0) < (1,0) < (0,1)
block a(0, 0, 1, 1); puzzle::block a(0, 0, 1, 1);
block b(1, 0, 1, 1); puzzle::block b(1, 0, 1, 1);
block c(0, 1, 1, 1); puzzle::block c(0, 1, 1, 1);
CHECK(a < b); CHECK(a < b);
CHECK(b < c); CHECK(b < c);
@ -43,7 +43,7 @@ TEST_CASE("Block comparison ordering", "[block]")
TEST_CASE("Block principal_dirs for horizontal block", "[block]") TEST_CASE("Block principal_dirs for horizontal block", "[block]")
{ {
block b(0, 0, 3, 1); // wider than tall puzzle::block b(0, 0, 3, 1); // wider than tall
CHECK((b.principal_dirs() & eas)); CHECK((b.principal_dirs() & eas));
CHECK((b.principal_dirs() & wes)); CHECK((b.principal_dirs() & wes));
CHECK_FALSE((b.principal_dirs() & nor)); 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]") TEST_CASE("Block principal_dirs for vertical block", "[block]")
{ {
block b(0, 0, 1, 3); // taller than wide puzzle::block b(0, 0, 1, 3); // taller than wide
CHECK((b.principal_dirs() & nor)); CHECK((b.principal_dirs() & nor));
CHECK((b.principal_dirs() & sou)); CHECK((b.principal_dirs() & sou));
CHECK_FALSE((b.principal_dirs() & eas)); 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]") TEST_CASE("Block principal_dirs for square block", "[block]")
{ {
block b(0, 0, 2, 2); puzzle::block b(0, 0, 2, 2);
CHECK((b.principal_dirs() & nor)); CHECK((b.principal_dirs() & nor));
CHECK((b.principal_dirs() & sou)); CHECK((b.principal_dirs() & sou));
CHECK((b.principal_dirs() & eas)); CHECK((b.principal_dirs() & eas));
@ -70,7 +70,7 @@ TEST_CASE("Block principal_dirs for square block", "[block]")
TEST_CASE("Block covers", "[block]") TEST_CASE("Block covers", "[block]")
{ {
block b(1, 2, 3, 2); puzzle::block b(1, 2, 3, 2);
// Covers (1,2) to (3,3) // Covers (1,2) to (3,3)
CHECK(b.covers(1, 2)); CHECK(b.covers(1, 2));
CHECK(b.covers(3, 3)); CHECK(b.covers(3, 3));
@ -83,10 +83,10 @@ TEST_CASE("Block covers", "[block]")
TEST_CASE("Block collides", "[block]") TEST_CASE("Block collides", "[block]")
{ {
block a(0, 0, 2, 2); puzzle::block a(0, 0, 2, 2);
block b(1, 1, 2, 2); puzzle::block b(1, 1, 2, 2);
block c(2, 2, 1, 1); puzzle::block c(2, 2, 1, 1);
block d(3, 0, 1, 1); puzzle::block d(3, 0, 1, 1);
CHECK(a.collides(b)); CHECK(a.collides(b));
CHECK(b.collides(a)); CHECK(b.collides(a));
@ -114,7 +114,7 @@ TEST_CASE("Puzzle creation and meta access", "[puzzle]")
TEST_CASE("Puzzle add and remove block", "[puzzle]") TEST_CASE("Puzzle add and remove block", "[puzzle]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
block b(0, 0, 2, 1); puzzle::block b(0, 0, 2, 1);
auto p2 = p.try_add_block(b); auto p2 = p.try_add_block(b);
REQUIRE(p2.has_value()); 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]") TEST_CASE("Puzzle meta roundtrip for all valid sizes", "[puzzle]")
{ {
for (u8 w = 3; w <= 8; ++w) { for (uint8_t w = 3; w <= 8; ++w) {
for (u8 h = 3; h <= 8; ++h) { for (uint8_t h = 3; h <= 8; ++h) {
puzzle p(w, h, 0, 0, true, false); puzzle p(w, h, 0, 0, true, false);
CHECK(p.get_width() == w); CHECK(p.get_width() == w);
CHECK(p.get_height() == h); CHECK(p.get_height() == h);
@ -151,11 +151,11 @@ TEST_CASE("Puzzle block_count", "[puzzle]")
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
CHECK(p.block_count() == 0); CHECK(p.block_count() == 0);
auto p2 = p.try_add_block(block(0, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
CHECK(p2->block_count() == 1); CHECK(p2->block_count() == 1);
auto p3 = p2->try_add_block(block(2, 2, 1, 1)); auto p3 = p2->try_add_block(puzzle::block(2, 2, 1, 1));
REQUIRE(p3); REQUIRE(p3);
CHECK(p3->block_count() == 2); CHECK(p3->block_count() == 2);
} }
@ -163,18 +163,18 @@ TEST_CASE("Puzzle block_count", "[puzzle]")
TEST_CASE("Puzzle cannot add overlapping block", "[puzzle]") TEST_CASE("Puzzle cannot add overlapping block", "[puzzle]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(block(0, 0, 2, 2)); auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 2));
REQUIRE(p2); REQUIRE(p2);
// Overlapping // Overlapping
auto p3 = p2->try_add_block(block(1, 1, 2, 2)); auto p3 = p2->try_add_block(puzzle::block(1, 1, 2, 2));
CHECK_FALSE(p3.has_value()); CHECK_FALSE(p3.has_value());
} }
TEST_CASE("Puzzle cannot add block outside board", "[puzzle]") TEST_CASE("Puzzle cannot add block outside board", "[puzzle]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(block(3, 3, 2, 2)); auto p2 = p.try_add_block(puzzle::block(3, 3, 2, 2));
CHECK_FALSE(p2.has_value()); 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]") TEST_CASE("bitmap_set_bit and bitmap_get_bit roundtrip", "[bitmap]")
{ {
for (u8 w = 3; w <= 8; ++w) { for (uint8_t w = 3; w <= 8; ++w) {
u64 bm = 0; uint64_t bm = 0;
// Set every cell on a w x w board // Set every cell on a w x w board
for (u8 y = 0; y < w; ++y) { for (uint8_t y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) { for (uint8_t x = 0; x < w; ++x) {
puzzle::bitmap_set_bit(bm, w, x, y); puzzle::bitmap_set_bit(bm, w, x, y);
} }
} }
// Verify all set // Verify all set
for (u8 y = 0; y < w; ++y) { for (uint8_t y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) { for (uint8_t x = 0; x < w; ++x) {
CHECK(puzzle::bitmap_get_bit(bm, w, x, y)); 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]") TEST_CASE("bitmap_set_bit sets only the target bit", "[bitmap]")
{ {
for (u8 w = 3; w <= 8; ++w) { for (uint8_t w = 3; w <= 8; ++w) {
for (u8 y = 0; y < w; ++y) { for (uint8_t y = 0; y < w; ++y) {
for (u8 x = 0; x < w; ++x) { for (uint8_t x = 0; x < w; ++x) {
u64 bm = 0; uint64_t bm = 0;
puzzle::bitmap_set_bit(bm, w, x, y); puzzle::bitmap_set_bit(bm, w, x, y);
CHECK(bm == (u64(1) << (y * w + x))); CHECK(bm == (uint64_t(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]") TEST_CASE("bitmap_clear_bit clears only the target bit", "[bitmap]")
{ {
u8 w = 6; uint8_t w = 6;
u64 bm = 0; uint64_t bm = 0;
puzzle::bitmap_set_bit(bm, w, 2, 3); puzzle::bitmap_set_bit(bm, w, 2, 3);
puzzle::bitmap_set_bit(bm, w, 4, 1); puzzle::bitmap_set_bit(bm, w, 4, 1);
u64 before = bm; uint64_t before = bm;
puzzle::bitmap_clear_bit(bm, w, 2, 3); puzzle::bitmap_clear_bit(bm, w, 2, 3);
CHECK_FALSE(puzzle::bitmap_get_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]") TEST_CASE("blocks_bitmap for single block", "[bitmap]")
{ {
puzzle p(6, 6, 0, 0, false, false); puzzle p(6, 6, 0, 0, false, false);
auto p2 = p.try_add_block(block(1, 2, 3, 1)); auto p2 = p.try_add_block(puzzle::block(1, 2, 3, 1));
REQUIRE(p2); REQUIRE(p2);
u64 bm = p2->blocks_bitmap(); uint64_t bm = p2->blocks_bitmap();
// Block at (1,2) width 3 height 1 on a 6-wide board // 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 // Bits: row 2, cols 1,2,3 -> positions 2*6+1=13, 14, 15
CHECK(puzzle::bitmap_get_bit(bm, 6, 1, 2)); 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]") TEST_CASE("blocks_bitmap for 2x2 block", "[bitmap]")
{ {
puzzle p(5, 5, 0, 0, false, false); puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(block(1, 1, 2, 2)); auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2));
REQUIRE(p2); REQUIRE(p2);
u64 bm = p2->blocks_bitmap(); uint64_t bm = p2->blocks_bitmap();
CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 1)); 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, 2, 1));
CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 2)); 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); puzzle p(6, 6, 0, 0, true, false);
// Horizontal block (2x1) -> principal_dirs has eas // Horizontal block (2x1) -> principal_dirs has eas
auto p2 = p.try_add_block(block(0, 0, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
REQUIRE(p2); REQUIRE(p2);
// Vertical block (1x2) -> principal_dirs has sou, not eas // Vertical block (1x2) -> principal_dirs has sou, not eas
auto p3 = p2->try_add_block(block(4, 0, 1, 2)); auto p3 = p2->try_add_block(puzzle::block(4, 0, 1, 2));
REQUIRE(p3); REQUIRE(p3);
u64 bm_h = p3->blocks_bitmap_h(); uint64_t bm_h = p3->blocks_bitmap_h();
// Horizontal block should be in 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, 0, 0));
CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 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); puzzle p(6, 6, 0, 0, true, false);
// Vertical block (1x2) // Vertical block (1x2)
auto p2 = p.try_add_block(block(0, 0, 1, 2)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 2));
REQUIRE(p2); REQUIRE(p2);
// Horizontal block (2x1) // Horizontal block (2x1)
auto p3 = p2->try_add_block(block(4, 0, 2, 1)); auto p3 = p2->try_add_block(puzzle::block(4, 0, 2, 1));
REQUIRE(p3); REQUIRE(p3);
u64 bm_v = p3->blocks_bitmap_v(); uint64_t bm_v = p3->blocks_bitmap_v();
// Vertical block should be in 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, 0));
CHECK(puzzle::bitmap_get_bit(bm_v, 6, 0, 1)); 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]") TEST_CASE("blocks_bitmap_h and blocks_bitmap_v both include square blocks", "[bitmap]")
{ {
puzzle p(6, 6, 0, 0, true, false); puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 1, 2, 2)); auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2));
REQUIRE(p2); REQUIRE(p2);
u64 bm_h = p2->blocks_bitmap_h(); uint64_t bm_h = p2->blocks_bitmap_h();
u64 bm_v = p2->blocks_bitmap_v(); uint64_t bm_v = p2->blocks_bitmap_v();
// Square block should appear in both // Square block should appear in both
CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 1)); 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) // 4x4 board, vertical 1x2 block at (1,2)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 2, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, vert, nor); 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) // 4x4 board, vertical 1x2 block at (1,0)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 0, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, vert, sou); 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) // 5x4 board, horizontal 2x1 block at (0,1)
puzzle p(5, 4, 0, 0, true, false); puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas); 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) // 5x4 board, horizontal 2x1 block at (2,1)
puzzle p(5, 4, 0, 0, true, false); puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(2, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes); 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 // 4x4 board, horizontal 2x1 block at (2,0) - rightmost position
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(2, 0, 2, 1)); auto p2 = p.try_add_block(puzzle::block(2, 0, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas); 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) // 4x4 board, horizontal 2x1 block at (0,1)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes); 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 // 6x4 board, two horizontal blocks side by side
puzzle p(6, 4, 0, 0, true, false); puzzle p(6, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(2, 0, 2, 1)); auto p3 = p2->try_add_block(puzzle::block(2, 0, 2, 1));
REQUIRE(p3); REQUIRE(p3);
u64 combined = p3->blocks_bitmap(); uint64_t combined = p3->blocks_bitmap();
u64 horiz = p3->blocks_bitmap_h(); uint64_t horiz = p3->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p3->bitmap_newly_occupied_after_move(bm, horiz, eas); 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) // 4x4 board, vertical 1x2 block at (1,2)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 2, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); 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) // 4x4 board, vertical 1x2 block at (1,0)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 0, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, sou); 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) // 5x4 board, horizontal 2x1 block at (0,1)
puzzle p(5, 4, 0, 0, true, false); puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
auto moves = p2->restricted_bitmap_get_moves(combined, horiz, eas); 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) // 5x4 board, horizontal 2x1 block at (2,1)
puzzle p(5, 4, 0, 0, true, false); puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(2, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
auto moves = p2->restricted_bitmap_get_moves(combined, horiz, wes); 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) // 4x4 board, vertical 1x2 block at (1,0) - can't move north (at top edge)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 0, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); 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) // 6x6 board, vertical 1x2 block at (2,3)
puzzle p(6, 6, 0, 0, true, false); puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(block(2, 3, 1, 2)); auto p2 = p.try_add_block(puzzle::block(2, 3, 1, 2));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 vert = p2->blocks_bitmap_v(); uint64_t vert = p2->blocks_bitmap_v();
auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); 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 // 6x6 board, two vertical blocks with space above
puzzle p(6, 6, 0, 0, true, false); puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 2, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(4, 3, 1, 2)); auto p3 = p2->try_add_block(puzzle::block(4, 3, 1, 2));
REQUIRE(p3); REQUIRE(p3);
u64 combined = p3->blocks_bitmap(); uint64_t combined = p3->blocks_bitmap();
u64 vert = p3->blocks_bitmap_v(); uint64_t vert = p3->blocks_bitmap_v();
auto moves = p3->restricted_bitmap_get_moves(combined, vert, nor); auto moves = p3->restricted_bitmap_get_moves(combined, vert, nor);
// Both blocks can move north // Both blocks can move north
// Block 1: newly occupied (1,1), source (1,2) = 1+2*6 = 13 // 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 // Block 2: newly occupied (4,2), source (4,3) = 4+3*6 = 22
std::set<u8> move_set; std::set<uint8_t> move_set;
for (int i = 0; i < puzzle::MAX_BLOCKS && moves[i] != 0xFF; ++i) { for (int i = 0; i < puzzle::MAX_BLOCKS && moves[i] != 0xFF; ++i) {
move_set.insert(moves[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]") TEST_CASE("try_move_block_at basic moves", "[move]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(block(1, 1, 1, 1)); auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto north = p2->try_move_block_at(1, 1, nor); 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]") TEST_CASE("try_move_block_at blocked by edge", "[move]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(block(0, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
CHECK_FALSE(p2->try_move_block_at(0, 0, nor).has_value()); 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]") TEST_CASE("try_move_block_at blocked by collision", "[move]")
{ {
puzzle p(4, 4, 0, 0, false, false); puzzle p(4, 4, 0, 0, false, false);
auto p2 = p.try_add_block(block(0, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(1, 0, 1, 1)); auto p3 = p2->try_add_block(puzzle::block(1, 0, 1, 1));
REQUIRE(p3); REQUIRE(p3);
CHECK_FALSE(p3->try_move_block_at(0, 0, eas).has_value()); 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]") TEST_CASE("try_move_block_at_fast matches try_move_block_at", "[move]")
{ {
puzzle p(5, 5, 0, 0, false, false); puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(block(1, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(0, 3, 1, 2)); auto p3 = p2->try_add_block(puzzle::block(0, 3, 1, 2));
REQUIRE(p3); REQUIRE(p3);
u64 bm = p3->blocks_bitmap(); uint64_t bm = p3->blocks_bitmap();
for (const dir d : {nor, eas, sou, wes}) { for (const direction d : {nor, eas, sou, wes}) {
// Block 0 is the first in sorted order // Block 0 is the first in sorted order
auto slow = p3->try_move_block_at(1, 1, d); auto slow = p3->try_move_block_at(1, 1, d);
auto fast = p3->try_move_block_at_fast(bm, 0, 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 puzzle p(5, 5, 0, 0, true, false); // restricted
// Horizontal block 2x1 at (1,2) // Horizontal block 2x1 at (1,2)
auto p2 = p.try_add_block(block(1, 2, 2, 1)); auto p2 = p.try_add_block(puzzle::block(1, 2, 2, 1));
REQUIRE(p2); REQUIRE(p2);
// In restricted mode, horizontal block can only move east/west // 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 // Helper: collect all adjacent states using for_each_adjacent_restricted
static auto collect_adjacent_restricted(const puzzle& p) -> std::set<size_t> static auto collect_adjacent_restricted(const puzzle& p) -> std::set<size_t>
{ {
const u8 w = p.get_width(); const uint8_t w = p.get_width();
std::array<u8, 64> bitmap_block_indices{}; std::array<uint8_t, 64> bitmap_block_indices{};
for (size_t i = 0; i < puzzle::MAX_BLOCKS; ++i) { for (size_t i = 0; i < puzzle::MAX_BLOCKS; ++i) {
const block b(p.repr_view().data()[i]); const puzzle::block b(p.repr_view().data()[i]);
if (i >= static_cast<size_t>(p.block_count())) { if (i >= static_cast<size_t>(p.block_count())) {
break; break;
} }
const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr();
for (u8 x = bx; x < bx + bw; ++x) { for (uint8_t x = bx; x < bx + bw; ++x) {
for (u8 y = by; y < by + bh; ++y) { for (uint8_t y = by; y < by + bh; ++y) {
bitmap_block_indices[y * w + x] = i; 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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - single block", "[cross_validate]")
{ {
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 1, 1, 1)); auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - horizontal block", "[cross_validate]")
{ {
puzzle p(5, 4, 0, 0, true, false); puzzle p(5, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - vertical block", "[cross_validate]")
{ {
puzzle p(4, 5, 0, 0, true, false); puzzle p(4, 5, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 1, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 2));
REQUIRE(p2); REQUIRE(p2);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - two blocks", "[cross_validate]")
{ {
puzzle p(5, 5, 0, 0, true, false); puzzle p(5, 5, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 2, 1)); // horizontal auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); // horizontal
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(4, 1, 1, 2)); // vertical auto p3 = p2->try_add_block(puzzle::block(4, 1, 1, 2)); // vertical
REQUIRE(p3); REQUIRE(p3);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - crowded board", "[cross_validate]")
{ {
puzzle p(5, 5, 0, 0, true, false); puzzle p(5, 5, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(3, 0, 1, 2)); auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2));
REQUIRE(p3); REQUIRE(p3);
auto p4 = p3->try_add_block(block(0, 2, 1, 3)); auto p4 = p3->try_add_block(puzzle::block(0, 2, 1, 3));
REQUIRE(p4); REQUIRE(p4);
auto p5 = p4->try_add_block(block(2, 3, 2, 1)); auto p5 = p4->try_add_block(puzzle::block(2, 3, 2, 1));
REQUIRE(p5); REQUIRE(p5);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - 6x6 board", "[cross_validate]")
{ {
puzzle p(6, 6, 0, 0, true, false); puzzle p(6, 6, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 3, 1)); // horizontal auto p2 = p.try_add_block(puzzle::block(0, 0, 3, 1)); // horizontal
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(5, 0, 1, 2)); // vertical auto p3 = p2->try_add_block(puzzle::block(5, 0, 1, 2)); // vertical
REQUIRE(p3); REQUIRE(p3);
auto p4 = p3->try_add_block(block(2, 2, 1, 2)); // vertical auto p4 = p3->try_add_block(puzzle::block(2, 2, 1, 2)); // vertical
REQUIRE(p4); REQUIRE(p4);
auto p5 = p4->try_add_block(block(3, 2, 2, 1)); // horizontal auto p5 = p4->try_add_block(puzzle::block(3, 2, 2, 1)); // horizontal
REQUIRE(p5); REQUIRE(p5);
auto simple = collect_adjacent_simple(*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]") TEST_CASE("explore_state_space simple case - single 1x1 block", "[explore]")
{ {
puzzle p(3, 3, 0, 0, false, false); puzzle p(3, 3, 0, 0, false, false);
auto p2 = p.try_add_block(block(1, 1, 1, 1)); auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space(); 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]") TEST_CASE("explore_state_space restricted - vertical block", "[explore]")
{ {
puzzle p(3, 4, 0, 0, true, false); puzzle p(3, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(1, 0, 1, 2)); auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2));
REQUIRE(p2); REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space(); 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]") TEST_CASE("explore_state_space restricted - horizontal block", "[explore]")
{ {
puzzle p(4, 3, 0, 0, true, false); puzzle p(4, 3, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 1, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space(); 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]") TEST_CASE("explore_state_space restricted - square block", "[explore]")
{ {
puzzle p(3, 3, 0, 0, true, false); puzzle p(3, 3, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space(); 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]") TEST_CASE("explore_state_space preserves board metadata", "[explore]")
{ {
puzzle p(5, 4, 2, 1, true, true); puzzle p(5, 4, 2, 1, true, true);
auto p2 = p.try_add_block(block(0, 0, 1, 1, true)); auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1, true));
REQUIRE(p2); REQUIRE(p2);
auto [states, transitions] = p2->explore_state_space(); auto [states, transitions] = p2->explore_state_space();
@ -959,9 +959,9 @@ TEST_CASE("explore_state_space two blocks restricted", "[explore]")
// 4x4 board, restricted // 4x4 board, restricted
// Horizontal 2x1 at (0,0) and vertical 1x2 at (3,0) // Horizontal 2x1 at (0,0) and vertical 1x2 at (3,0)
puzzle p(4, 4, 0, 0, true, false); puzzle p(4, 4, 0, 0, true, false);
auto p2 = p.try_add_block(block(0, 0, 2, 1)); auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(3, 0, 1, 2)); auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2));
REQUIRE(p3); REQUIRE(p3);
auto [states, transitions] = p3->explore_state_space(); 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]") TEST_CASE("East move does not wrap for various board widths", "[bitmap_move]")
{ {
for (u8 w = 3; w <= 8; ++w) { for (uint8_t w = 3; w <= 8; ++w) {
puzzle p(w, 3, 0, 0, true, false); puzzle p(w, 3, 0, 0, true, false);
// Place a 1x1 block at rightmost column, row 0 // Place a 1x1 block at rightmost column, row 0
auto p2 = p.try_add_block(block(w - 1, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(w - 1, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v uint64_t horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, eas); 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]") TEST_CASE("West move does not wrap for various board widths", "[bitmap_move]")
{ {
for (u8 w = 3; w <= 8; ++w) { for (uint8_t w = 3; w <= 8; ++w) {
puzzle p(w, 3, 0, 0, true, false); puzzle p(w, 3, 0, 0, true, false);
// Place a 1x1 block at leftmost column, row 1 // Place a 1x1 block at leftmost column, row 1
auto p2 = p.try_add_block(block(0, 1, 1, 1)); auto p2 = p.try_add_block(puzzle::block(0, 1, 1, 1));
REQUIRE(p2); REQUIRE(p2);
u64 combined = p2->blocks_bitmap(); uint64_t combined = p2->blocks_bitmap();
u64 horiz = p2->blocks_bitmap_h(); uint64_t horiz = p2->blocks_bitmap_h();
u64 bm = combined; uint64_t bm = combined;
p2->bitmap_newly_occupied_after_move(bm, horiz, wes); 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]") TEST_CASE("bitmap_check_collision detects collision", "[collision]")
{ {
puzzle p(5, 5, 0, 0, false, false); puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(block(2, 2, 1, 1)); auto p2 = p.try_add_block(puzzle::block(2, 2, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(0, 0, 2, 1)); auto p3 = p2->try_add_block(puzzle::block(0, 0, 2, 1));
REQUIRE(p3); REQUIRE(p3);
u64 bm = p3->blocks_bitmap(); uint64_t bm = p3->blocks_bitmap();
// Check if a hypothetical block at (1,2) 2x1 collides // Check if a hypothetical block at (1,2) 2x1 collides
block hyp(1, 2, 2, 1); puzzle::block hyp(1, 2, 2, 1);
CHECK(p3->bitmap_check_collision(bm, hyp)); // overlaps with (2,2) CHECK(p3->bitmap_check_collision(bm, hyp)); // overlaps with (2,2)
// Check if a hypothetical block at (3,3) 1x1 collides // Check if a hypothetical block at (3,3) 1x1 collides
block hyp2(3, 3, 1, 1); puzzle::block hyp2(3, 3, 1, 1);
CHECK_FALSE(p3->bitmap_check_collision(bm, hyp2)); CHECK_FALSE(p3->bitmap_check_collision(bm, hyp2));
} }
TEST_CASE("bitmap_check_collision directional", "[collision]") TEST_CASE("bitmap_check_collision directional", "[collision]")
{ {
puzzle p(5, 5, 0, 0, false, false); puzzle p(5, 5, 0, 0, false, false);
auto p2 = p.try_add_block(block(2, 0, 1, 1)); auto p2 = p.try_add_block(puzzle::block(2, 0, 1, 1));
REQUIRE(p2); REQUIRE(p2);
auto p3 = p2->try_add_block(block(2, 1, 1, 1)); auto p3 = p2->try_add_block(puzzle::block(2, 1, 1, 1));
REQUIRE(p3); REQUIRE(p3);
u64 bm = p3->blocks_bitmap(); uint64_t bm = p3->blocks_bitmap();
// Clear block at (2,1) from bitmap to check if (2,1) can move north // Clear block at (2,1) from bitmap to check if (2,1) can move north
block b(2, 1, 1, 1); puzzle::block b(2, 1, 1, 1);
p3->bitmap_clear_block(bm, b); p3->bitmap_clear_block(bm, b);
// (2,1) moving north -> check (2,0) which has a block // (2,1) moving north -> check (2,0) which has a block