add single state space benchmark + some tests
This commit is contained in:
@ -1,43 +1,46 @@
|
|||||||
cmake_minimum_required(VERSION 3.25)
|
cmake_minimum_required(VERSION 3.28)
|
||||||
project(MassSprings)
|
project(MassSprings)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 26)
|
set(CMAKE_CXX_STANDARD 26)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# Disable boost warning because our cmake/boost are recent enough
|
||||||
if(POLICY CMP0167)
|
if(POLICY CMP0167)
|
||||||
cmake_policy(SET CMP0167 NEW)
|
cmake_policy(SET CMP0167 NEW)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF)
|
option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF)
|
||||||
option(DISABLE_TRACY "Disable the Tracy profiler client" OFF)
|
option(DISABLE_TRACY "Disable the Tracy profiler client" OFF)
|
||||||
option(DISABLE_TESTS "Disable building and running tests" OFF)
|
option(DISABLE_TESTS "Disable building tests" OFF)
|
||||||
|
option(DISABLE_BENCH "Disable building benchmarks" OFF)
|
||||||
|
|
||||||
# Headers + Sources
|
# Headers + Sources (excluding main.cpp)
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
src/backward.cpp
|
src/backward.cpp
|
||||||
src/graph_distances.cpp
|
src/graph_distances.cpp
|
||||||
src/input_handler.cpp
|
src/input_handler.cpp
|
||||||
|
src/load_save.cpp
|
||||||
src/mass_spring_system.cpp
|
src/mass_spring_system.cpp
|
||||||
src/octree.cpp
|
src/octree.cpp
|
||||||
src/orbit_camera.cpp
|
src/orbit_camera.cpp
|
||||||
|
src/puzzle.cpp
|
||||||
src/renderer.cpp
|
src/renderer.cpp
|
||||||
src/state_manager.cpp
|
src/state_manager.cpp
|
||||||
src/threaded_physics.cpp
|
src/threaded_physics.cpp
|
||||||
src/user_interface.cpp
|
src/user_interface.cpp
|
||||||
src/puzzle.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Libraries
|
# Libraries
|
||||||
|
include(FetchContent)
|
||||||
find_package(raylib REQUIRED)
|
find_package(raylib REQUIRED)
|
||||||
find_package(Boost REQUIRED)
|
find_package(Boost COMPONENTS program_options REQUIRED)
|
||||||
set(LIBS raylib Boost::headers)
|
set(LIBS raylib Boost::headers Boost::program_options)
|
||||||
set(FLAGS "")
|
set(FLAGS "")
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
list(APPEND LIBS opengl32 gdi32 winmm)
|
list(APPEND LIBS opengl32 gdi32 winmm)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
include(FetchContent)
|
|
||||||
if(NOT DISABLE_BACKWARD)
|
if(NOT DISABLE_BACKWARD)
|
||||||
find_package(Backward REQUIRED)
|
find_package(Backward REQUIRED)
|
||||||
|
|
||||||
@ -63,7 +66,7 @@ endif()
|
|||||||
# Set this after fetching tracy to hide tracy's warnings
|
# Set this after fetching tracy to hide tracy's warnings
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code")
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0")
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0")
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -Ofast -march=native")
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -O3 -ffast-math -march=native")
|
||||||
|
|
||||||
message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
|
message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
|
||||||
message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}")
|
message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}")
|
||||||
@ -78,18 +81,21 @@ target_include_directories(masssprings PRIVATE include)
|
|||||||
target_link_libraries(masssprings PRIVATE ${LIBS})
|
target_link_libraries(masssprings PRIVATE ${LIBS})
|
||||||
target_compile_definitions(masssprings PRIVATE ${FLAGS})
|
target_compile_definitions(masssprings PRIVATE ${FLAGS})
|
||||||
|
|
||||||
# Testing sources
|
# Testing
|
||||||
if(NOT DISABLE_TESTS AND NOT WIN32)
|
if(NOT DISABLE_TESTS AND NOT WIN32)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
FetchContent_Declare(Catch2
|
FetchContent_Declare(Catch2
|
||||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||||
GIT_TAG v3.13.0
|
GIT_TAG v3.13.0
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(Catch2)
|
FetchContent_MakeAvailable(Catch2)
|
||||||
|
|
||||||
set(TEST_SOURCES
|
set(TEST_SOURCES
|
||||||
test/bits.cpp
|
test/bits.cpp
|
||||||
|
test/bitmap.cpp
|
||||||
|
test/bitmap_find_first_empty.cpp
|
||||||
|
# test/puzzle.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(tests ${TEST_SOURCES} ${SOURCES})
|
add_executable(tests ${TEST_SOURCES} ${SOURCES})
|
||||||
@ -100,8 +106,20 @@ if(NOT DISABLE_TESTS AND NOT WIN32)
|
|||||||
catch_discover_tests(tests)
|
catch_discover_tests(tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Benchmarking
|
||||||
|
if(NOT DISABLE_BENCH AND NOT WIN32)
|
||||||
|
find_package(benchmark REQUIRED)
|
||||||
|
|
||||||
|
set(BENCH_SOURCES
|
||||||
|
benchmark/state_space.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(benchmarks ${BENCH_SOURCES} ${SOURCES})
|
||||||
|
target_include_directories(benchmarks PRIVATE include)
|
||||||
|
target_link_libraries(benchmarks benchmark raylib)
|
||||||
|
endif()
|
||||||
|
|
||||||
# LTO
|
# LTO
|
||||||
#if(NOT WIN32)
|
|
||||||
include(CheckIPOSupported)
|
include(CheckIPOSupported)
|
||||||
check_ipo_supported(RESULT supported OUTPUT error)
|
check_ipo_supported(RESULT supported OUTPUT error)
|
||||||
if(supported)
|
if(supported)
|
||||||
@ -109,5 +127,4 @@ if(supported)
|
|||||||
set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
|
set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||||
else()
|
else()
|
||||||
message(STATUS "IPO / LTO not supported")
|
message(STATUS "IPO / LTO not supported")
|
||||||
endif()
|
endif()
|
||||||
#endif()
|
|
||||||
51
benchmark/state_space.cpp
Normal file
51
benchmark/state_space.cpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#include "puzzle.hpp"
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
static std::vector<std::string> puzzles = {
|
||||||
|
// 0: RushHour 1
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ _ _ 1x3} {_ _ _ _ _ _} {_ _ 1x2 2X1 _ _} {_ _ _ 1x2 2x1 _} {1x2 _ 1x2 _ 2x1 _} {_ _ _ 3x1 _ _}]",
|
||||||
|
// 1: RushHour 2
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{1x2 3x1 _ _ 1x2 1x3} {_ 3x1 _ _ _ _} {2X1 _ 1x2 1x2 1x2 _} {2x1 _ _ _ _ _} {_ _ _ 1x2 2x1 _} {_ _ _ _ 2x1 _}]",
|
||||||
|
// 2: RushHour 3
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ 1x2 _ _} {1x2 2x1 _ _ _ 1x2} {_ 2X1 _ 1x2 1x2 _} {2x1 _ 1x2 _ _ 1x2} {_ _ _ 2x1 _ _} {_ 2x1 _ 2x1 _ _}]",
|
||||||
|
// 3: RushHour 4
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{1x3 2x1 _ _ 1x2 _} {_ 1x2 1x2 _ _ 1x3} {_ _ _ 2X1 _ _} {3x1 _ _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {2x1 _ _ 2x1 _ _}]",
|
||||||
|
// 4: RushHour + Walls 1
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{1x2 2x1 _ 1*1 _ _} {_ _ _ 1x2 2x1 _} {1x2 2X1 _ _ _ _} {_ _ 1x2 2x1 _ 1x3} {2x1 _ _ _ _ _} {2x1 _ 3x1 _ _ _}]",
|
||||||
|
// 5: RushHour + Walls 2
|
||||||
|
"S:[6x6] G:[4,2] M:[R] B:[{2x1 _ _ 1x2 1x2 1*1} {3x1 _ _ _ _ _} {1x2 2X1 _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {_ _ _ 2x1 _ 1x2} {_ _ 2x1 _ 1*1 _}]",
|
||||||
|
// 6: Dad's Puzzler
|
||||||
|
"S:[4x5] G:[0,3] M:[F] B:[{2X2 _ 2x1 _} {_ _ 2x1 _} {1x1 1x1 _ _} {1x2 1x2 2x1 _} {_ _ 2x1 _}]",
|
||||||
|
// 7: Nine Blocks
|
||||||
|
"S:[4x5] G:[0,3] M:[F] B:[{1x2 1x2 _ _} {_ _ 2x1 _} {1x2 1x2 2x1 _} {_ _ 2X2 _} {1x1 1x1 _ _}]",
|
||||||
|
// 8: Quzzle
|
||||||
|
"S:[4x5] G:[2,0] M:[F] B:[{2X2 _ 2x1 _} {_ _ 1x2 1x2} {_ _ _ _} {1x2 2x1 _ 1x1} {_ 2x1 _ 1x1}]",
|
||||||
|
// 9: Thin Klotski
|
||||||
|
"S:[4x5] G:[1,4] M:[F] B:[{1x2 _ 2X1 _} {_ 2x2 _ 1x1} {_ _ _ 1x1} {2x2 _ 1x1 1x1} {_ _ 1x1 1x1}]",
|
||||||
|
// 10: Fat Klotski
|
||||||
|
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ 1x1} {1x1 _ _ 1x2} {1x1 2x2 _ _} {1x1 _ _ _} {1x1 1x1 2x1 _}]",
|
||||||
|
// 11: Klotski
|
||||||
|
"S:[4x5] G:[1,3] M:[F] B:[{1x2 2X2 _ 1x2} {_ _ _ _} {1x2 2x1 _ 1x2} {_ 1x1 1x1 _} {1x1 _ _ 1x1}]",
|
||||||
|
// 12: Century
|
||||||
|
"S:[4x5] G:[1,3] M:[F] B:[{1x1 2X2 _ 1x1} {1x2 _ _ 1x2} {_ 1x2 _ _} {1x1 _ _ 1x1} {2x1 _ 2x1 _}]",
|
||||||
|
// 13: Super Century
|
||||||
|
"S:[4x5] G:[1,3] M:[F] B:[{1x2 1x1 1x1 1x1} {_ 1x2 2X2 _} {1x2 _ _ _} {_ 2x1 _ 1x1} {_ 2x1 _ _}]",
|
||||||
|
// 14: Supercompo
|
||||||
|
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]",
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto explore_state_space(benchmark::State& state) -> void
|
||||||
|
{
|
||||||
|
const puzzle p = puzzle(puzzles[state.range(0)]);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
auto space = p.explore_state_space();
|
||||||
|
|
||||||
|
benchmark::DoNotOptimize(space);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK(explore_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
74
test/bitmap.cpp
Normal file
74
test/bitmap.cpp
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// ReSharper disable CppLocalVariableMayBeConst
|
||||||
|
#include "puzzle.hpp"
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("bitmap_is_full all bits set", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p1(5, 5);
|
||||||
|
puzzle p2(3, 4);
|
||||||
|
puzzle p3(5, 4);
|
||||||
|
puzzle p4(3, 7);
|
||||||
|
uint64_t bitmap = -1;
|
||||||
|
|
||||||
|
REQUIRE(p1.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE(p2.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE(p3.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE(p4.bitmap_is_full(bitmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("bitmap_is_full no bits set", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p1(5, 5);
|
||||||
|
puzzle p2(3, 4);
|
||||||
|
puzzle p3(5, 4);
|
||||||
|
puzzle p4(3, 7);
|
||||||
|
uint64_t bitmap = 0;
|
||||||
|
|
||||||
|
REQUIRE_FALSE(p1.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE_FALSE(p2.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE_FALSE(p3.bitmap_is_full(bitmap));
|
||||||
|
REQUIRE_FALSE(p4.bitmap_is_full(bitmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("bitmap_is_full necessary bits set", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p1(5, 5);
|
||||||
|
puzzle p2(3, 4);
|
||||||
|
puzzle p3(5, 4);
|
||||||
|
puzzle p4(3, 7);
|
||||||
|
|
||||||
|
uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5
|
||||||
|
uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4
|
||||||
|
uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4
|
||||||
|
uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7
|
||||||
|
|
||||||
|
REQUIRE(p1.bitmap_is_full(bitmap1));
|
||||||
|
REQUIRE(p2.bitmap_is_full(bitmap2));
|
||||||
|
REQUIRE(p3.bitmap_is_full(bitmap3));
|
||||||
|
REQUIRE(p4.bitmap_is_full(bitmap4));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("bitmap_is_full necessary bits not set", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p1(5, 5);
|
||||||
|
puzzle p2(3, 4);
|
||||||
|
puzzle p3(5, 4);
|
||||||
|
puzzle p4(3, 7);
|
||||||
|
|
||||||
|
uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5
|
||||||
|
uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4
|
||||||
|
uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4
|
||||||
|
uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7
|
||||||
|
|
||||||
|
bitmap1 &= ~(1ull << 12);
|
||||||
|
bitmap2 &= ~(1ull << 6);
|
||||||
|
bitmap3 &= ~(1ull << 8);
|
||||||
|
bitmap4 &= ~(1ull << 18);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(p1.bitmap_is_full(bitmap1));
|
||||||
|
REQUIRE_FALSE(p2.bitmap_is_full(bitmap2));
|
||||||
|
REQUIRE_FALSE(p3.bitmap_is_full(bitmap3));
|
||||||
|
REQUIRE_FALSE(p4.bitmap_is_full(bitmap4));
|
||||||
|
}
|
||||||
266
test/bitmap_find_first_empty.cpp
Normal file
266
test/bitmap_find_first_empty.cpp
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
// ReSharper disable CppLocalVariableMayBeConst
|
||||||
|
#include "puzzle.hpp"
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
|
||||||
|
static auto board_mask(const int w, const int h) -> uint64_t
|
||||||
|
{
|
||||||
|
const int cells = w * h;
|
||||||
|
if (cells == 64) {
|
||||||
|
return ~0ULL;
|
||||||
|
}
|
||||||
|
return (1ULL << cells) - 1ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Empty board returns (0,0)", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p(5, 5);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(0ULL, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == 0);
|
||||||
|
REQUIRE(y == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Full board detection respects width*height only", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 6}, std::tuple{8, 8});
|
||||||
|
|
||||||
|
puzzle p(w, h);
|
||||||
|
|
||||||
|
uint64_t mask = board_mask(w, h);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
|
||||||
|
REQUIRE_FALSE(p.bitmap_find_first_empty(mask, x, y));
|
||||||
|
|
||||||
|
// Bits outside board should not affect fullness
|
||||||
|
REQUIRE_FALSE(p.bitmap_find_first_empty(mask | (~mask), x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Single empty cell at various positions", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 5}, std::tuple{8, 8});
|
||||||
|
|
||||||
|
puzzle p(w, h);
|
||||||
|
|
||||||
|
int cells = w * h;
|
||||||
|
|
||||||
|
auto empty_index = GENERATE_COPY(values<int>({ 0, cells / 2, cells - 1}));
|
||||||
|
|
||||||
|
uint64_t bitmap = board_mask(w, h);
|
||||||
|
bitmap &= ~(1ULL << empty_index);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == empty_index % w);
|
||||||
|
REQUIRE(y == empty_index / w);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Bits outside board are ignored", "[puzzle][board]")
|
||||||
|
{
|
||||||
|
puzzle p(3, 3); // 9 cells
|
||||||
|
|
||||||
|
uint64_t mask = board_mask(3, 3);
|
||||||
|
|
||||||
|
// Board is full
|
||||||
|
uint64_t bitmap = mask;
|
||||||
|
|
||||||
|
// Set extra bits outside 9 cells
|
||||||
|
bitmap |= (1ULL << 20);
|
||||||
|
bitmap |= (1ULL << 63);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE_FALSE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("First empty found in forward search branch", "[puzzle][branch]")
|
||||||
|
{
|
||||||
|
puzzle p(4, 4); // 16 cells
|
||||||
|
|
||||||
|
// Only MSB (within board) set
|
||||||
|
uint64_t bitmap = (1ULL << 15);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == 0);
|
||||||
|
REQUIRE(y == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Backward search branch finds gap before MSB cluster", "[puzzle][branch]")
|
||||||
|
{
|
||||||
|
puzzle p(4, 4); // 16 cells
|
||||||
|
|
||||||
|
// Set bits 15,14,13 but leave 12 empty
|
||||||
|
uint64_t bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == 0);
|
||||||
|
REQUIRE(y == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Rectangular board coordinate mapping", "[puzzle][rect]")
|
||||||
|
{
|
||||||
|
puzzle p(5, 3); // 15 cells
|
||||||
|
|
||||||
|
int empty_index = 11;
|
||||||
|
|
||||||
|
uint64_t bitmap = board_mask(5, 3);
|
||||||
|
bitmap &= ~(1ULL << empty_index);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == empty_index % 5);
|
||||||
|
REQUIRE(y == empty_index / 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]")
|
||||||
|
{
|
||||||
|
puzzle p(7, 8); // 56 cells
|
||||||
|
|
||||||
|
uint64_t mask = board_mask(7, 8);
|
||||||
|
|
||||||
|
// Full board should return false
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE_FALSE(p.bitmap_find_first_empty(mask, x, y));
|
||||||
|
|
||||||
|
// Clear highest valid cell
|
||||||
|
int empty_index = 55;
|
||||||
|
mask &= ~(1ULL << empty_index);
|
||||||
|
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(mask, x, y));
|
||||||
|
REQUIRE(x == empty_index % 7);
|
||||||
|
REQUIRE(y == empty_index / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Oracle: find first zero bit inside board ---
|
||||||
|
static auto oracle_find_first_empty(uint64_t bitmap, int w, int h, int& x, int& y) -> bool
|
||||||
|
{
|
||||||
|
int cells = w * h;
|
||||||
|
|
||||||
|
for (int i = 0; i < cells; ++i) {
|
||||||
|
if ((bitmap & (1ULL << i)) == 0) {
|
||||||
|
x = i % w;
|
||||||
|
y = i / w;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Oracle validation across board sizes 3x3 to 8x8", "[puzzle][oracle]")
|
||||||
|
{
|
||||||
|
auto [w, h] = GENERATE(std::tuple{3, 3}, std::tuple{4, 4}, std::tuple{5, 5}, std::tuple{6, 6}, std::tuple{7, 7},
|
||||||
|
std::tuple{8, 8}, std::tuple{3, 5}, std::tuple{5, 3}, std::tuple{7, 8}, std::tuple{8, 7});
|
||||||
|
|
||||||
|
puzzle p(w, h);
|
||||||
|
|
||||||
|
uint64_t mask = board_mask(w, h);
|
||||||
|
|
||||||
|
std::mt19937_64 rng(12345);
|
||||||
|
std::uniform_int_distribution<uint64_t> dist(0, UINT64_MAX);
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 200; ++iteration) {
|
||||||
|
uint64_t bitmap = dist(rng);
|
||||||
|
|
||||||
|
int ox = -1, oy = -1;
|
||||||
|
bool oracle_result = oracle_find_first_empty(bitmap, w, h, ox, oy);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
bool result = p.bitmap_find_first_empty(bitmap, x, y);
|
||||||
|
|
||||||
|
REQUIRE(result == oracle_result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
REQUIRE(x == ox);
|
||||||
|
REQUIRE(y == oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Bits set outside board only behaves as empty board", "[puzzle][outside]")
|
||||||
|
{
|
||||||
|
puzzle p(3, 3); // 9 cells
|
||||||
|
|
||||||
|
uint64_t bitmap = (1ULL << 40) | (1ULL << 63);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == 0);
|
||||||
|
REQUIRE(y == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Last valid cell empty stresses upper bound", "[puzzle][boundary]")
|
||||||
|
{
|
||||||
|
auto [w, h] = GENERATE(std::tuple{4, 4}, std::tuple{5, 6}, std::tuple{7, 8}, std::tuple{8, 8});
|
||||||
|
|
||||||
|
puzzle p(w, h);
|
||||||
|
|
||||||
|
int cells = w * h;
|
||||||
|
uint64_t bitmap = board_mask(w, h);
|
||||||
|
|
||||||
|
// Clear last valid bit
|
||||||
|
bitmap &= ~(1ULL << (cells - 1));
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == (cells - 1) % w);
|
||||||
|
REQUIRE(y == (cells - 1) / w);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Board sizes between 33 and 63 cells", "[puzzle][midrange]")
|
||||||
|
{
|
||||||
|
auto [w, h] = GENERATE(std::tuple{6, 6}, // 36
|
||||||
|
std::tuple{7, 6}, // 42
|
||||||
|
std::tuple{7, 7}, // 49
|
||||||
|
std::tuple{8, 7}, // 56
|
||||||
|
std::tuple{7, 8} // 56
|
||||||
|
);
|
||||||
|
|
||||||
|
puzzle p(w, h);
|
||||||
|
|
||||||
|
int cells = w * h;
|
||||||
|
|
||||||
|
for (int index : {31, 32, cells - 2}) {
|
||||||
|
if (index >= cells) continue;
|
||||||
|
|
||||||
|
uint64_t bitmap = board_mask(w, h);
|
||||||
|
bitmap &= ~(1ULL << index);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
REQUIRE(x == index % w);
|
||||||
|
REQUIRE(y == index / w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Multiple holes choose lowest index", "[puzzle][multiple]")
|
||||||
|
{
|
||||||
|
puzzle p(5, 5);
|
||||||
|
|
||||||
|
uint64_t bitmap = board_mask(5, 5);
|
||||||
|
|
||||||
|
// Clear several positions
|
||||||
|
bitmap &= ~(1ULL << 3);
|
||||||
|
bitmap &= ~(1ULL << 7);
|
||||||
|
bitmap &= ~(1ULL << 12);
|
||||||
|
|
||||||
|
int x = -1, y = -1;
|
||||||
|
REQUIRE(p.bitmap_find_first_empty(bitmap, x, y));
|
||||||
|
|
||||||
|
// Oracle expectation: index 3
|
||||||
|
REQUIRE(x == 3 % 5);
|
||||||
|
REQUIRE(y == 3 / 5);
|
||||||
|
}
|
||||||
1092
test/puzzle.cpp
Normal file
1092
test/puzzle.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user