Compare commits

...

93 Commits

Author SHA1 Message Date
592c4b6cc0 massive state space solver improvement: supercomp took ~10s, now ~40ms
achieved by imposing a 15 block limit on each board and changing the
internal representation from std::string to 4x uint64_t
2026-03-02 05:26:18 +01:00
c7361fe47e fix board goal rendering bug if no target block exists or board has no win condition 2026-03-01 00:30:06 +01:00
846ff72d1f fix bug where starting index was set incorrectly when populating graph 2026-02-28 23:16:00 +01:00
9ab96c5903 rename files based on their classes 2026-02-28 22:30:00 +01:00
809fbf1b93 add popups to certain user actions 2026-02-28 21:49:41 +01:00
bc8dd423be fix bug in preset validation and clear_goal (board repr wasn't updated correctly) 2026-02-28 18:58:42 +01:00
ce05dd504a small refactor 2026-02-28 18:37:47 +01:00
3f71603961 improve gui elements styling when disabled 2026-02-27 14:27:33 +01:00
b01cfdecfe implement option to lock camera to graph's center of mass 2026-02-27 14:06:30 +01:00
e8bd90911d reset selection when resetting state 2026-02-27 13:26:14 +01:00
db694838a7 fix compiler warnings 2026-02-27 13:17:18 +01:00
9d8b364330 simplify cmake file 2026-02-27 12:43:19 +01:00
85ed3a758a make windows-compliant
- fix BS_thread_pool include error (pulls in windows.h, symbols were
redefined)
- remove all std::println uses :(, seems like mingw doesn't like those
- allow to disable backward from cmake
2026-02-27 12:31:08 +01:00
16df3b7d51 add windows cross-compile package target to flake 2026-02-27 12:30:13 +01:00
0cc62009c5 fix crash when resetting the state after clearing the graph 2026-02-27 11:35:30 +01:00
f512306109 prevent zooming when in orthographic perspective 2026-02-27 03:35:54 +01:00
2517a9d33b complete rework of the user interface (using raygui) 2026-02-27 03:01:43 +01:00
bd1bd79825 patch raygui to allow prefixing the number spinner values 2026-02-27 00:06:44 +01:00
a879357437 add fonts 2026-02-27 00:06:29 +01:00
0a55996df3 add raygui patch - adds a prefix label to valuebox/spinner 2026-02-26 21:54:26 +01:00
330fe694a7 remove non-goal presets 2026-02-26 03:56:21 +01:00
1195716369 add raygui library dependency 2026-02-26 03:56:14 +01:00
4a37e93b98 implement immovable blocks (walls) and add two presets 2026-02-25 17:21:54 +01:00
81620d8709 add more puzzles 2026-02-25 16:53:24 +01:00
5e3d007a9d display puzzle title 2026-02-25 16:53:13 +01:00
5a2172cb00 fix crash when resetting an edited board 2026-02-25 16:06:13 +01:00
dc894d1c38 add a second tracy target instead of modifying the main target 2026-02-25 15:24:12 +01:00
79f088d10e disable tracy in flake instead of CMakeList.txt 2026-02-25 15:14:49 +01:00
a3a8c2f31a add fat klotski 2026-02-25 14:13:33 +01:00
58235ac52e some valgrind bullshit 2026-02-25 13:34:52 +01:00
82d618f692 update menu + block placing visualization + add move history 2026-02-25 12:45:06 +01:00
3f8274f1d9 add button to move to a state farthest from any win condition 2026-02-25 03:20:40 +01:00
271902ab1f implement automatic graph traversal along shortest path to solution 2026-02-25 02:58:30 +01:00
fd58f217c6 implement bfs multi-target distance calculation to nearest winning state 2026-02-25 01:15:47 +01:00
b9e3ab8d2d implement editing the board win condition 2026-02-24 22:43:53 +01:00
d8534cacdd store winning condition in state + remove presets 2026-02-24 22:08:00 +01:00
f31f9557b3 update flags again 2026-02-24 20:53:27 +01:00
349d614611 add flag to toggle tracy (disabled for now) 2026-02-24 20:42:38 +01:00
d88b66c058 capture stack traces on new/delete 2026-02-24 19:00:36 +01:00
8a4e5c1ebf squash merge threaded-physics into main 2026-02-24 19:00:25 +01:00
3e87bbb6a5 remove timings print 2026-02-24 00:12:04 +01:00
bfe8c6d572 store masses/springs inside vector and manage unordered_maps for a state<->index mapping
this reduces the time required to iterate over all masses/springs
because data is stored in contiguous memory
2026-02-24 00:01:04 +01:00
404a76654c update compiler flags, fix tracy allocation profiling, fix compiler warnings 2026-02-23 23:01:30 +01:00
59a4303d62 mark visited and starting states 2026-02-23 14:41:20 +01:00
6698ace0c6 implement smooth camera 2026-02-23 14:27:56 +01:00
861fb34d39 fix octree corruption bug because of node vector reallocation 2026-02-23 13:46:00 +01:00
30b02c13ed add vertices draw limit after I accidentally removed it 2026-02-23 00:27:51 +01:00
f5bb155b5c fix physics animation jitter 2026-02-23 00:15:25 +01:00
443069f597 replace openmp with thread-pool library bc openmp has larger fork boundary overhead 2026-02-23 00:14:09 +01:00
73b01f6af3 add thread-pool library dependency 2026-02-22 23:32:33 +01:00
21a18443e9 add tracy profiler dependency 2026-02-22 23:32:24 +01:00
e43e505110 implement barnes-hut particle repulsion using octree 2026-02-22 23:29:56 +01:00
f07f2772c4 remove octree library dependency (it doesn't store center of mass per node) 2026-02-22 20:29:01 +01:00
bdb4242076 remove useless manual move/copy constructors/assignment operators (pls stop killing my hands bjarne) 2026-02-22 20:28:42 +01:00
6a86324f53 add octree library dependency 2026-02-22 19:42:57 +01:00
768a7eaf82 implement operator<< for ryalib vectors 2026-02-22 19:42:48 +01:00
9726d5fecc cleanup repulsion force calculation 2026-02-22 19:19:06 +01:00
2580d6d527 remove unused inputs from instancing vertex shader 2026-02-22 14:33:22 +01:00
157985df6b extract orbital camera into separate file 2026-02-22 14:30:55 +01:00
7bc1eaae75 move shaders to separate directory 2026-02-22 14:18:49 +01:00
f06afc210f make naming more consistent + rename some files 2026-02-22 14:17:55 +01:00
fe6bbe9bbb remove batched rendering code 2026-02-22 14:04:11 +01:00
0083143268 cleanup flake 2026-02-22 13:57:04 +01:00
9446e1b86c improve rendering performance even more by using instanced rendering 2026-02-22 12:31:58 +01:00
12a96cba66 decrease special mass size + add hjkl movement bindings 2026-02-22 01:49:54 +01:00
35de23e865 improve rendering performance by batching edge and cube draws 2026-02-22 01:41:39 +01:00
05172d0d8f replace physics loop with fixed-step loop 2026-02-22 00:27:56 +01:00
cc4f8ce865 refactor usage of std::string to refer to states + improve initial mass positioning 2026-02-22 00:12:47 +01:00
0d3913e27e refactor state management and input handling into separate classes 2026-02-21 22:09:42 +01:00
f8fe9e35d6 add more presets (century, super_century, new_century) 2026-02-20 02:15:46 +01:00
71e1dd4f32 fix state representation not being updated when toggling restricted movement 2026-02-20 02:15:13 +01:00
7860963fcb only clear the graph on board reset if necessary 2026-02-20 01:41:03 +01:00
6e5a4acdd0 tint the board if a winning state is reached 2026-02-20 01:36:48 +01:00
d87df74834 add another preset 2026-02-20 01:36:24 +01:00
199646cae9 allow changing the target block 2026-02-20 01:26:59 +01:00
11ae406073 add another preset 2026-02-20 01:22:06 +01:00
f8ac60f6a6 implement state editing 2026-02-20 00:58:01 +01:00
ca83d5724f fix BlockIterator always starting at pos 0 (even if there's no Block there) 2026-02-20 00:07:42 +01:00
a48a6caefc make window resizable 2026-02-19 23:10:16 +01:00
53a38e9cf3 update physics engine parameters 2026-02-19 22:05:46 +01:00
b873a1e9d7 enable parallel build 2026-02-19 22:05:07 +01:00
d7ed12f6f9 move board state presets to separate file 2026-02-19 21:20:13 +01:00
0f131e2504 fix incorrectly added state transition on board reset 2026-02-19 21:19:17 +01:00
55ff0f3490 rebuild the repulsion force grid every n frames 2026-02-18 23:12:02 +01:00
1dad350f7d add winning states to menu panel 2026-02-18 21:43:13 +01:00
d92391271f add winning conditions and ability to mark them in the graph 2026-02-18 20:27:22 +01:00
47628d06ad add menu pane at the top 2026-02-18 13:30:59 +01:00
7faa8ecdb7 allow to restrict block movement to principal block directions + add more samples 2026-02-18 03:16:13 +01:00
e2e75204ef parallelize repulsion forces using openmp 2026-02-18 02:08:46 +01:00
43c9a5b715 implement uniform grid for repulsion forces 2026-02-18 01:21:22 +01:00
47fcea6bcb implement klotski graph closure solving + improve camera controls (panning) 2026-02-18 00:53:42 +01:00
039d96eee3 replace manual 3d-2d projection with orbital camera 2026-02-17 22:17:19 +01:00
8d5a6a827c add basic input handling for klotski board/graph + populate graph based on klotski moves 2026-02-17 15:12:32 +01:00
46 changed files with 7036 additions and 1251 deletions

56
.clang-format Normal file
View File

@ -0,0 +1,56 @@
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignOperands: false
AlignTrailingComments: true
AllowShortBlocksOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: true
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: false
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterStruct: true
AfterUnion: true
AfterExternBlock: false
BeforeCatch: true
BeforeElse: false
BeforeLambdaBody: true
BeforeWhile: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBraces: Custom
ColumnLimit: 100
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseBlocks: true
IndentGotoLabels: false
IndentPPDirectives: BeforeHash
IndentWidth: 4
InsertNewlineAtEOF: true
MacroBlockBegin: ''
MacroBlockEnd: ''
PointerAlignment: Left
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
TabWidth: 4
...

145
.clang-tidy Normal file
View File

@ -0,0 +1,145 @@
# Generated from CLion Inspection settings
---
Checks: '-*,
bugprone-argument-comment,
bugprone-assert-side-effect,
bugprone-bad-signal-to-kill-thread,
bugprone-branch-clone,
bugprone-copy-constructor-init,
bugprone-dangling-handle,
bugprone-dynamic-static-initializers,
bugprone-fold-init-type,
bugprone-forward-declaration-namespace,
bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-integer-division,
bugprone-lambda-function-name,
bugprone-macro-parentheses,
bugprone-macro-repeated-side-effects,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-misplaced-widening-cast,
bugprone-move-forwarding-reference,
bugprone-multiple-statement-macro,
bugprone-no-escape,
bugprone-parent-virtual-call,
bugprone-posix-return,
bugprone-reserved-identifier,
bugprone-sizeof-container,
bugprone-sizeof-expression,
bugprone-spuriously-wake-up-functions,
bugprone-string-constructor,
bugprone-string-integer-assignment,
bugprone-string-literal-with-embedded-nul,
bugprone-suspicious-enum-usage,
bugprone-suspicious-include,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-suspicious-memory-comparison,
bugprone-suspicious-realloc-usage,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-self-assignment,
bugprone-unused-raii,
bugprone-unused-return-value,
bugprone-use-after-move,
bugprone-virtual-near-miss,
cert-dcl21-cpp,
cert-dcl58-cpp,
cert-err34-c,
cert-err52-cpp,
cert-err60-cpp,
cert-flp30-c,
cert-msc50-cpp,
cert-msc51-cpp,
cert-str34-c,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-slicing,
google-default-arguments,
google-runtime-operator,
hicpp-exception-baseclass,
hicpp-multiway-paths-covered,
misc-misplaced-const,
misc-new-delete-overloads,
misc-non-copyable-objects,
misc-throw-by-value-catch-by-reference,
misc-unconventional-assign-operator,
misc-uniqueptr-reset-release,
modernize-avoid-bind,
modernize-concat-nested-namespaces,
modernize-deprecated-headers,
modernize-deprecated-ios-base-aliases,
modernize-loop-convert,
modernize-make-shared,
modernize-make-unique,
modernize-pass-by-value,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-auto-ptr,
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-auto,
modernize-use-bool-literals,
modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-use-nodiscard,
modernize-use-noexcept,
modernize-use-nullptr,
modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
mpi-buffer-deref,
mpi-type-mismatch,
openmp-use-default-none,
performance-faster-string-find,
performance-for-range-copy,
performance-implicit-conversion-in-loop,
performance-inefficient-algorithm,
performance-inefficient-string-concatenation,
performance-inefficient-vector-operation,
performance-move-const-arg,
performance-move-constructor-init,
performance-no-automatic-move,
performance-noexcept-move-constructor,
performance-trivially-destructible,
performance-type-promotion-in-math-fn,
performance-unnecessary-copy-initialization,
performance-unnecessary-value-param,
portability-simd-intrinsics,
readability-avoid-const-params-in-decls,
readability-const-return-type,
readability-container-size-empty,
readability-convert-member-functions-to-static,
readability-delete-null-pointer,
readability-deleted-default,
readability-inconsistent-declaration-parameter-name,
readability-make-member-function-const,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-redundant-function-ptr-dereference,
readability-redundant-smartptr-get,
readability-redundant-string-cstr,
readability-redundant-string-init,
readability-simplify-subscript-expr,
readability-static-accessed-through-instance,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-use-anyofallof'

View File

@ -1 +0,0 @@
./cmake-build-release/.clangd

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
.cache .cache
cmake-build-debug cmake-build-debug
cmake-build-release cmake-build-release
/platform
/result
/.gdb_history
/valgrind.log
.idea

View File

@ -1,19 +1,113 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
project(MassSprings) project(MassSprings)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(raylib REQUIRED) if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
include_directories(include) option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF)
option(DISABLE_TRACY "Disable the Tracy profiler client" ON)
option(DISABLE_TESTS "Disable building and running tests" ON)
add_executable(masssprings # Headers + Sources
src/main.cpp set(SOURCES
src/backward.cpp
src/graph_distances.cpp
src/input_handler.cpp
src/mass_spring_system.cpp
src/octree.cpp
src/orbit_camera.cpp
src/renderer.cpp src/renderer.cpp
src/mass_springs.cpp src/state_manager.cpp
src/klotski.cpp src/threaded_physics.cpp
src/user_interface.cpp
src/puzzle.cpp
) )
target_include_directories(masssprings PUBLIC ${RAYLIB_CPP_INCLUDE_DIR}) # Libraries
target_link_libraries(masssprings PUBLIC raylib) find_package(raylib REQUIRED)
find_package(Boost REQUIRED)
set(LIBS raylib Boost::headers)
set(FLAGS "")
if(WIN32)
list(APPEND LIBS opengl32 gdi32 winmm)
endif()
include(FetchContent)
if(NOT DISABLE_BACKWARD)
find_package(Backward REQUIRED)
list(APPEND LIBS Backward::Backward)
list(APPEND FLAGS BACKWARD)
endif()
if(NOT DISABLE_TRACY)
FetchContent_Declare(tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG v0.11.1
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(tracy)
option(TRACY_ENABLE "" ON)
option(TRACY_ON_DEMAND "" ON)
list(APPEND LIBS TracyClient)
list(APPEND FLAGS TRACY)
endif()
# 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_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -Ofast -march=native")
message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}")
message("-- CMAKE_C_FLAGS_RELEASE: ${CMAKE_C_FLAGS_RELEASE}")
message("-- CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
message("-- CMAKE_CXX_FLAGS_DEBUG: ${CMAKE_CXX_FLAGS_DEBUG}")
message("-- CMAKE_CXX_FLAGS_RELEASE: ${CMAKE_CXX_FLAGS_RELEASE}")
# Main target
add_executable(masssprings src/main.cpp ${SOURCES})
target_include_directories(masssprings PRIVATE include)
target_link_libraries(masssprings PRIVATE ${LIBS})
target_compile_definitions(masssprings PRIVATE ${FLAGS})
# Testing sources
if(NOT DISABLE_TESTS AND NOT WIN32)
enable_testing()
FetchContent_Declare(Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.13.0
)
FetchContent_MakeAvailable(Catch2)
set(TEST_SOURCES
test/bits.cpp
)
add_executable(tests ${TEST_SOURCES} ${SOURCES})
target_include_directories(tests PRIVATE include)
target_link_libraries(tests Catch2::Catch2WithMain raylib)
include(Catch)
catch_discover_tests(tests)
endif()
# LTO
#if(NOT WIN32)
include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error)
if(supported)
message(STATUS "IPO / LTO enabled")
set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(STATUS "IPO / LTO not supported")
endif()
#endif()

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# MassSprings - Puzzle Board State Space Explorer
All combinations of pieces reachable from an initial puzzle are explored, the resulting puzzle state-space is visualized as a force-directed graph.
The graph layout is calculated iteratively using a mass-spring-system with additional pairwise repulsive forces simulated using Barnes-Hut.
![](screenshot.png)
## Running
Requirements:
- Directory `fonts`
- Directory `shader`
- Preset file `default.puzzle` (optional)
Run `nix run git+https://gitea.local.chriphost.de/christoph/cpp-masssprings` from the working directory containing the listed requirements.

View File

@ -1 +1 @@
./cmake-build-release/compile_commands.json ./cmake-build-debug/compile_commands.json

30
default.puzzle Normal file
View File

@ -0,0 +1,30 @@
# RushHour 1
S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ _ _ 1x3} {_ _ _ _ _ _} {_ _ 1x2 2X1 _ _} {_ _ _ 1x2 2x1 _} {1x2 _ 1x2 _ 2x1 _} {_ _ _ 3x1 _ _}]
# RushHour 2
S:[6x6] G:[4,2] M:[R] B:[{1x2 3x1 _ _ 1x2 1x3} {_ 3x1 _ _ _ _} {2X1 _ 1x2 1x2 1x2 _} {2x1 _ _ _ _ _} {_ _ _ 1x2 2x1 _} {_ _ _ _ 2x1 _}]
# RushHour 3
S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ 1x2 _ _} {1x2 2x1 _ _ _ 1x2} {_ 2X1 _ 1x2 1x2 _} {2x1 _ 1x2 _ _ 1x2} {_ _ _ 2x1 _ _} {_ 2x1 _ 2x1 _ _}]
# RushHour 4
S:[6x6] G:[4,2] M:[R] B:[{1x3 2x1 _ _ 1x2 _} {_ 1x2 1x2 _ _ 1x3} {_ _ _ 2X1 _ _} {3x1 _ _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {2x1 _ _ 2x1 _ _}]
# 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 _ _ _}]
# 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 _}]
# Dad's Puzzler
S:[4x5] G:[0,3] M:[F] B:[{2X2 _ 2x1 _} {_ _ 2x1 _} {1x1 1x1 _ _} {1x2 1x2 2x1 _} {_ _ 2x1 _}]
# Nine Blocks
S:[4x5] G:[0,3] M:[F] B:[{1x2 1x2 _ _} {_ _ 2x1 _} {1x2 1x2 2x1 _} {_ _ 2X2 _} {1x1 1x1 _ _}]
# Quzzle
S:[4x5] G:[2,0] M:[F] B:[{2X2 _ 2x1 _} {_ _ 1x2 1x2} {_ _ _ _} {1x2 2x1 _ 1x1} {_ 2x1 _ 1x1}]
# Thin Klotski
S:[4x5] G:[1,4] M:[F] B:[{1x2 _ 2X1 _} {_ 2x2 _ 1x1} {_ _ _ 1x1} {2x2 _ 1x1 1x1} {_ _ 1x1 1x1}]
# Fat Klotski
S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ 1x1} {1x1 _ _ 1x2} {1x1 2x2 _ _} {1x1 _ _ _} {1x1 1x1 2x1 _}]
# Klotski
S:[4x5] G:[1,3] M:[F] B:[{1x2 2X2 _ 1x2} {_ _ _ _} {1x2 2x1 _ 1x2} {_ 1x1 1x1 _} {1x1 _ _ 1x1}]
# Century
S:[4x5] G:[1,3] M:[F] B:[{1x1 2X2 _ 1x1} {1x2 _ _ 1x2} {_ 1x2 _ _} {1x1 _ _ 1x1} {2x1 _ 2x1 _}]
# Super Century
S:[4x5] G:[1,3] M:[F] B:[{1x2 1x1 1x1 1x1} {_ 1x2 2X2 _} {1x2 _ _ _} {_ 2x1 _ 1x1} {_ 2x1 _ _}]
# Supercompo
S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]

119
flake.lock generated
View File

@ -1,66 +1,5 @@
{ {
"nodes": { "nodes": {
"clj-nix": {
"inputs": {
"devshell": "devshell",
"nix-fetcher-data": "nix-fetcher-data",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1770563144,
"narHash": "sha256-pddc5NHWPRYmQm723SLTjXVNDi4VnMOWqVbTOkfOE9k=",
"owner": "jlesquembre",
"repo": "clj-nix",
"rev": "b439ecd3eb92737f56330c4395c2d0eba0a4dbdd",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "clj-nix",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768818222,
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide",
"repo": "devshell",
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@ -79,28 +18,6 @@
"type": "github" "type": "github"
} }
}, },
"nix-fetcher-data": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1755022803,
"narHash": "sha256-/QtBdVfZlrRJW5enUoWlBE2wrLXJBMJ45X0rZh0jiaU=",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"rev": "9da3926b1459d6ff15268072d1c51351b82811b9",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1770843696, "lastModified": 1770843696,
@ -115,44 +32,10 @@
"type": "indirect" "type": "indirect"
} }
}, },
"nixpkgs-lib": {
"locked": {
"lastModified": 1717284937,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"root": { "root": {
"inputs": { "inputs": {
"clj-nix": "clj-nix",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1770865833,
"narHash": "sha256-oiARqnlvaW6pVGheVi4ye6voqCwhg5hCcGish2ZvQzI=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c8cfbe26238638e2f3a2c0ae7e8d240f5e4ded85",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
} }
}, },
"systems": { "systems": {

601
flake.nix
View File

@ -4,282 +4,371 @@ rec {
inputs = { inputs = {
nixpkgs.url = "nixpkgs"; # Use nixpkgs from system registry nixpkgs.url = "nixpkgs"; # Use nixpkgs from system registry
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
clj-nix.url = "github:jlesquembre/clj-nix";
clj-nix.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
flake-utils, flake-utils,
rust-overlay,
clj-nix,
}: }:
# Create a shell (and possibly package) for each possible system, not only x86_64-linux # Create a shell (and possibly package) for each possible system, not only x86_64-linux
flake-utils.lib.eachDefaultSystem (system: let flake-utils.lib.eachDefaultSystem (
pkgs = import nixpkgs { system: let
inherit system; pkgs = import nixpkgs {
config.allowUnfree = true; inherit system;
overlays = [ config.allowUnfree = true;
rust-overlay.overlays.default overlays = [];
]; };
}; inherit (pkgs) lib stdenv;
inherit (pkgs) lib stdenv;
# =========================================================================================== windowsPkgs = import nixpkgs {
# Define custom dependencies inherit system;
# =========================================================================================== crossSystem = {
config = "x86_64-w64-mingw32";
# 64 bit C/C++ compilers that don't collide (use the same libc) };
bintools = pkgs.wrapBintoolsWith { config.allowUnfree = true;
bintools = pkgs.bintools.bintools; # Unwrapped bintools
libc = pkgs.glibc;
};
gcc = lib.hiPrio (pkgs.wrapCCWith {
cc = pkgs.gcc.cc; # Unwrapped gcc
libc = pkgs.glibc;
bintools = bintools;
});
clang = pkgs.wrapCCWith {
cc = pkgs.clang.cc; # Unwrapped clang
libc = pkgs.glibc;
bintools = bintools;
};
# Multilib C/C++ compilers that don't collide (use the same libc)
# bintools_multilib = pkgs.wrapBintoolsWith {
# bintools = pkgs.bintools.bintools; # Unwrapped bintools
# libc = pkgs.glibc_multi;
# };
# gcc_multilib = pkgs.hiPrio (pkgs.wrapCCWith {
# cc = pkgs.gcc.cc; # Unwrapped gcc
# libc = pkgs.glibc_multi;
# bintools = bintools_multilib;
# });
# clang_multilib = pkgs.wrapCCWith {
# cc = pkgs.clang.cc; # Unwrapped clang
# libc = pkgs.glibc_multi;
# bintools = bintools_multilib;
# };
# Raylib CPP wrapper
raylib-cpp = stdenv.mkDerivation {
pname = "raylib-cpp";
version = "5.5.0-unstable-2025-11-12";
src = pkgs.fetchFromGitHub {
owner = "RobLoach";
repo = "raylib-cpp";
rev = "21b0d0f57a09a7f741d20b7157f440ae87f02c76";
hash = "sha256-P9x6Zc5t648gR7oYXe38PEX/a4oh4PfuVCnjT0vC10k=";
}; };
# autoPatchelfHook is needed for appendRunpaths # ===========================================================================================
nativeBuildInputs = with pkgs; [ # Define custom dependencies
cmake # ===========================================================================================
# autoPatchelfHook
];
buildInputs = with pkgs; [ raygui = stdenv.mkDerivation (finalAttrs: {
raylib pname = "raygui";
glfw version = "4.0-unstable-2026-02-24";
SDL2
];
propagatedBuildInputs = with pkgs; [ src = pkgs.fetchFromGitHub {
libGLU owner = "raysan5";
libx11 repo = "raygui";
]; rev = "5788707b6b7000343c14653b1ad3b971ca0597e4";
hash = "sha256-wKylPeNw7wO5xuTfnp1OYETQ78EPlr4NU9erbmIFgjE=";
};
cmakeFlags = [ patches = [./raygui.patch];
"-DBUILD_RAYLIB_CPP_EXAMPLES=OFF"
"-DBUILD_TESTING=OFF"
# Point CMake to the nixpkgs raylib so it doesn't try to fetch its own
"-Draylib_DIR=${pkgs.raylib}/lib/cmake/raylib"
];
};
# =========================================================================================== dontBuild = true;
# Specify dependencies installPhase = ''
# https://nixos.org/manual/nixpkgs/stable/#ssec-stdenv-dependencies-overview runHook preInstall
# Just for a "nix develop" shell, buildInputs can be used for everything.
# ===========================================================================================
# Add dependencies to nativeBuildInputs if they are executed during the build: mkdir -p $out/{include,lib/pkgconfig}
# - Those which are needed on $PATH during the build, for example cmake and pkg-config
# - Setup hooks, for example makeWrapper
# - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl
nativeBuildInputs = with pkgs; [
# Languages:
# bintools
gcc
# clang
# bintools_multilib
# gcc_multilib
# clang_multilib
# C/C++: install -Dm644 src/raygui.h $out/include/raygui.h
gdb
valgrind
# gnumake
cmake
# pkg-config
# clang-tools
# compdb
# pprof
gprof2dot
kdePackages.kcachegrind
];
# Add dependencies to buildInputs if they will end up copied or linked into the final output or otherwise used at runtime: cat <<EOF > $out/lib/pkgconfig/raygui.pc
# - Libraries used by compilers, for example zlib prefix=$out
# - Interpreters needed by patchShebangs for scripts which are installed, which can be the case for e.g. perl includedir=$out/include
buildInputs = with pkgs; [
# C/C++:
# boost
# sfml
raylib
# raylib-cpp
# tinyobjloader
# gperftools
];
# ===========================================================================================
# Define buildable + installable packages
# ===========================================================================================
# package = stdenv.mkDerivation {
# inherit nativeBuildInputs buildInputs;
# pname = "";
# version = "1.0.0";
# src = ./.;
#
# installPhase = ''
# mkdir -p $out/bin
# mv ./BINARY $out/bin
# '';
# };
in rec {
# Provide package for "nix build"
# defaultPackage = package;
# defaultApp = flake-utils.lib.mkApp {
# drv = defaultPackage;
# };
# Provide environment for "nix develop" Name: raygui
devShell = pkgs.mkShell { Description: Simple and easy-to-use immediate-mode gui library
inherit nativeBuildInputs buildInputs; URL: https://github.com/raysan5/raygui
name = description; Version: ${finalAttrs.version}
Cflags: -I"{includedir}"
EOF
# ========================================================================================= runHook postInstall
# Define environment variables
# =========================================================================================
# Custom dynamic libraries:
# LD_LIBRARY_PATH = builtins.concatStringsSep ":" [
# # Rust Bevy GUI app:
# # "${pkgs.xorg.libX11}/lib"
# # "${pkgs.xorg.libXcursor}/lib"
# # "${pkgs.xorg.libXrandr}/lib"
# # "${pkgs.xorg.libXi}/lib"
# # "${pkgs.libGL}/lib"
#
# # JavaFX app:
# # "${pkgs.libGL}/lib"
# # "${pkgs.gtk3}/lib"
# # "${pkgs.glib.out}/lib"
# # "${pkgs.xorg.libXtst}/lib"
# ];
# Dynamic libraries from buildinputs:
# LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs;
# =========================================================================================
# Define shell environment
# =========================================================================================
# Setup the shell when entering the "nix develop" environment (bash script).
shellHook = let
mkCmakeScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-${typeLower}.fish" ''
cd $FLAKE_PROJECT_ROOT
echo "Removing build directory ./cmake-build-${typeLower}/"
rm -rf ./cmake-build-${typeLower}
echo "Creating build directory"
mkdir cmake-build-${typeLower}
cd cmake-build-${typeLower}
echo "Running cmake"
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE="${type}" \
..
echo "Generating .clangd"
echo "CompileFlags:" >> .clangd
echo " Add:" >> .clangd
echo " - \"-I${pkgs.raylib}/include\"" >> .clangd
echo "Linking compile_commands.json"
cd ..
ln -sf ./cmake-build-${typeLower}/compile_commands.json ./compile_commands.json
echo "Linking .clangd"
ln -sf ./cmake-build-${typeLower}/.clangd ./.clangd
'';
cmakeDebug = mkCmakeScript "Debug";
cmakeRelease = mkCmakeScript "Release";
mkBuildScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-build.fish" ''
cd $FLAKE_PROJECT_ROOT/cmake-build-${typeLower}
echo "Running cmake"
cmake --build .
'';
buildDebug = mkBuildScript "Debug";
buildRelease = mkBuildScript "Release";
# Use this to specify commands that should be ran after entering fish shell
initProjectShell = pkgs.writers.writeFish "init-shell.fish" ''
echo "Entering \"${description}\" environment..."
# Determine the project root, used e.g. in cmake scripts
set -g -x FLAKE_PROJECT_ROOT (git rev-parse --show-toplevel)
# Rust Bevy:
# abbr -a build-release-windows "CARGO_FEATURE_PURE=1 cargo xwin build --release --target x86_64-pc-windows-msvc"
# C/C++:
abbr -a cmake-debug "${cmakeDebug}"
abbr -a cmake-release "${cmakeRelease}"
abbr -a build-debug "${buildDebug}"
abbr -a build-release "${buildRelease}"
# Clojure:
# abbr -a clojure-deps "deps-lock --lein"
# Python:
# abbr -a run "python ./app/main.py"
# abbr -a profile "py-spy record -o profile.svg -- python ./app/main.py && firefox profile.svg"
# abbr -a ptop "py-spy top -- python ./app/main.py"
''; '';
in });
builtins.concatStringsSep "\n" [
# Launch into pure fish shell thread-pool = stdenv.mkDerivation {
'' pname = "thread-pool";
exec "$(type -p fish)" -C "source ${initProjectShell} && abbr -a menu '${pkgs.bat}/bin/bat "${initProjectShell}"'" version = "5.1.0";
''
src = pkgs.fetchFromGitHub {
owner = "bshoshany";
repo = "thread-pool";
rev = "bd4533f1f70c2b975cbd5769a60d8eaaea1d2233";
hash = "sha256-/RMo5pe9klgSWmoqBpHMq2lbJsnCxMzhsb3ZPsw3aZw=";
};
# Header-only library
dontBuild = true;
installPhase = ''
mkdir -p $out
mv ./include $out/include
'';
};
# ===========================================================================================
# Specify dependencies
# https://nixos.org/manual/nixpkgs/stable/#ssec-stdenv-dependencies-overview
# Just for a "nix develop" shell, buildInputs can be used for everything.
# ===========================================================================================
# Add dependencies to nativeBuildInputs if they are executed during the build:
# - Those which are needed on $PATH during the build, for example cmake and pkg-config
# - Setup hooks, for example makeWrapper
# - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl
nativeBuildInputs = with pkgs; [
# Languages:
binutils
gcc
# C/C++:
gdb
valgrind
# gnumake
cmake
ninja
# cling
# pkg-config
# clang-tools
# compdb
# pprof
# gprof2dot
perf
hotspot
kdePackages.kcachegrind
gdbgui
massif-visualizer
heaptrack
# renderdoc
];
# Add dependencies to buildInputs if they will end up copied or linked into the final output or otherwise used at runtime:
# - Libraries used by compilers, for example zlib
# - Interpreters needed by patchShebangs for scripts which are installed, which can be the case for e.g. perl
buildInputs = with pkgs; [
# C/C++:
raylib
raygui
thread-pool
boost
# Debugging
tracy-wayland
backward-cpp
libbfd
catch2_3
];
# ===========================================================================================
# Define buildable + installable packages
# ===========================================================================================
package = stdenv.mkDerivation rec {
inherit buildInputs;
pname = "masssprings";
version = "1.0.0";
src = ./.;
nativeBuildInputs = with pkgs; [
gcc
cmake
]; ];
};
}); cmakeFlags = [
"-DDISABLE_TRACY=On"
"-DDISABLE_BACKWARD=On"
"-DDISABLE_TESTS=On"
];
hardeningDisable = ["all"];
preConfigure = ''
unset NIX_ENFORCE_NO_NATIVE
'';
installPhase = ''
mkdir -p $out/bin
cp ./${pname} $out/bin/
cp $src/default.puzzle $out/bin/
cp -r $src/fonts $out/bin/fonts
cp -r $src/shader $out/bin/shader
'';
};
windowsPackage = windowsPkgs.stdenv.mkDerivation rec {
pname = "masssprings";
version = "1.0.0";
src = ./.;
# nativeBuildInputs must be from the build-platform (not cross)
# so we use "pkgs" here, not "windowsPkgs"
nativeBuildInputs = with pkgs; [
cmake
];
buildInputs = with windowsPkgs; [
raylib
raygui
thread-pool
boost
];
cmakeFlags = [
"-DCMAKE_SYSTEM_NAME=Windows"
"-DDISABLE_TRACY=On"
"-DDISABLE_BACKWARD=On"
];
installPhase = ''
mkdir -p $out/bin
cp ./${pname}.exe $out/bin/
cp $src/default.puzzle $out/bin/
cp -r $src/fonts $out/bin/fonts
cp -r $src/shader $out/bin/shader
'';
};
in rec {
# Provide packages for "nix build" and "nix run"
packages = {
default = package;
windows = windowsPackage;
};
apps.default = flake-utils.lib.mkApp {drv = package;};
# Provide environment for "nix develop"
devShells = {
default = pkgs.mkShell {
inherit nativeBuildInputs buildInputs;
name = description;
# =========================================================================================
# Define environment variables
# =========================================================================================
# Custom dynamic libraries:
# LD_LIBRARY_PATH = builtins.concatStringsSep ":" [
# # Rust Bevy GUI app:
# # "${pkgs.xorg.libX11}/lib"
# # "${pkgs.xorg.libXcursor}/lib"
# # "${pkgs.xorg.libXrandr}/lib"
# # "${pkgs.xorg.libXi}/lib"
# # "${pkgs.libGL}/lib"
#
# # JavaFX app:
# # "${pkgs.libGL}/lib"
# # "${pkgs.gtk3}/lib"
# # "${pkgs.glib.out}/lib"
# # "${pkgs.xorg.libXtst}/lib"
# ];
# Dynamic libraries from buildinputs:
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs;
# =========================================================================================
# Define shell environment
# =========================================================================================
# Setup the shell when entering the "nix develop" environment (bash script).
shellHook = let
mkCmakeScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-${typeLower}.fish" ''
cd $FLAKE_PROJECT_ROOT
# set -g -x CC ${pkgs.clang}/bin/clang
# set -g -x CXX ${pkgs.clang}/bin/clang++
echo "Removing build directory ./cmake-build-${typeLower}/"
rm -rf ./cmake-build-${typeLower}
echo "Creating build directory"
mkdir cmake-build-${typeLower}
cd cmake-build-${typeLower}
echo "Running cmake"
cmake -G "Ninja" \
-DCMAKE_BUILD_TYPE="${type}" \
..
echo "Linking compile_commands.json"
cd ..
ln -sf ./cmake-build-${typeLower}/compile_commands.json ./compile_commands.json
'';
cmakeDebug = mkCmakeScript "Debug";
cmakeRelease = mkCmakeScript "Release";
mkBuildScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-build.fish" ''
cd $FLAKE_PROJECT_ROOT/cmake-build-${typeLower}
echo "Running cmake"
NIX_ENFORCE_NO_NATIVE=0 cmake --build . -j$(nproc)
'';
buildDebug = mkBuildScript "Debug";
buildRelease = mkBuildScript "Release";
# Use this to specify commands that should be ran after entering fish shell
initProjectShell = pkgs.writers.writeFish "init-shell.fish" ''
echo "Entering \"${description}\" environment..."
# Determine the project root, used e.g. in cmake scripts
set -g -x FLAKE_PROJECT_ROOT (git rev-parse --show-toplevel)
# C/C++:
abbr -a cmake-debug "${cmakeDebug}"
abbr -a cmake-release "${cmakeRelease}"
abbr -a build-debug "${buildDebug}"
abbr -a build-release "${buildRelease}"
abbr -a debug "${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release "${buildRelease} && ./cmake-build-release/masssprings"
abbr -a debug-clean "${cmakeDebug} && ${buildDebug} && ./cmake-build-debug/masssprings"
abbr -a release-clean "${cmakeRelease} && ${buildRelease} && ./cmake-build-release/masssprings"
abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings"
abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data"
abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/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 runtests "${buildDebug} && ./cmake-build-debug/tests"
abbr -a runclion "clion ./CMakeLists.txt 2>/dev/null 1>&2 & disown;"
'';
in
builtins.concatStringsSep "\n" [
# Launch into pure fish shell
''
exec "$(type -p fish)" -C "source ${initProjectShell} && abbr -a menu '${pkgs.bat}/bin/bat "${initProjectShell}"'"
''
];
};
# TODO: Can't get renderdoc in FHS to work
# FHS environment for renderdoc. Access with "nix develop .#renderdoc".
# https://ryantm.github.io/nixpkgs/builders/special/fhs-environments
# renderdoc =
# (pkgs.buildFHSEnv {
# name = "renderdoc-env";
#
# targetPkgs = pkgs:
# with pkgs; [
# # RenderDoc
# renderdoc
#
# # Build tools
# gcc
# cmake
#
# # Raylib
# raylib
# libGL
# mesa
#
# # X11
# libx11
# libxcursor
# libxrandr
# libxinerama
# libxi
# libxext
# libxfixes
#
# # Wayland
# wayland
# wayland-protocols
# libxkbcommon
# ];
#
# runScript = "fish";
#
# profile = ''
# '';
# }).env;
};
}
);
} }

BIN
fonts/MonoSpace.ttf Normal file

Binary file not shown.

BIN
fonts/SpaceMono.ttf Normal file

Binary file not shown.

View File

@ -1,24 +1,75 @@
#ifndef __CONFIG_HPP_ #ifndef CONFIG_HPP_
#define __CONFIG_HPP_ #define CONFIG_HPP_
#include <raylib.h> #include <raylib.h>
constexpr int WIDTH = 800; #define THREADPOOL // Enable physics threadpool
constexpr int HEIGHT = 800;
constexpr float VERTEX_SIZE = 50.0; // Gets set by CMake
constexpr Color VERTEX_COLOR = GREEN; // #define BACKWARD // Enable pretty stack traces
constexpr Color EDGE_COLOR = DARKGREEN; // #define TRACY // Enable tracy profiling support
constexpr float SIM_SPEED = 4.0; // Window
constexpr float ROTATION_SPEED = 1.0; constexpr int INITIAL_WIDTH = 600;
constexpr float CAMERA_DISTANCE = 2.2; constexpr int INITIAL_HEIGHT = 600;
constexpr int MENU_HEIGHT = 300;
constexpr int POPUP_WIDTH = 450;
constexpr int POPUP_HEIGHT = 150;
constexpr float DEFAULT_SPRING_CONSTANT = 1.5; // Menu
constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1; constexpr int MENU_PAD = 5;
constexpr float DEFAULT_REST_LENGTH = 0.5; constexpr int BUTTON_PAD = 12;
constexpr int MENU_ROWS = 7;
constexpr int MENU_COLS = 3;
constexpr const char* FONT = "fonts/SpaceMono.ttf";
constexpr int FONT_SIZE = 26;
constexpr int BOARD_PADDING = 5; // Camera Controls
constexpr int BLOCK_PADDING = 5; constexpr float CAMERA_FOV = 90.0;
constexpr float FOV_SPEED = 1.0;
constexpr float MIN_FOV = 10.0;
constexpr float MAX_FOV = 180.0;
constexpr float CAMERA_DISTANCE = 20.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float MIN_CAMERA_DISTANCE = 2.0;
constexpr float MAX_CAMERA_DISTANCE = 2000.0;
constexpr float ZOOM_MULTIPLIER = 4.0;
constexpr float PAN_SPEED = 2.0;
constexpr float PAN_MULTIPLIER = 10.0;
constexpr float ROT_SPEED = 1.0;
constexpr float CAMERA_SMOOTH_SPEED = 15.0;
#endif // Physics Engine
constexpr float TARGET_UPS = 90; // How often to update physics
constexpr float TIMESTEP = 1.0 / TARGET_UPS; // Update interval in seconds
constexpr float SIM_SPEED = 4.0; // How large each update should be
constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_CONSTANT = 5.0; // Mass spring system
constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system
constexpr float REST_LENGTH = 3.0; // Mass spring system
constexpr float VERLET_DAMPENING = 0.05; // [0, 1]
constexpr float BH_FORCE = 2.5; // Barnes-Hut [1.0, 3.0]
constexpr float THETA = 0.8; // Barnes-Hut [0.5, 1.0]
constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0]
// Graph Drawing
constexpr Color EDGE_COLOR = DARKBLUE;
constexpr float VERTEX_SIZE = 0.5;
static const Color VERTEX_COLOR = Fade(BLUE, 0.5);
constexpr Color VERTEX_VISITED_COLOR = DARKBLUE;
constexpr Color VERTEX_PATH_COLOR = GREEN;
constexpr Color VERTEX_TARGET_COLOR = RED;
constexpr Color VERTEX_START_COLOR = ORANGE;
constexpr Color VERTEX_CURRENT_COLOR = PURPLE;
constexpr int DRAW_VERTICES_LIMIT = 1000000;
// Klotski Drawing
constexpr int BOARD_PADDING = 10;
constexpr Color BOARD_COLOR_WON = DARKGREEN;
constexpr Color BOARD_COLOR_RESTRICTED = GRAY;
constexpr Color BOARD_COLOR_FREE = RAYWHITE;
constexpr Color BLOCK_COLOR = DARKBLUE;
constexpr Color TARGET_BLOCK_COLOR = RED;
constexpr Color WALL_COLOR = BLACK;
#endif

View File

@ -0,0 +1,24 @@
#ifndef DISTANCE_HPP_
#define DISTANCE_HPP_
#include <cstddef>
#include <vector>
class graph_distances
{
public:
std::vector<int> distances; // distances[n] = distance from node n to target
std::vector<size_t> parents; // parents[n] = next node on the path from node n to target
std::vector<size_t> nearest_targets; // nearest_target[n] = closest target node to node n
public:
auto clear() -> void;
[[nodiscard]] auto empty() const -> bool;
auto calculate_distances(size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<size_t>& targets) -> void;
[[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector<size_t>;
};
#endif

177
include/input_handler.hpp Normal file
View File

@ -0,0 +1,177 @@
#ifndef INPUT_HPP_
#define INPUT_HPP_
#include "orbit_camera.hpp"
#include "state_manager.hpp"
#include <functional>
#include <queue>
#include <raylib.h>
#include <raymath.h>
struct show_ok_message
{
std::string title;
std::string message;
};
struct show_yes_no_message
{
std::string title;
std::string message;
std::function<void()> on_yes;
};
struct show_save_preset_window
{};
using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>;
class input_handler
{
struct generic_handler
{
std::function<void(input_handler&)> handler;
};
struct mouse_handler : generic_handler
{
MouseButton button;
};
struct keyboard_handler : generic_handler
{
KeyboardKey key;
};
private:
std::vector<generic_handler> generic_handlers;
std::vector<mouse_handler> mouse_pressed_handlers;
std::vector<mouse_handler> mouse_released_handlers;
std::vector<keyboard_handler> key_pressed_handlers;
std::vector<keyboard_handler> key_released_handlers;
public:
state_manager& state;
orbit_camera& camera;
std::queue<ui_command> ui_commands;
bool disable = false;
// Block selection
int hov_x = -1;
int hov_y = -1;
int sel_x = 0;
int sel_y = 0;
// Editing
bool editing = false;
bool has_block_add_xy = false;
int block_add_x = -1;
int block_add_y = -1;
// Graph display
bool mark_path = false;
bool mark_solutions = false;
bool connect_solutions = false;
// Camera
bool camera_lock = true;
bool camera_mass_center_lock = false;
bool camera_panning = false;
bool camera_rotating = false;
// Mouse dragging
Vector2 mouse = Vector2Zero();
Vector2 last_mouse = Vector2Zero();
public:
input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera)
{
init_handlers();
}
input_handler(const input_handler& copy) = delete;
auto operator=(const input_handler& copy) -> input_handler& = delete;
input_handler(input_handler&& move) = delete;
auto operator=(input_handler&& move) -> input_handler& = delete;
private:
auto init_handlers() -> void;
public:
// Helpers
[[nodiscard]] auto mouse_in_menu_pane() const -> bool;
[[nodiscard]] auto mouse_in_board_pane() const -> bool;
[[nodiscard]] auto mouse_in_graph_pane() const -> bool;
// Mouse actions
auto mouse_hover() -> void;
auto camera_start_pan() -> void;
auto camera_pan() const -> void;
auto camera_stop_pan() -> void;
auto camera_start_rotate() -> void;
auto camera_rotate() const -> void;
auto camera_stop_rotate() -> void;
auto camera_zoom() const -> void;
auto camera_fov() const -> void;
auto select_block() -> void;
auto start_add_block() -> void;
auto clear_add_block() -> void;
auto add_block() -> void;
auto remove_block() -> void;
auto place_goal() const -> void;
// Key actions
auto toggle_camera_lock() -> void;
auto toggle_camera_mass_center_lock() -> void;
auto toggle_camera_projection() const -> void;
auto move_block_nor() -> void;
auto move_block_wes() -> void;
auto move_block_sou() -> void;
auto move_block_eas() -> void;
auto print_state() const -> void;
auto load_previous_preset() -> void;
auto load_next_preset() -> void;
auto goto_starting_state() -> void;
auto populate_graph() const -> void;
auto clear_graph() -> void;
auto toggle_mark_solutions() -> void;
auto toggle_connect_solutions() -> void;
auto toggle_mark_path() -> void;
auto goto_optimal_next_state() const -> void;
auto goto_most_distant_state() const -> void;
auto goto_closest_target_state() const -> void;
auto goto_previous_state() const -> void;
auto toggle_restricted_movement() const -> void;
auto toggle_target_block() const -> void;
auto toggle_wall_block() const -> void;
auto remove_board_column() const -> void;
auto add_board_column() const -> void;
auto remove_board_row() const -> void;
auto add_board_row() const -> void;
auto toggle_editing() -> void;
auto clear_goal() const -> void;
auto save_preset() -> void;
// General
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_released_handler(MouseButton button,
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 handle_input() -> void;
};
#endif

View File

@ -1,237 +0,0 @@
#ifndef __KLOTSKI_HPP_
#define __KLOTSKI_HPP_
#include <array>
#include <cstddef>
#include <format>
#include <iostream>
#include <string>
#include <vector>
// #define DBG_PRINT
enum Direction {
NOR = 1 << 0,
EAS = 1 << 1,
SOU = 1 << 2,
WES = 1 << 3,
};
// A block is represented as a 2-digit string "wh", where w is the block width
// and h the block height.
// The target block (to remove from the board) is represented as a 2-letter
// string "xy", where x is the block width and y the block height and
// width/height are represented by [abcdefghi] (~= [123456789]).
class Block {
public:
int x;
int y;
int width;
int height;
bool target;
public:
Block(int x, int y, int width, int height, bool target)
: x(x), y(y), width(width), height(height), target(target) {
if (x < 0 || x + width >= 10 || y < 0 || y + height >= 10) {
std::cerr << "Block must fit on a 9x9 board!" << std::endl;
exit(1);
}
#ifdef DBG_PRINT
std::cout << ToString() << std::endl;
#endif
}
Block(int x, int y, std::string block) : x(x), y(y) {
if (block == "..") {
this->x = 0;
this->y = 0;
width = 0;
height = 0;
target = false;
return;
}
const std::array<char, 9> chars{'a', 'b', 'c', 'd', 'e',
'f', 'g', 'h', 'i'};
target = false;
for (const char c : chars) {
if (block.contains(c)) {
target = true;
break;
}
}
if (target) {
width = static_cast<int>(block.at(0)) - static_cast<int>('a') + 1;
height = static_cast<int>(block.at(1)) - static_cast<int>('a') + 1;
} else {
width = std::stoi(block.substr(0, 1));
height = std::stoi(block.substr(1, 1));
}
if (x < 0 || x + width >= 10 || y < 0 || y + height >= 10) {
std::cerr << "Block must fit on a 9x9 board!" << std::endl;
exit(1);
}
if (block.length() != 2) {
std::cerr << "Block representation must have length [2]!" << std::endl;
exit(1);
}
#ifdef DBG_PRINT
std::cout << "At: (" << x << ", " << y << "), Size: (" << width << ", "
<< height << "), Target: " << target << std::endl;
#endif
}
Block(const Block &copy)
: x(copy.x), y(copy.y), width(copy.width), height(copy.height),
target(copy.target) {}
Block &operator=(const Block &copy) = delete;
Block(Block &&move)
: x(move.x), y(move.y), width(move.width), height(move.height),
target(move.target) {}
Block &operator=(Block &&move) = delete;
bool operator==(const Block &other) {
return x == other.x && y == other.y && width && other.width &&
target == other.target;
}
bool operator!=(const Block &other) { return !(*this == other); }
~Block() {}
public:
auto Hash() -> int;
static auto Invalid() -> Block const;
auto IsValid() -> bool;
auto ToString() -> std::string;
auto Covers(int xx, int yy) -> bool;
auto Collides(const Block &other) -> bool;
};
// A state is represented by a string "WxH:blocks", where W is the board width,
// H is the board height and blocks is an enumeration of each of the board's
// cells, with each cell being a 2-letter or 2-digit block representation (a 3x3
// board would have a string representation with length 4 + 3*3 * 2).
// The board's cells are enumerated from top-left to bottom-right with each
// block's pivot being its top-left corner.
class State {
public:
int width;
int height;
std::string state;
// https://en.cppreference.com/w/cpp/iterator/input_iterator.html
class BlockIterator {
public:
using difference_type = std::ptrdiff_t;
using value_type = Block;
private:
const State &state;
int current_pos;
public:
BlockIterator(const State &state) : state(state), current_pos(0) {}
BlockIterator(const State &state, int current_pos)
: state(state), current_pos(current_pos) {}
Block operator*() const {
return Block(current_pos % state.width, current_pos / state.width,
state.state.substr(current_pos * 2 + 4, 2));
}
BlockIterator &operator++() {
do {
current_pos++;
} while (state.state.substr(current_pos * 2 + 4, 2) == "..");
return *this;
}
bool operator==(const BlockIterator &other) {
return state == other.state && current_pos == other.current_pos;
}
bool operator!=(const BlockIterator &other) { return !(*this == other); }
};
public:
State(int width, int height)
: width(width), height(height),
state(std::format("{}x{}:{}", width, height,
std::string(width * height * 2, '.'))) {
if (width <= 0 || width >= 10 || height <= 0 || height >= 10) {
std::cerr << "State width/height must be in [1, 9]!" << std::endl;
exit(1);
}
#ifdef DBG_PRINT
std::cout << "State(" << width << ", " << height << "): \"" << state << "\""
<< std::endl;
#endif
}
State(std::string state)
: width(std::stoi(state.substr(0, 1))),
height(std::stoi(state.substr(2, 1))), state(state) {
if (width <= 0 || width >= 10 || height <= 0 || height >= 10) {
std::cerr << "State width/height must be in [1, 9]!" << std::endl;
exit(1);
}
if (state.length() != width * height * 2 + 4) {
std::cerr
<< "State representation must have length [width * height * 2 + 4]!"
<< std::endl;
exit(1);
}
}
State(const State &copy)
: width(copy.width), height(copy.height), state(copy.state) {}
State &operator=(const State &copy) = delete;
State(State &&move)
: width(move.width), height(move.height), state(std::move(move.state)) {}
State &operator=(State &&move) = delete;
bool operator==(const State &other) const { return state == other.state; }
bool operator!=(const State &other) const { return !(*this == other); }
BlockIterator begin() { return BlockIterator(*this); }
BlockIterator end() { return BlockIterator(*this, width * height); }
~State() {}
public:
auto Hash() -> int;
auto AddBlock(Block block) -> bool;
auto GetBlock(int x, int y) -> Block;
auto RemoveBlock(int x, int y) -> bool;
auto MoveBlockAt(int x, int y, Direction dir) -> bool;
auto GetNextStates() -> std::vector<State>;
};
#endif

View File

@ -0,0 +1,114 @@
#ifndef MASS_SPRING_SYSTEM_HPP_
#define MASS_SPRING_SYSTEM_HPP_
#include "octree.hpp"
#include "util.hpp"
#include "config.hpp"
#include <raylib.h>
#include <raymath.h>
#ifdef THREADPOOL
#if defined(_WIN32)
#define NOGDI // All GDI defines and routines
#define NOUSER // All USER defines and routines
#endif
#define BS_THREAD_POOL_NATIVE_EXTENSIONS
#include <BS_thread_pool.hpp>
#if defined(_WIN32) // raylib uses these names as function parameters
#undef near
#undef far
#endif
#endif
class mass_spring_system
{
public:
class mass
{
public:
Vector3 position = Vector3Zero();
Vector3 previous_position = Vector3Zero(); // for verlet integration
Vector3 velocity = Vector3Zero();
Vector3 force = Vector3Zero();
public:
mass() = delete;
explicit mass(const Vector3 _position)
: position(_position), previous_position(_position) {}
public:
auto clear_force() -> void;
auto calculate_velocity(float delta_time) -> void;
auto calculate_position(float delta_time) -> void;
auto verlet_update(float delta_time) -> void;
};
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 auto calculate_spring_force(mass& _a, mass& _b) -> void;
};
private:
#ifdef THREADPOOL
BS::thread_pool<> threads;
#endif
public:
octree tree;
// This is the main ownership of all the states/masses/springs.
std::vector<mass> masses;
std::vector<spring> springs;
public:
mass_spring_system()
#ifdef THREADPOOL
: threads(std::thread::hardware_concurrency() - 1, set_thread_name)
#endif
{
infoln("Using Barnes-Hut + Octree repulsion force calculation.");
#ifdef THREADPOOL
infoln("Thread-pool: {} threads.", threads.get_thread_count());
#else
infoln("Thread-pool: Disabled.");
#endif
}
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;
private:
#ifdef THREADPOOL
static auto set_thread_name(size_t idx) -> void;
#endif
auto build_octree() -> void;
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_forces() -> void;
auto calculate_repulsion_forces() -> void;
auto verlet_update(float delta_time) -> void;
auto center_masses() -> void;
};
#endif

View File

@ -1,123 +0,0 @@
#ifndef __MASS_SPRINGS_HPP_
#define __MASS_SPRINGS_HPP_
#include <cstddef>
#include <raylib.h>
#include <raymath.h>
#include <vector>
class Mass {
public:
float mass;
Vector3 position;
Vector3 previous_position; // for verlet integration
Vector3 velocity;
Vector3 force;
bool fixed;
public:
Mass(float mass, Vector3 position, bool fixed)
: mass(mass), position(position), previous_position(position),
velocity(Vector3Zero()), force(Vector3Zero()), fixed(fixed) {}
Mass(const Mass &copy)
: mass(copy.mass), position(copy.position),
previous_position(copy.previous_position), velocity(copy.velocity),
force(copy.force), fixed(copy.fixed) {};
Mass &operator=(const Mass &copy) = delete;
Mass(Mass &&move)
: mass(move.mass), position(move.position),
previous_position(move.previous_position), velocity(move.velocity),
force(move.force), fixed(move.fixed) {};
Mass &operator=(Mass &&move) = delete;
~Mass() {}
public:
auto ClearForce() -> void;
auto CalculateVelocity(const float delta_time) -> void;
auto CalculatePosition(const float delta_time) -> void;
auto VerletUpdate(const float delta_time) -> void;
};
using MassList = std::vector<Mass>;
class Spring {
public:
Mass &massA;
Mass &massB;
float spring_constant;
float dampening_constant;
float rest_length;
public:
Spring(Mass &massA, Mass &massB, float spring_constant,
float dampening_constant, float rest_length)
: massA(massA), massB(massB), spring_constant(spring_constant),
dampening_constant(dampening_constant), rest_length(rest_length) {}
Spring(const Spring &copy)
: massA(copy.massA), massB(copy.massB),
spring_constant(copy.spring_constant),
dampening_constant(copy.dampening_constant),
rest_length(copy.rest_length) {};
Spring &operator=(const Spring &copy) = delete;
Spring(Spring &&move)
: massA(move.massA), massB(move.massB),
spring_constant(move.spring_constant),
dampening_constant(move.dampening_constant),
rest_length(move.rest_length) {}
Spring &operator=(Spring &&move) = delete;
~Spring() {}
public:
auto CalculateSpringForce() -> void;
};
using SpringList = std::vector<Spring>;
class MassSpringSystem {
public:
MassList masses;
SpringList springs;
public:
MassSpringSystem() {};
MassSpringSystem(const MassSpringSystem &copy) = delete;
MassSpringSystem &operator=(const MassSpringSystem &copy) = delete;
MassSpringSystem(MassSpringSystem &move) = delete;
MassSpringSystem &operator=(MassSpringSystem &&move) = delete;
~MassSpringSystem() {};
public:
auto AddMass(float mass, Vector3 position, bool fixed) -> void;
auto GetMass(const size_t index) -> Mass &;
auto AddSpring(int massA, int massB, float spring_constant,
float dampening_constant, float rest_length) -> void;
auto GetSpring(const size_t index) -> Spring &;
auto ClearForces() -> void;
auto CalculateSpringForces() -> void;
auto EulerUpdate(const float delta_time) -> void;
auto VerletUpdate(const float delta_time) -> void;
};
#endif

52
include/octree.hpp Normal file
View File

@ -0,0 +1,52 @@
#ifndef OCTREE_HPP_
#define OCTREE_HPP_
#include <array>
#include <raylib.h>
#include <raymath.h>
#include <vector>
class octree
{
class node
{
public:
Vector3 mass_center = Vector3Zero();
float mass_total = 0.0;
Vector3 box_min = Vector3Zero(); // area start
Vector3 box_max = Vector3Zero(); // area end
std::array<int, 8> children = {-1, -1, -1, -1, -1, -1, -1, -1};
int mass_id = -1;
bool leaf = true;
public:
[[nodiscard]] auto child_count() const -> int;
};
public:
static constexpr int MAX_DEPTH = 20;
std::vector<node> nodes;
public:
octree() = default;
octree(const octree& copy) = delete;
auto operator=(const octree& copy) -> octree& = delete;
octree(octree&& move) = delete;
auto operator=(octree&& move) -> octree& = delete;
public:
auto create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int;
[[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int;
[[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
-> std::pair<Vector3, Vector3>;
auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void;
[[nodiscard]] auto calculate_force(int node_idx, const Vector3& pos) const -> Vector3;
};
#endif

31
include/orbit_camera.hpp Normal file
View File

@ -0,0 +1,31 @@
#ifndef CAMERA_HPP_
#define CAMERA_HPP_
#include "config.hpp"
#include <raylib.h>
#include <raymath.h>
class orbit_camera
{
public:
Vector3 position = Vector3Zero();
Vector3 target = Vector3Zero();
float distance = CAMERA_DISTANCE;
float fov = CAMERA_FOV;
CameraProjection projection = CAMERA_PERSPECTIVE;
float angle_x = 0.0;
float angle_y = 0.0;
Camera camera = Camera{Vector3(0, 0, -distance), target, Vector3(0, 1.0, 0), fov, projection};
public:
auto rotate(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;
};
#endif

482
include/puzzle.hpp Normal file
View File

@ -0,0 +1,482 @@
#ifndef PUZZLE_HPP_
#define PUZZLE_HPP_
#include "util.hpp"
#include <array>
#include <cstddef>
#include <format>
#include <functional>
#include <ranges>
#include <string>
#include <vector>
#include <bits/fs_fwd.h>
/*
* 8x8 board
* -> 64 (sizes) * 2 (target) * 2 (movable) blocks = 1 Byte
*
* 1. Encode the position inside the board (max 64 cells)
* -> 64 (slots) * 1 Byte (block) = 64 Byte
* 2. Encode the position inside the block (max 64 cells)
* -> 64 (blocks) * 2 Byte (size + pos) = 128 Byte
* 3. Encode the position inside the block (with 15 block limit)
* -> 15 (blocks) * 2 (block) = 30 Byte
*
* Store board size + restricted: +1 Byte
* Store target position: +1 Byte
*
* => Limit to 15 blocks max and use option 3. (4x uint64_t)
*
*/
class puzzle
{
public:
/*
* A block is represented as uint16_t.
* It stores its position, width, height and if it's the target block or immovable.
*/
class block
{
friend class puzzle;
private:
static constexpr uint16_t INVALID = 0x8000;
static constexpr uint8_t IMMOVABLE_S = 0;
static constexpr uint8_t IMMOVABLE_E = 0;
static constexpr uint8_t TARGET_S = 1;
static constexpr uint8_t TARGET_E = 1;
static constexpr uint8_t WIDTH_S = 2;
static constexpr uint8_t WIDTH_E = 4;
static constexpr uint8_t HEIGHT_S = 5;
static constexpr uint8_t HEIGHT_E = 7;
static constexpr uint8_t X_S = 8;
static constexpr uint8_t X_E = 10;
static constexpr uint8_t Y_S = 11;
static constexpr uint8_t Y_E = 13;
/*
* Memory layout:
*
* 154 321 098 765 432 1 0
* B0 YYY XXX HHH WWW T I
* ---------- -----------
* Byte 1 Byte 0
*
* Store the y-position at the most significant bits to obtain a row-major ordering when comparing reprs.
* The block at (0, 0) will be the smallest, the block at (width, height) the largest,
* the block at (1, 0) will be smaller than the block at (0, 1), all independent of block size.
* Then, the block with size (1, 1) will be smaller than the block with size (2, 2),
* the block with size (1, 0) - horizontal - will be smaller than the block with size (0, 1) - vertical.
*
* To mark if a block is empty, the first B bit is set to 1. This is required,
* since otherwise uint16_t{0} would be a valid block. This also makes empty blocks sorted last.
*/
uint16_t repr;
public:
// Produces an invalid block, for usage with std::array<block, MaxBlocks>
block()
: repr(INVALID) {}
explicit block(const uint16_t _repr)
: repr(_repr) {}
block(const int x, const int y, const int w, const int h, const bool t = false, const bool i = false)
: repr(create_repr(x, y, w, h, t, i))
{
if (x < 0 || x + w > MAX_WIDTH || y < 0 || y + h > MAX_HEIGHT) {
throw std::invalid_argument("Block size out of bounds");
}
if (t && i) {
throw std::invalid_argument("Target block can't be immovable");
}
}
auto operator==(const block other) const -> bool
{
return repr == other.repr;
}
auto operator!=(const block other) const -> bool
{
return repr != other.repr;
}
auto operator<(const block other) const -> bool
{
return repr < other.repr;
}
auto operator<=(const block other) const -> bool
{
return repr <= other.repr;
}
auto operator>(const block other) const -> bool
{
return repr > other.repr;
}
auto operator>=(const block other) const -> bool
{
return repr >= other.repr;
}
private:
[[nodiscard]] static auto create_repr(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool t = false,
bool i = false) -> uint16_t;
// Repr setters
[[nodiscard]] auto set_x(uint8_t x) const -> block;
[[nodiscard]] auto set_y(uint8_t y) const -> block;
[[nodiscard]] auto set_width(uint8_t width) const -> block;
[[nodiscard]] auto set_height(uint8_t height) const -> block;
[[nodiscard]] auto set_target(bool target) const -> block;
[[nodiscard]] auto set_immovable(bool immovable) const -> block;
public:
[[nodiscard]] auto unpack_repr() const -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>;
// Repr getters
[[nodiscard]] auto get_x() const -> uint8_t;
[[nodiscard]] auto get_y() const -> uint8_t;
[[nodiscard]] auto get_width() const -> uint8_t;
[[nodiscard]] auto get_height() const -> uint8_t;
[[nodiscard]] auto get_target() const -> bool;
[[nodiscard]] auto get_immovable() const -> bool;
// Util
[[nodiscard]] auto valid() const -> bool;
[[nodiscard]] auto principal_dirs() const -> uint8_t;
[[nodiscard]] auto covers(int _x, int _y) const -> bool;
[[nodiscard]] auto collides(block b) const -> bool;
};
public:
static constexpr uint8_t MAX_BLOCKS = 15;
static constexpr uint8_t MIN_WIDTH = 3;
static constexpr uint8_t MIN_HEIGHT = 3;
static constexpr uint8_t MAX_WIDTH = 8;
static constexpr uint8_t MAX_HEIGHT = 8;
private:
static constexpr uint16_t INVALID = 0x8000;
static constexpr uint8_t RESTRICTED_S = 0;
static constexpr uint8_t RESTRICTED_E = 0;
static constexpr uint8_t GOAL_X_S = 1;
static constexpr uint8_t GOAL_X_E = 3;
static constexpr uint8_t GOAL_Y_S = 4;
static constexpr uint8_t GOAL_Y_E = 6;
static constexpr uint8_t WIDTH_S = 7;
static constexpr uint8_t WIDTH_E = 9;
static constexpr uint8_t HEIGHT_S = 10;
static constexpr uint8_t HEIGHT_E = 12;
static constexpr uint8_t GOAL_S = 13;
static constexpr uint8_t GOAL_E = 13;
struct repr_cooked
{
/*
* Memory layout:
*
* 1543 210 98 7 654 321 0
* B0G HHH WW W YYY XXX R
* ---------- -----------
* Byte 1 Byte 0
*
* To mark if a puzzle is empty, the first B bit is set to 1.
* An extra bit is used to mark if the board has a goal, because we can't store MAX_WIDTH=8 in 3 bits.
*/
uint16_t meta;
// NOTE: For the hashes to work, this array needs to be sorted always.
// NOTE: This array might contain empty blocks at the end. The iterator handles this.
std::array<uint16_t, MAX_BLOCKS> blocks;
// repr_cooked() = delete;
// repr_cooked(const repr_cooked& copy) = delete;
// repr_cooked(repr_cooked&& move) = delete;
} __attribute__((packed));
/**
* With gcc, were allowed to acces the members arbitrarily, even if they're not active (not the ones last written):
* - https://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Structures-unions-enumerations-and-bit_002dfields-implementation.html#Structures-unions-enumerations-and-bit_002dfields-implementation
* - https://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Optimize-Options.html#Type_002dpunning
*/
union repr_u
{
// The representation split into meta information and the blocks array
repr_cooked cooked;
// For 15 blocks, we have sizeof(meta) + blocks.size() * sizeof(block) = 2 + 15 * 2 = 32 Bytes
std::array<uint64_t, 4> raw;
};
repr_u repr;
public:
// Produces an invalid puzzle, for usage with containers
puzzle()
: repr(repr_cooked{INVALID, invalid_blocks()}) {}
explicit puzzle(const uint16_t meta)
: repr(repr_cooked{meta, invalid_blocks()}) {}
// NOTE: This constructor does not sort the blocks and is only for state space generation
puzzle(const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>& meta,
const std::array<uint16_t, MAX_BLOCKS>& sorted_blocks)
: repr(repr_cooked{create_meta(meta), sorted_blocks}) {}
puzzle(const uint64_t byte_0, const uint64_t byte_1, const uint64_t byte_2, const uint64_t byte_3)
: repr(create_repr(byte_0, byte_1, byte_2, byte_3)) {}
puzzle(const uint16_t meta, const std::array<uint16_t, MAX_BLOCKS>& blocks)
: repr(repr_cooked{meta, blocks}) {}
puzzle(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r, const bool g)
: repr(create_repr(w, h, tx, ty, r, g, invalid_blocks()))
{
if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) {
throw std::invalid_argument("Board size out of bounds");
}
if (tx >= MAX_WIDTH || ty >= MAX_HEIGHT) {
throw std::invalid_argument("Goal out of bounds");
}
}
puzzle(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r, const bool g,
const std::array<uint16_t, MAX_BLOCKS>& b)
: repr(create_repr(w, h, tx, ty, r, g, b))
{
if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) {
throw std::invalid_argument("Board size out of bounds");
}
if (tx >= MAX_WIDTH || ty >= MAX_HEIGHT) {
throw std::invalid_argument("Goal out of bounds");
}
}
explicit puzzle(const std::string& string_repr)
: repr(create_repr(string_repr)) {}
public:
auto operator==(const puzzle& other) const -> bool
{
return repr.raw == other.repr.raw;
}
auto operator!=(const puzzle& other) const -> bool
{
return repr.raw != other.repr.raw;
}
auto operator<(const puzzle& other) const -> bool
{
// Start from MSB and go to LSB. If equal, check the next.
for (int i = 3; i >= 0; --i) {
if (repr.raw[i] < other.repr.raw[i]) {
return true;
}
if (repr.raw[i] > other.repr.raw[i]) {
return false;
}
}
// All are equal
return false;
}
auto operator<=(const puzzle& other) const -> bool
{
return *this < other || *this == other;
}
auto operator>(const puzzle& other) const -> bool
{
return !(*this <= other);
}
auto operator>=(const puzzle& other) const -> bool
{
return !(*this < other);
}
auto repr_view() const
{
return std::span<const uint16_t>(repr.cooked.blocks.data(), block_count());
}
auto block_view() const
{
return std::span<const uint16_t>(repr.cooked.blocks.data(), block_count()) | std::views::transform(
[](const uint16_t val)
{
return block(val);
});
}
template <typename T, typename... Rest>
static auto hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void;
private:
[[nodiscard]] static constexpr auto invalid_blocks() -> std::array<uint16_t, MAX_BLOCKS>
{
std::array<uint16_t, MAX_BLOCKS> blocks;
for (size_t i = 0; i < MAX_BLOCKS; ++i) {
blocks[i] = block::INVALID;
}
return blocks;
}
[[nodiscard]] static auto create_meta(const std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>& meta) -> uint16_t;
[[nodiscard]] static auto create_repr(uint8_t w, uint8_t h, uint8_t tx, uint8_t ty, bool r, bool g,
const std::array<uint16_t, MAX_BLOCKS>& b) -> repr_cooked;
[[nodiscard]] static auto create_repr(uint64_t byte_0, uint64_t byte_1, uint64_t byte_2,
uint64_t byte_3) -> repr_cooked;
[[nodiscard]] static auto create_repr(const std::string& string_repr) -> repr_cooked;
// Repr setters
[[nodiscard]] auto set_restricted(bool restricted) const -> puzzle;
[[nodiscard]] auto set_width(uint8_t width) const -> puzzle;
[[nodiscard]] auto set_height(uint8_t height) const -> puzzle;
[[nodiscard]] auto set_goal(bool goal) const -> puzzle;
[[nodiscard]] auto set_goal_x(uint8_t target_x) const -> puzzle;
[[nodiscard]] auto set_goal_y(uint8_t target_y) const -> puzzle;
[[nodiscard]] auto set_blocks(std::array<uint16_t, MAX_BLOCKS> blocks) const -> puzzle;
public:
// Repr getters
[[nodiscard]] auto unpack_meta() const -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, bool, bool>;
[[nodiscard]] auto get_restricted() const -> bool;
[[nodiscard]] auto get_width() const -> uint8_t;
[[nodiscard]] auto get_height() const -> uint8_t;
[[nodiscard]] auto get_goal() const -> bool;
[[nodiscard]] auto get_goal_x() const -> uint8_t;
[[nodiscard]] auto get_goal_y() const -> uint8_t;
// Util
[[nodiscard]] auto hash() const -> size_t;
[[nodiscard]] auto string_repr() const -> std::string;
[[nodiscard]] static auto try_parse_string_repr(const std::string& string_repr) -> std::optional<repr_cooked>;
[[nodiscard]] auto valid() const -> bool;
[[nodiscard]] auto try_get_invalid_reason() const -> std::optional<std::string>;
[[nodiscard]] auto block_count() const -> uint8_t;
[[nodiscard]] auto goal_reached() const -> bool;
[[nodiscard]] auto try_get_block(uint8_t x, uint8_t y) const -> std::optional<block>;
[[nodiscard]] auto try_get_target_block() const -> std::optional<block>;
[[nodiscard]] auto covers(uint8_t x, uint8_t y, uint8_t _w, uint8_t _h) const -> bool;
[[nodiscard]] auto covers(uint8_t x, uint8_t y) const -> bool;
[[nodiscard]] auto covers(block b) const -> bool;
// Editing
[[nodiscard]] auto toggle_restricted() const -> puzzle;
[[nodiscard]] auto try_set_goal(uint8_t x, uint8_t y) const -> std::optional<puzzle>;
[[nodiscard]] auto clear_goal() const -> puzzle;
[[nodiscard]] auto try_add_column() const -> std::optional<puzzle>;
[[nodiscard]] auto try_remove_column() const -> std::optional<puzzle>;
[[nodiscard]] auto try_add_row() const -> std::optional<puzzle>;
[[nodiscard]] auto try_remove_row() const -> std::optional<puzzle>;
[[nodiscard]] auto try_add_block(block b) const -> std::optional<puzzle>;
[[nodiscard]] auto try_remove_block(uint8_t x, uint8_t y) const -> std::optional<puzzle>;
[[nodiscard]] auto try_toggle_target(uint8_t x, uint8_t y) const -> std::optional<puzzle>;
[[nodiscard]] auto try_toggle_wall(uint8_t x, uint8_t y) const -> std::optional<puzzle>;
// Playing
[[nodiscard]] auto try_move_block_at(uint8_t x, uint8_t y, direction dir) const -> std::optional<puzzle>;
// Statespace
[[nodiscard]] auto try_move_block_at_fast(uint64_t bitmap, uint8_t block_idx,
direction dir) const -> std::optional<puzzle>;
static auto sorted_replace(std::array<uint16_t, MAX_BLOCKS> blocks, uint8_t idx,
uint16_t new_val) -> std::array<uint16_t, MAX_BLOCKS>;
auto blocks_bitmap() const -> uint64_t;
static inline auto bitmap_set_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> uint64_t;
static inline auto bitmap_get_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> bool;
static auto bitmap_clear_block(uint64_t bitmap, block b) -> uint64_t;
static auto bitmap_check_collision(uint64_t bitmap, block b) -> bool;
static auto bitmap_check_collision(uint64_t bitmap, block b, direction dir) -> bool;
template <typename F>
auto for_each_adjacent(F&& callback) const -> void
{
const uint64_t bitmap = blocks_bitmap();
const bool r = get_restricted();
for (uint8_t idx = 0; idx < MAX_BLOCKS; idx++) {
const block b = block(repr.cooked.blocks[idx]);
if (!b.valid()) {
break;
}
if (b.get_immovable()) {
continue;
}
const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes;
for (const direction d : {nor, eas, sou, wes}) {
if (dirs & d) {
if (auto moved = try_move_block_at_fast(bitmap, idx, d)) {
callback(*moved);
}
}
}
}
}
[[nodiscard]] auto explore_state_space() const
-> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>;
};
// Hash functions for sets and maps
struct puzzle_hasher
{
auto operator()(const puzzle& s) const noexcept -> size_t
{
return s.hash();
}
};
struct link_hasher
{
auto operator()(const std::pair<puzzle, puzzle>& s) const noexcept -> size_t
{
size_t h = 0;
if (s.first < s.second) {
puzzle::hash_combine(h, s.first, s.second);
} else {
puzzle::hash_combine(h, s.second, s.first);
}
return h;
}
};
struct link_equal_to
{
auto operator()(const std::pair<puzzle, puzzle>& a, const std::pair<puzzle, puzzle>& b) const noexcept -> bool
{
return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first);
}
};
template <typename T, typename... Rest>
auto puzzle::hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void
{
auto h = []<typename HashedType>(const HashedType& val) -> std::size_t
{
if constexpr (std::is_same_v<std::decay_t<HashedType>, puzzle>) {
return puzzle_hasher{}(val);
} else if constexpr (std::is_same_v<std::decay_t<HashedType>, std::pair<puzzle, puzzle>>) {
return link_hasher{}(val);
} else {
return std::hash<std::decay_t<HashedType>>{}(val);
}
};
seed ^= h(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
(hash_combine(seed, rest), ...);
}
#endif

View File

@ -1,64 +1,109 @@
#ifndef __RENDERER_HPP_ #ifndef RENDERER_HPP_
#define __RENDERER_HPP_ #define RENDERER_HPP_
#include "orbit_camera.hpp"
#include "config.hpp"
#include "input_handler.hpp"
#include "state_manager.hpp"
#include "user_interface.hpp"
#include <immintrin.h>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <rlgl.h>
#include <vector>
#include "klotski.hpp" class renderer
#include "mass_springs.hpp" {
using Edge3Set = std::vector<std::pair<Vector3, Vector3>>;
using Edge2Set = std::vector<std::pair<Vector2, Vector2>>;
using Vertex2Set =
std::vector<Vector3>; // Vertex2Set uses Vector3 to retain the z-coordinate
// for circle size adaptation
class Renderer {
private: private:
int width; const state_manager& state;
int height; const input_handler& input;
RenderTexture2D render_target; user_interface& gui;
RenderTexture2D klotski_target;
const orbit_camera& camera;
RenderTexture render_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
// TODO: Those should be moved to the user_interface.h
RenderTexture klotski_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Batching
std::vector<std::pair<Vector3, Vector3>> connections;
// Instancing
static constexpr int INSTANCE_COLOR_ATTR = 5;
std::vector<Matrix> transforms;
std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
Shader instancing_shader =
LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
unsigned int color_vbo_id = 0;
public: public:
Renderer(int width, int height) : width(width), height(height) { renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input,
render_target = LoadRenderTexture(width, height); user_interface& _gui)
klotski_target = LoadRenderTexture(width, height); : state(_state), input(_input), gui(_gui), camera(_camera)
} {
instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
GetShaderLocationAttrib(instancing_shader, "instanceTransform");
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] =
GetShaderLocation(instancing_shader, "viewPos");
Renderer(const Renderer &copy) = delete; // infoln("LOC vertexPosition: {}",
Renderer &operator=(const Renderer &copy) = delete; // rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
Renderer(Renderer &&move) = delete; // infoln("LOC instanceTransform: {}",
Renderer &operator=(Renderer &&move) = delete; // rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
// infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor"));
~Renderer() { // vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR;
UnloadRenderTexture(render_target); vertex_mat.shader = instancing_shader;
UnloadRenderTexture(klotski_target);
} transforms.reserve(DRAW_VERTICES_LIMIT);
colors.reserve(DRAW_VERTICES_LIMIT);
color_vbo_id = rlLoadVertexBuffer(colors.data(), DRAW_VERTICES_LIMIT * sizeof(Color), true);
rlEnableVertexArray(cube_instance.vaoId);
rlEnableVertexBuffer(color_vbo_id);
rlSetVertexAttribute(INSTANCE_COLOR_ATTR, 4, RL_UNSIGNED_BYTE, true, 0, 0);
rlEnableVertexAttribute(INSTANCE_COLOR_ATTR);
rlSetVertexAttributeDivisor(INSTANCE_COLOR_ATTR, 1);
rlDisableVertexBuffer();
rlDisableVertexArray();
}
renderer(const renderer& copy) = delete;
auto operator=(const renderer& copy) -> renderer& = delete;
renderer(renderer&& move) = delete;
auto operator=(renderer&& move) -> renderer& = delete;
~renderer()
{
UnloadRenderTexture(render_target);
UnloadRenderTexture(klotski_target);
UnloadRenderTexture(menu_target);
// Instancing
UnloadMaterial(vertex_mat);
UnloadMesh(cube_instance);
// I think the shader already gets unloaded with the material?
// UnloadShader(instancing_shader);
}
private: private:
auto Rotate(const Vector3 &a, const float cos_angle, const float sin_angle) auto update_texture_sizes() -> void;
-> Vector3;
auto Translate(const Vector3 &a, const float distance) -> Vector3; auto draw_mass_springs(const std::vector<Vector3>& masses) -> void;
auto draw_klotski() const -> void;
auto Project(const Vector3 &a) -> Vector2; auto draw_menu() const -> void;
auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
auto Map(const Vector2 &a) -> Vector2;
public: public:
auto Transform(Edge2Set &edges, Vertex2Set &vertices, auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count,
const MassSpringSystem &mass_springs, const float angle, size_t spring_count) -> void;
const float distance) -> void;
auto DrawMassSprings(const Edge2Set &edges, const Vertex2Set &vertices)
-> void;
auto DrawKlotski(State &state) -> void;
auto DrawTextures() -> void;
}; };
#endif #endif

153
include/state_manager.hpp Normal file
View File

@ -0,0 +1,153 @@
#ifndef STATE_MANAGER_HPP_
#define STATE_MANAGER_HPP_
#include "graph_distances.hpp"
#include "threaded_physics.hpp"
#include "puzzle.hpp"
#include <stack>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
class state_manager
{
private:
threaded_physics& physics;
std::string preset_file;
size_t current_preset = 0;
std::vector<puzzle> preset_states = {puzzle(4, 5, 0, 0, true, false)};
std::vector<std::string> preset_comments = {"# Empty"};
// State storage (store states twice for bidirectional lookup).
// Everything else should only store indices to state_pool.
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
boost::unordered_flat_map<puzzle, size_t, puzzle_hasher> state_indices; // Maps states to indices
std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices
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
std::vector<size_t> winning_path; // Ordered list of node indices leading to the nearest solved state
boost::unordered_flat_set<size_t> path_indices; // For faster lookup if a vertex is part of the path in renderer
std::vector<size_t> move_history; // Moves between the starting state and the current state
boost::unordered_flat_map<size_t, int> visit_counts; // How often each state was visited
size_t starting_state_index = 0;
size_t current_state_index = 0;
size_t previous_state_index = 0;
int total_moves = 0;
bool edited = false;
public:
state_manager(threaded_physics& _physics, const std::string& _preset_file)
: physics(_physics)
{
parse_preset_file(_preset_file);
load_preset(0);
}
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:
/**
* Inserts a board state into the state_manager and the physics system.
* States should only be inserted using this function to keep both systems in sync.
* The function checks for duplicates before insertion.
*
* @param state State to insert
* @return Index of insertion (or existing index if duplicate)
*/
auto synced_try_insert_state(const puzzle& state) -> size_t;
/**
* Inserts a state link into the state_manager and the physics system.
* Links should only be inserted using this function to keep both systems in sync.
* The function does not check for duplicates before insertion.
*
* @param first_index Index of the first linked state
* @param second_index Index of the second linked state
*/
auto synced_insert_link(size_t first_index, size_t second_index) -> void;
/**
* Inserts an entire statespace into the state_manager and the physics system.
* If inserting many states and links in bulk, this function should always be used
* to not stress the physics command mutex.
* The function does not check for duplicates before insertion.
*
* @param states List of states to insert
* @param _links List of links to insert
*/
auto synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void;
/**
* Clears all states and links (and related) from the state_manager and the physics system.
* Note that this leaves any dangling indices (e.g., current_state_index) in an invalid state.
*/
auto synced_clear_statespace() -> void;
public:
// Presets
auto parse_preset_file(const std::string& _preset_file) -> bool;
auto append_preset_file(const std::string& preset_name) -> bool;
auto load_preset(size_t preset) -> void;
auto load_previous_preset() -> void;
auto load_next_preset() -> void;
// Update current_state
auto update_current_state(const puzzle& p) -> void;
auto edit_starting_state(const puzzle& p) -> void;
auto goto_starting_state() -> void;
auto goto_optimal_next_state() -> void;
auto goto_previous_state() -> void;
auto goto_most_distant_state() -> void;
auto goto_closest_target_state() -> void;
// Update graph
auto populate_graph() -> void;
auto clear_graph_and_add_current(const puzzle& p) -> void;
auto clear_graph_and_add_current() -> void;
auto populate_winning_indices() -> void;
auto populate_node_target_distances() -> void;
auto populate_winning_path() -> void;
// Index mapping
[[nodiscard]] auto get_index(const puzzle& state) const -> size_t;
[[nodiscard]] auto get_current_index() const -> size_t;
[[nodiscard]] auto get_starting_index() const -> size_t;
[[nodiscard]] auto get_state(size_t index) const -> const puzzle&;
[[nodiscard]] auto get_current_state() const -> const puzzle&;
[[nodiscard]] auto get_starting_state() const -> const puzzle&;
// Access
[[nodiscard]] auto get_state_count() const -> size_t;
[[nodiscard]] auto get_target_count() const -> size_t;
[[nodiscard]] auto get_link_count() const -> size_t;
[[nodiscard]] auto get_path_length() const -> size_t;
[[nodiscard]] auto get_links() const -> const std::vector<std::pair<size_t, size_t>>&;
[[nodiscard]] auto get_winning_indices() const -> const boost::unordered_flat_set<size_t>&;
[[nodiscard]] auto get_visit_counts() const -> const boost::unordered_flat_map<size_t, int>&;
[[nodiscard]] auto get_winning_path() const -> const std::vector<size_t>&;
[[nodiscard]] auto get_path_indices() const -> const boost::unordered_flat_set<size_t>&;
[[nodiscard]] auto get_current_visits() const -> int;
[[nodiscard]] auto get_current_preset() const -> size_t;
[[nodiscard]] auto get_preset_count() const -> size_t;
[[nodiscard]] auto get_current_preset_comment() const -> const std::string&;
[[nodiscard]] auto has_history() const -> bool;
[[nodiscard]] auto has_distances() const -> bool;
[[nodiscard]] auto get_total_moves() const -> size_t;
[[nodiscard]] auto was_edited() const -> bool;
};
#endif

View File

@ -0,0 +1,95 @@
#ifndef PHYSICS_HPP_
#define PHYSICS_HPP_
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <queue>
#include <raylib.h>
#include <raymath.h>
#include <thread>
#include <variant>
#include <vector>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
class threaded_physics
{
struct add_mass {};
struct add_spring
{
size_t a;
size_t b;
};
struct clear_graph {};
using command = std::variant<add_mass, add_spring, clear_graph>;
struct physics_state
{
#ifdef TRACY
TracyLockable(std::mutex, command_mtx);
#else
std::mutex command_mtx;
#endif
std::queue<command> pending_commands;
#ifdef TRACY
TracyLockable(std::mutex, data_mtx);
#else
std::mutex data_mtx;
#endif
std::condition_variable_any data_ready_cnd;
std::condition_variable_any data_consumed_cnd;
Vector3 mass_center = Vector3Zero();
int ups = 0;
size_t mass_count = 0; // For debug
size_t spring_count = 0; // For debug
std::vector<Vector3> masses; // Read by renderer
bool data_ready = false;
bool data_consumed = true;
std::atomic<bool> running{true};
};
private:
std::thread physics;
public:
physics_state state;
public:
threaded_physics()
: physics(physics_thread, std::ref(state)) {}
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;
~threaded_physics()
{
state.running = false;
state.data_ready_cnd.notify_all();
state.data_consumed_cnd.notify_all();
physics.join();
}
private:
static auto physics_thread(physics_state& state) -> void;
public:
auto add_mass_cmd() -> void;
auto add_spring_cmd(size_t a, size_t b) -> void;
auto clear_cmd() -> void;
auto add_mass_springs_cmd(size_t num_masses, const std::vector<std::pair<size_t, size_t>>& springs) -> void;
};
#endif

186
include/user_interface.hpp Normal file
View File

@ -0,0 +1,186 @@
#ifndef GUI_HPP_
#define GUI_HPP_
#include "orbit_camera.hpp"
#include "config.hpp"
#include "input_handler.hpp"
#include "state_manager.hpp"
#include <raylib.h>
class user_interface
{
class grid
{
public:
int x;
int y;
int width;
int height;
int columns;
int rows;
const int padding;
public:
grid(const int _x, const int _y, const int _width, const int _height, const int _columns,
const int _rows, const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows),
padding(_padding)
{}
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) -> void;
auto update_bounds(int _x, int _y) -> void;
[[nodiscard]] auto bounds() 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(int _x, int _y, int _width, int _height) const
-> Rectangle;
};
struct style
{
int border_color_normal;
int base_color_normal;
int text_color_normal;
int border_color_focused;
int base_color_focused;
int text_color_focused;
int border_color_pressed;
int base_color_pressed;
int text_color_pressed;
int border_color_disabled;
int base_color_disabled;
int text_color_disabled;
};
struct default_style : style
{
int background_color;
int line_color;
int text_size;
int text_spacing;
int text_line_spacing;
int text_alignment_vertical;
int text_wrap_mode;
};
struct component_style : style
{
int border_width;
int text_padding;
int text_alignment;
};
private:
input_handler& input;
state_manager& state;
const orbit_camera& camera;
grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD);
grid board_grid =
grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().get_width(), state.get_current_state().get_height(), BOARD_PADDING);
grid 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);
// Windows
std::string message_title;
std::string message_message;
std::function<void()> yes_no_handler;
bool ok_message = false;
bool yes_no_message = false;
bool save_window = false;
std::array<char, 256> preset_name = {};
bool help_window = false;
public:
user_interface(input_handler& _input, state_manager& _state, const orbit_camera& _camera)
: input(_input), state(_state), camera(_camera)
{
init();
}
user_interface(const user_interface& copy) = delete;
auto operator=(const user_interface& copy) -> user_interface& = delete;
user_interface(user_interface&& move) = delete;
auto operator=(user_interface&& move) -> user_interface& = delete;
private:
static auto init() -> void;
static auto apply_color(style& style, Color color) -> void;
static auto apply_block_color(style& style, Color color) -> void;
static auto apply_text_color(style& style, Color color) -> void;
static auto get_default_style() -> default_style;
static auto set_default_style(const default_style& style) -> void;
static auto get_component_style(int component) -> component_style;
static auto set_component_style(int component, const component_style& style) -> void;
[[nodiscard]] static auto popup_bounds() -> Rectangle;
auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x, int y, int width, int height, const std::string& label,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label,
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label,
int* value, int min, int max, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_board_block(int x, int y, int width, int height, Color color,
bool enabled = true) const -> bool;
[[nodiscard]] auto window_open() const -> bool;
// Different menu sections
auto draw_menu_header(Color color) const -> void;
auto draw_graph_info(Color color) const -> void;
auto draw_graph_controls(Color color) const -> void;
auto draw_camera_controls(Color color) const -> void;
auto draw_puzzle_controls(Color color) const -> void;
auto draw_edit_controls(Color color) const -> void;
auto draw_menu_footer(Color color) -> void;
public:
static auto get_background_color() -> Color;
auto help_popup() -> void;
auto draw_save_preset_popup() -> void;
auto draw_ok_message_box() -> void;
auto draw_yes_no_message_box() -> void;
auto draw_main_menu() -> void;
auto draw_puzzle_board() -> void;
auto draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void;
auto draw(int fps, int ups, size_t mass_count, size_t spring_count) -> void;
};
#endif

164
include/util.hpp Normal file
View File

@ -0,0 +1,164 @@
#ifndef UTIL_HPP_
#define UTIL_HPP_
#include <iostream>
#include <raylib.h>
// Bit shifting + masking
template <class T>
requires std::unsigned_integral<T>
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>
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>
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>
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,
};
enum ctrl
{
reset = 0,
bold_bright = 1,
underline = 4,
inverse = 7,
bold_bright_off = 21,
underline_off = 24,
inverse_off = 27
};
enum fg
{
fg_black = 30,
fg_red = 31,
fg_green = 32,
fg_yellow = 33,
fg_blue = 34,
fg_magenta = 35,
fg_cyan = 36,
fg_white = 37
};
enum bg
{
bg_black = 40,
bg_red = 41,
bg_green = 42,
bg_yellow = 43,
bg_blue = 44,
bg_magenta = 45,
bg_cyan = 46,
bg_white = 47
};
// Output
inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream&
{
os << "(" << v.x << ", " << v.y << ")";
return os;
}
inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
{
os << "(" << v.x << ", " << v.y << ", " << v.z << ")";
return os;
}
inline auto ansi_bold_fg(const fg color) -> std::string
{
return std::format("\033[1;{}m", static_cast<int>(color));
}
inline auto ansi_reset() -> std::string
{
return "\033[0m";
}
// std::println doesn't work with mingw
template <typename... Args>
auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
}
template <typename... Args>
auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
}
template <typename... Args>
auto errln(std::format_string<Args...> fmt, Args&&... args) -> void
{
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) << std::format(
fmt, std::forward<Args>(args)...) << std::endl;
}
#endif

53
raygui.patch Normal file
View File

@ -0,0 +1,53 @@
diff --git a/src/raygui.h b/src/raygui.h
index 03e4879..6986498 100644
--- a/src/raygui.h
+++ b/src/raygui.h
@@ -787,8 +787,8 @@ RAYGUIAPI int GuiCheckBox(Rectangle bounds, const char *text, bool *checked);
RAYGUIAPI int GuiComboBox(Rectangle bounds, const char *text, int *active); // Combo Box control
RAYGUIAPI int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode); // Dropdown Box control
-RAYGUIAPI int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Spinner control
-RAYGUIAPI int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers
+RAYGUIAPI int GuiSpinner(Rectangle bounds, const char *text, const char *prefix, int *value, int minValue, int maxValue, bool editMode); // Spinner control
+RAYGUIAPI int GuiValueBox(Rectangle bounds, const char *text, const char *prefix, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers
RAYGUIAPI int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode); // Value box control for float values
RAYGUIAPI int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode); // Text Box control, updates input text
@@ -2989,7 +2989,7 @@ bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode)
*/
// Spinner control, returns selected value
-int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
+int GuiSpinner(Rectangle bounds, const char *text, const char *prefix, int *value, int minValue, int maxValue, bool editMode)
{
int result = 1;
GuiState state = guiState;
@@ -3045,7 +3045,7 @@ int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int
// Draw control
//--------------------------------------------------------------------
- result = GuiValueBox(valueBoxBounds, NULL, &tempValue, minValue, maxValue, editMode);
+ result = GuiValueBox(valueBoxBounds, NULL, prefix, &tempValue, minValue, maxValue, editMode);
// Draw value selector custom buttons
// NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values
@@ -3067,17 +3067,17 @@ int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int
// Value Box control, updates input text with numbers
// NOTE: Requires static variables: frameCounter
-int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
+int GuiValueBox(Rectangle bounds, const char *text, const char *prefix, int *value, int minValue, int maxValue, bool editMode)
{
#if !defined(RAYGUI_VALUEBOX_MAX_CHARS)
#define RAYGUI_VALUEBOX_MAX_CHARS 32
#endif
int result = 0;
GuiState state = guiState;
char textValue[RAYGUI_VALUEBOX_MAX_CHARS + 1] = { 0 };
- snprintf(textValue, RAYGUI_VALUEBOX_MAX_CHARS + 1, "%i", *value);
+ snprintf(textValue, RAYGUI_VALUEBOX_MAX_CHARS + 1, "%s%i", prefix, *value);
Rectangle textBounds = { 0 };
if (text != NULL)

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

View File

@ -0,0 +1,9 @@
#version 330
in vec4 fragColor;
out vec4 finalColor;
void main() {
// Advanced coloring. CG lecture really paying off now
finalColor = fragColor;
}

View File

@ -0,0 +1,14 @@
#version 330
layout(location=0) in vec3 vertexPosition;
layout(location=1) in mat4 instanceTransform;
layout(location=5) in vec4 instanceColor;
uniform mat4 mvp;
out vec4 fragColor;
void main() {
fragColor = instanceColor;
gl_Position = mvp * instanceTransform * vec4(vertexPosition, 1.0);
}

45
src/backward.cpp Normal file
View File

@ -0,0 +1,45 @@
// Pick your poison.
//
// On GNU/Linux, you have few choices to get the most out of your stack trace.
//
// By default, you get:
// - object filename
// - function name
//
// In order to add:
// - source filename
// - line and column numbers
// - source code snippet (assuming the file is accessible)
// Install one of the following libraries then uncomment one of the macro (or
// better, add the detection of the lib and the macro definition in your build
// system)
// - apt-get install libdw-dev ...
// - g++/clang++ -ldw ...
// #define BACKWARD_HAS_DW 1
// - apt-get install binutils-dev ...
// - g++/clang++ -lbfd ...
// #define BACKWARD_HAS_BFD 1
// - apt-get install libdwarf-dev ...
// - g++/clang++ -ldwarf ...
// #define BACKWARD_HAS_DWARF 1
// Regardless of the library you choose to read the debug information,
// for potentially more detailed stack traces you can use libunwind
// - apt-get install libunwind-dev
// - g++/clang++ -lunwind
// #define BACKWARD_HAS_LIBUNWIND 1
#ifdef BACKWARD
#include "backward.hpp"
namespace backward
{
SignalHandling sh;
} // namespace backward
#endif

73
src/graph_distances.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "graph_distances.hpp"
#include <queue>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto graph_distances::clear() -> void
{
distances.clear();
parents.clear();
nearest_targets.clear();
}
auto graph_distances::empty() const -> bool
{
return distances.empty() || parents.empty() || nearest_targets.empty();
}
auto graph_distances::calculate_distances(const size_t node_count,
const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<size_t>& targets) -> void
{
// Build a list of adjacent nodes to speed up BFS
std::vector<std::vector<size_t>> adjacency(node_count);
for (const auto& [from, to] : edges) {
adjacency[from].push_back(to);
adjacency[to].push_back(from);
}
distances = std::vector<int>(node_count, -1);
parents = std::vector<size_t>(node_count, -1);
nearest_targets = std::vector<size_t>(node_count, -1);
std::queue<size_t> queue;
for (size_t target : targets) {
distances[target] = 0;
nearest_targets[target] = target;
queue.push(target);
}
while (!queue.empty()) {
const size_t current = queue.front();
queue.pop();
for (size_t neighbor : adjacency[current]) {
if (distances[neighbor] == -1) {
// If distance is -1 we haven't visited the node yet
distances[neighbor] = distances[current] + 1;
parents[neighbor] = current;
nearest_targets[neighbor] = nearest_targets[current];
queue.push(neighbor);
}
}
}
}
auto graph_distances::get_shortest_path(const size_t source) const -> std::vector<size_t>
{
if (empty() || distances[source] == -1) {
// Unreachable
return {};
}
std::vector<size_t> path;
for (size_t n = source; n != static_cast<size_t>(-1); n = parents[n]) {
path.push_back(n);
}
return path;
}

617
src/input_handler.cpp Normal file
View File

@ -0,0 +1,617 @@
#include "input_handler.hpp"
#include "config.hpp"
#include <raylib.h>
auto input_handler::init_handlers() -> void
{
// The order matters if multiple handlers are registered to the same key
register_generic_handler(&input_handler::camera_pan);
register_generic_handler(&input_handler::camera_rotate);
register_generic_handler(&input_handler::camera_zoom);
register_generic_handler(&input_handler::camera_fov);
register_generic_handler(&input_handler::mouse_hover);
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::camera_start_pan);
register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::select_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_MIDDLE, &input_handler::place_goal);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block);
register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block);
register_mouse_released_handler(MOUSE_BUTTON_LEFT, &input_handler::camera_stop_pan);
register_mouse_released_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_stop_rotate);
register_key_pressed_handler(KEY_W, &input_handler::move_block_nor);
register_key_pressed_handler(KEY_D, &input_handler::move_block_eas);
register_key_pressed_handler(KEY_S, &input_handler::move_block_sou);
register_key_pressed_handler(KEY_A, &input_handler::move_block_wes);
register_key_pressed_handler(KEY_N, &input_handler::load_previous_preset);
register_key_pressed_handler(KEY_M, &input_handler::load_next_preset);
register_key_pressed_handler(KEY_R, &input_handler::goto_starting_state);
register_key_pressed_handler(KEY_V, &input_handler::goto_most_distant_state);
register_key_pressed_handler(KEY_B, &input_handler::goto_closest_target_state);
register_key_pressed_handler(KEY_SPACE, &input_handler::goto_optimal_next_state);
register_key_pressed_handler(KEY_BACKSPACE, &input_handler::goto_previous_state);
register_key_pressed_handler(KEY_G, &input_handler::populate_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_O, &input_handler::toggle_connect_solutions);
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_T, &input_handler::toggle_target_block);
register_key_pressed_handler(KEY_Y, &input_handler::toggle_wall_block);
register_key_pressed_handler(KEY_UP, &input_handler::add_board_row);
register_key_pressed_handler(KEY_RIGHT, &input_handler::add_board_column);
register_key_pressed_handler(KEY_DOWN, &input_handler::remove_board_row);
register_key_pressed_handler(KEY_LEFT, &input_handler::remove_board_column);
register_key_pressed_handler(KEY_X, &input_handler::clear_goal);
register_key_pressed_handler(KEY_P, &input_handler::print_state);
register_key_pressed_handler(KEY_S, &input_handler::save_preset); // + CTRL
register_key_pressed_handler(KEY_L, &input_handler::toggle_camera_lock);
register_key_pressed_handler(KEY_LEFT_ALT, &input_handler::toggle_camera_projection);
register_key_pressed_handler(KEY_U, &input_handler::toggle_camera_mass_center_lock);
}
auto input_handler::mouse_in_menu_pane() const -> bool
{
return mouse.y < MENU_HEIGHT;
}
auto input_handler::mouse_in_board_pane() const -> bool
{
return mouse.x < GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT;
}
auto input_handler::mouse_in_graph_pane() const -> bool
{
return mouse.x >= GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT;
}
auto input_handler::mouse_hover() -> void
{
last_mouse = mouse;
mouse = GetMousePosition();
}
auto input_handler::camera_start_pan() -> void
{
if (!mouse_in_graph_pane()) {
return;
}
camera_panning = true;
// Enable this if the camera should be pannable even when locked (releasing the lock in the
// process): camera_lock = false;
}
auto input_handler::camera_pan() const -> void
{
if (camera_panning) {
camera.pan(last_mouse, mouse);
}
}
auto input_handler::camera_stop_pan() -> void
{
camera_panning = false;
}
auto input_handler::camera_start_rotate() -> void
{
if (!mouse_in_graph_pane()) {
return;
}
camera_rotating = true;
}
auto input_handler::camera_rotate() const -> void
{
if (camera_rotating) {
camera.rotate(last_mouse, mouse);
}
}
auto input_handler::camera_stop_rotate() -> void
{
camera_rotating = false;
}
auto input_handler::camera_zoom() const -> void
{
if (!mouse_in_graph_pane() || IsKeyDown(KEY_LEFT_CONTROL) || camera.projection == CAMERA_ORTHOGRAPHIC) {
return;
}
const float wheel = GetMouseWheelMove();
if (IsKeyDown(KEY_LEFT_SHIFT)) {
camera.distance -= wheel * ZOOM_SPEED * ZOOM_MULTIPLIER;
} else {
camera.distance -= wheel * ZOOM_SPEED;
}
}
auto input_handler::camera_fov() const -> void
{
if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_LEFT_SHIFT)) {
return;
}
const float wheel = GetMouseWheelMove();
camera.fov -= wheel * FOV_SPEED;
}
auto input_handler::select_block() -> void
{
const puzzle& current = state.get_current_state();
if (current.try_get_block(hov_x, hov_y)) {
sel_x = hov_x;
sel_y = hov_y;
}
}
auto input_handler::start_add_block() -> void
{
const puzzle& current = state.get_current_state();
if (!editing || current.try_get_block(hov_x, hov_y) || has_block_add_xy || current.block_count() >=
puzzle::MAX_BLOCKS) {
return;
}
if (hov_x >= 0 && hov_x < current.get_width() && hov_y >= 0 && hov_y < current.get_height()) {
block_add_x = hov_x;
block_add_y = hov_y;
has_block_add_xy = true;
}
}
auto input_handler::clear_add_block() -> void
{
if (!editing || !has_block_add_xy) {
return;
}
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
}
auto input_handler::add_block() -> void
{
const puzzle& current = state.get_current_state();
if (!editing || current.try_get_block(hov_x, hov_y) || !has_block_add_xy || current.block_count() >=
puzzle::MAX_BLOCKS) {
return;
}
const int block_add_width = hov_x - block_add_x + 1;
const int block_add_height = hov_y - block_add_y + 1;
if (block_add_width <= 0 || block_add_height <= 0) {
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) {
const std::optional<puzzle>& next = current.try_add_block(
puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) {
sel_x = block_add_x;
sel_y = block_add_y;
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.edit_starting_state(*next);
}
}
}
auto input_handler::remove_block() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle::block>& b = current.try_get_block(hov_x, hov_y);
if (!editing || has_block_add_xy || !b) {
return;
}
const std::optional<puzzle>& next = current.try_remove_block(hov_x, hov_y);
if (!next) {
return;
}
// Reset selection if we removed the selected block
if (b->covers(sel_x, sel_y)) {
sel_x = 0;
sel_y = 0;
}
state.edit_starting_state(*next);
}
auto input_handler::place_goal() const -> void
{
const puzzle& current = state.get_current_state();
if (!editing || !current.covers(hov_x, hov_y)) {
return;
}
const std::optional<puzzle>& next = current.try_set_goal(hov_x, hov_y);
if (!next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::toggle_camera_lock() -> void
{
if (!camera_lock) {
camera_panning = false;
}
camera_lock = !camera_lock;
}
auto input_handler::toggle_camera_mass_center_lock() -> void
{
if (!camera_mass_center_lock) {
camera_lock = true;
camera_panning = false;
}
camera_mass_center_lock = !camera_mass_center_lock;
}
auto input_handler::toggle_camera_projection() const -> void
{
camera.projection = camera.projection == CAMERA_PERSPECTIVE ? CAMERA_ORTHOGRAPHIC : CAMERA_PERSPECTIVE;
}
auto input_handler::move_block_nor() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, nor);
if (!next) {
return;
}
sel_y--;
state.update_current_state(*next);
}
auto input_handler::move_block_wes() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, wes);
if (!next) {
return;
}
sel_x--;
state.update_current_state(*next);
}
auto input_handler::move_block_sou() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, sou);
if (!next) {
return;
}
sel_y++;
state.update_current_state(*next);
}
auto input_handler::move_block_eas() -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_move_block_at_fast(sel_x, sel_y, eas);
if (!next) {
return;
}
sel_x++;
state.update_current_state(*next);
}
auto input_handler::print_state() const -> void
{
infoln("State: \"{}\"", state.get_current_state().string_repr());
}
auto input_handler::load_previous_preset() -> void
{
if (editing) {
return;
}
const auto handler = [&]
{
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_previous_preset();
};
if (state.was_edited()) {
ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler});
} else {
handler();
}
}
auto input_handler::load_next_preset() -> void
{
if (editing) {
return;
}
const auto handler = [&]
{
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_next_preset();
};
if (state.was_edited()) {
ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler});
} else {
handler();
}
}
auto input_handler::goto_starting_state() -> void
{
const auto handler = [&]
{
state.goto_starting_state();
sel_x = 0;
sel_y = 0;
};
ui_commands.emplace(show_yes_no_message{"Reset Board?", "This Clears the Move History.", handler});
}
auto input_handler::populate_graph() const -> void
{
state.populate_graph();
}
auto input_handler::clear_graph() -> void
{
const auto handler = [&]
{
state.clear_graph_and_add_current();
};
ui_commands.emplace(show_yes_no_message{"Clear Graph?", "This Clears the Move History.", handler});
}
auto input_handler::toggle_mark_solutions() -> void
{
mark_solutions = !mark_solutions;
}
auto input_handler::toggle_connect_solutions() -> void
{
connect_solutions = !connect_solutions;
}
auto input_handler::toggle_mark_path() -> void
{
mark_path = !mark_path;
}
auto input_handler::goto_optimal_next_state() const -> void
{
state.goto_optimal_next_state();
}
auto input_handler::goto_most_distant_state() const -> void
{
state.goto_most_distant_state();
}
auto input_handler::goto_closest_target_state() const -> void
{
state.goto_closest_target_state();
}
auto input_handler::goto_previous_state() const -> void
{
state.goto_previous_state();
}
auto input_handler::toggle_restricted_movement() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.toggle_restricted();
if (!editing || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::toggle_target_block() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_toggle_target(sel_x, sel_y);
if (!editing || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::toggle_wall_block() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_toggle_wall(sel_x, sel_y);
if (!editing || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::remove_board_column() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_remove_column();
if (!editing || current.get_width() <= puzzle::MIN_WIDTH || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::add_board_column() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_add_column();
if (!editing || current.get_width() >= puzzle::MAX_WIDTH || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::remove_board_row() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_remove_row();
if (!editing || current.get_height() <= puzzle::MIN_HEIGHT || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::add_board_row() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.try_add_row();
if (!editing || current.get_height() >= puzzle::MAX_HEIGHT || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::toggle_editing() -> void
{
if (editing) {
has_block_add_xy = false;
block_add_x = -1;
block_add_y = -1;
}
editing = !editing;
}
auto input_handler::clear_goal() const -> void
{
const puzzle& current = state.get_current_state();
const std::optional<puzzle>& next = current.clear_goal();
if (!editing || !next) {
return;
}
state.edit_starting_state(*next);
}
auto input_handler::save_preset() -> void
{
if (!IsKeyDown(KEY_LEFT_CONTROL)) {
return;
}
if (const std::optional<std::string>& reason = state.get_current_state().try_get_invalid_reason()) {
ui_commands.emplace(show_ok_message{"Can't Save Preset", std::format("Invalid Board: {}.", *reason)});
} else {
ui_commands.emplace(show_save_preset_window{});
}
}
auto input_handler::register_generic_handler(const std::function<void(input_handler&)>& handler) -> void
{
generic_handlers.push_back({handler});
}
auto input_handler::register_mouse_pressed_handler(const MouseButton button,
const std::function<void(input_handler&)>& handler) -> void
{
mouse_pressed_handlers.push_back({{handler}, button});
}
auto input_handler::register_mouse_released_handler(const MouseButton button,
const std::function<void(input_handler&)>& handler) -> void
{
mouse_released_handlers.push_back({{handler}, button});
}
auto input_handler::register_key_pressed_handler(const KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void
{
key_pressed_handlers.push_back({{handler}, key});
}
auto input_handler::register_key_released_handler(const KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void
{
key_released_handlers.push_back({{handler}, key});
}
auto input_handler::handle_input() -> void
{
if (disable) {
return;
}
for (const auto& [handler] : generic_handlers) {
handler(*this);
}
for (const mouse_handler& handler : mouse_pressed_handlers) {
if (IsMouseButtonPressed(handler.button)) {
handler.handler(*this);
}
}
for (const mouse_handler& handler : mouse_released_handlers) {
if (IsMouseButtonReleased(handler.button)) {
handler.handler(*this);
}
}
for (const keyboard_handler& handler : key_pressed_handlers) {
if (IsKeyPressed(handler.key)) {
handler.handler(*this);
}
}
for (const keyboard_handler& handler : key_released_handlers) {
if (IsKeyReleased(handler.key)) {
handler.handler(*this);
}
}
}

View File

@ -1,130 +0,0 @@
#include "klotski.hpp"
auto Block::Hash() -> int {
std::string s = std::format("{},{},{},{}", x, y, width, height);
return std::hash<std::string>{}(s);
}
auto Block::Invalid() -> Block const {
Block block = Block(0, 0, 1, 1, false);
block.width = 0;
block.height = 0;
return block;
}
auto Block::IsValid() -> bool { return width != 0 && height != 0; }
auto Block::ToString() -> std::string {
if (target) {
return std::format("{}{}",
static_cast<char>(width + static_cast<int>('a') - 1),
static_cast<char>(height + static_cast<int>('a') - 1));
} else {
return std::format("{}{}", width, height);
}
}
auto Block::Covers(int xx, int yy) -> bool {
return xx >= x && xx < x + width && yy >= y && yy < y + height;
}
auto Block::Collides(const Block &other) -> bool {
return x < other.x + other.width && x + width > other.x &&
y < other.y + other.height && y + height > other.y;
}
auto State::Hash() -> int { return std::hash<std::string>{}(state); }
auto State::AddBlock(Block block) -> bool {
if (block.x + block.width > width || block.y + block.height > height) {
return false;
}
for (Block b : *this) {
if (b.Collides(block)) {
return false;
}
}
int index = 4 + (width * block.y + block.x) * 2;
state.replace(index, 2, block.ToString());
return true;
}
auto State::GetBlock(int x, int y) -> Block {
if (x >= width || y >= height) {
return Block::Invalid();
}
for (Block b : *this) {
if (b.Covers(x, y)) {
return b;
}
}
return Block::Invalid();
}
auto State::RemoveBlock(int x, int y) -> bool {
Block b = GetBlock(x, y);
if (!b.IsValid()) {
return false;
}
int index = 4 + (width * b.y + b.x) * 2;
state.replace(index, 2, "..");
return true;
}
auto State::MoveBlockAt(int x, int y, Direction dir) -> bool {
Block block = GetBlock(x, y);
if (!block.IsValid()) {
return false;
}
// Get target block
int target_x = block.x;
int target_y = block.y;
switch (dir) {
case Direction::NOR:
if (target_y < 1) {
return false;
}
target_y--;
break;
case Direction::EAS:
if (target_x + block.width >= width) {
return false;
}
target_x++;
break;
case Direction::SOU:
if (target_y + block.height >= height) {
return false;
}
target_y++;
break;
case Direction::WES:
if (target_x < 1) {
return false;
}
target_x--;
break;
}
Block target =
Block(target_x, target_y, block.width, block.height, block.target);
// Check collisions
for (Block b : *this) {
if (b != block && b.Collides(target)) {
return false;
}
}
RemoveBlock(x, y);
AddBlock(target);
return true;
}

View File

@ -1,78 +1,176 @@
#define VERLET_UPDATE #include <chrono>
#include <mutex>
#include <iostream>
#include <raylib.h> #include <raylib.h>
#include <raymath.h>
#include "config.hpp" #include "config.hpp"
#include "klotski.hpp" #include "input_handler.hpp"
#include "mass_springs.hpp" #include "mass_spring_system.hpp"
#include "threaded_physics.hpp"
#include "renderer.hpp" #include "renderer.hpp"
#include "state_manager.hpp"
#include "user_interface.hpp"
auto main(int argc, char *argv[]) -> int { #ifdef TRACY
// if (argc < 2) { #include <tracy/Tracy.hpp>
// std::cout << "Missing .klotski file." << std::endl;
// return 1;
// }
SetTraceLogLevel(LOG_ERROR);
// SetTargetFPS(60);
SetConfigFlags(FLAG_VSYNC_HINT);
SetConfigFlags(FLAG_MSAA_4X_HINT);
InitWindow(WIDTH * 2, HEIGHT, "MassSprings");
MassSpringSystem mass_springs;
mass_springs.AddMass(1.0, Vector3(-0.5, 0.5, 0.0), true);
mass_springs.AddMass(1.0, Vector3(0.5, 0.5, 0.0), false);
mass_springs.AddMass(1.0, Vector3(0.5, 0.0, 0.0), false);
mass_springs.AddSpring(0, 1, DEFAULT_SPRING_CONSTANT,
DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH);
mass_springs.AddSpring(1, 2, DEFAULT_SPRING_CONSTANT,
DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH);
State s = State(4, 5);
Block a = Block(0, 0, 2, 1, false);
Block b = Block(0, 1, 1, 3, true);
Block c = Block(0, 2, "45");
Block d = Block(0, 3, "de");
s.AddBlock(a);
s.AddBlock(b);
for (Block block : s) {
std::cout << "Block (" << block.x << ", " << block.y << ")" << std::endl;
}
Renderer renderer(WIDTH, HEIGHT);
Edge2Set edges;
edges.reserve(mass_springs.springs.size());
Vertex2Set vertices;
vertices.reserve(mass_springs.masses.size());
float frametime;
float abstime = 0.0;
while (!WindowShouldClose()) {
frametime = GetFrameTime();
mass_springs.ClearForces();
mass_springs.CalculateSpringForces();
#ifdef VERLET_UPDATE
mass_springs.VerletUpdate(frametime * SIM_SPEED);
#else
mass_springs.EulerUpdate(frametime * SIM_SPEED);
#endif #endif
renderer.Transform(edges, vertices, mass_springs, abstime * ROTATION_SPEED, // TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly)
CAMERA_DISTANCE); // - Clear graph: Notify that this will clear the visited states and move
renderer.DrawMassSprings(edges, vertices); // history
renderer.DrawKlotski(s); // TODO: Don't need to render each frame // - Reset state: Notify that this will reset the move count
renderer.DrawTextures();
abstime += frametime; // TODO: Reduce memory usage
} // - The memory model of the puzzle board is terrible (bitboards?)
CloseWindow(); // TODO: Improve solver
// - Move discovery is terrible
// - Instead of trying each direction for each block, determine the
// possible moves more efficiently (requires a different memory model)
// - 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
return 0; // 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
// NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy.
// For profiling explore_state_space
auto main2(int argc, char* argv[]) -> int
{
const puzzle p = puzzle(
"S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]");
for (int i = 0; i < 50; ++i) {
auto space = p.explore_state_space();
}
return 0;
} }
auto main(int argc, char* argv[]) -> int
{
std::string preset_file;
if (argc != 2) {
preset_file = "default.puzzle";
} else {
preset_file = argv[1];
}
#ifdef BACKWARD
infoln("Backward stack-traces enabled.");
#else
infoln("Backward stack-traces disabled.");
#endif
#ifdef TRACY
infoln("Tracy adapter enabled.");
#else
infoln("Tracy adapter disabled.");
#endif
// RayLib window setup
SetTraceLogLevel(LOG_ERROR);
SetConfigFlags(FLAG_VSYNC_HINT);
SetConfigFlags(FLAG_MSAA_4X_HINT);
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN);
InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings");
// Game setup
threaded_physics physics;
state_manager state(physics, preset_file);
orbit_camera camera;
input_handler input(state, camera);
user_interface gui(input, state, camera);
renderer renderer(camera, state, input, gui);
std::chrono::time_point last = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> fps_accumulator(0);
int loop_iterations = 0;
int fps = 0;
int ups = 0; // Read from physics
Vector3 mass_center; // Read from physics
std::vector<Vector3> masses; // Read from physics
size_t mass_count = 0;
size_t spring_count = 0;
// Game loop
while (!WindowShouldClose()) {
#ifdef TRACY
FrameMarkStart("MainThread");
#endif
// Time tracking
std::chrono::time_point now = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> delta_time = now - last;
fps_accumulator += delta_time;
last = now;
// Input update
input.handle_input();
// Read positions from physics thread
#ifdef TRACY
FrameMarkStart("MainThreadConsumeLock");
#endif
{
#ifdef TRACY
std::unique_lock<LockableBase(std::mutex)> lock(physics.state.data_mtx);
#else
std::unique_lock<std::mutex> lock(physics.state.data_mtx);
#endif
ups = physics.state.ups;
mass_center = physics.state.mass_center;
mass_count = physics.state.mass_count;
spring_count = physics.state.spring_count;
// Only copy data if any has been produced
if (physics.state.data_ready) {
masses = physics.state.masses;
physics.state.data_ready = false;
physics.state.data_consumed = true;
lock.unlock();
// Notify the physics thread that data has been consumed
physics.state.data_consumed_cnd.notify_all();
}
}
#ifdef TRACY
FrameMarkEnd("MainThreadConsumeLock");
#endif
// Update the camera after the physics, so target lock is smooth
size_t current_index = state.get_current_index();
if (masses.size() > current_index) {
const mass_spring_system::mass& current_mass = mass_spring_system::mass(masses.at(current_index));
camera.update(current_mass.position, mass_center, input.camera_lock, input.camera_mass_center_lock);
}
// Rendering
renderer.render(masses, fps, ups, mass_count, spring_count);
if (fps_accumulator.count() > 1.0) {
// Update each second
fps = loop_iterations;
loop_iterations = 0;
fps_accumulator = std::chrono::duration<double>(0);
}
++loop_iterations;
#ifdef TRACY
FrameMark; FrameMarkEnd("MainThread");
#endif
}
CloseWindow();
return 0;
}

231
src/mass_spring_system.cpp Normal file
View File

@ -0,0 +1,231 @@
#include "mass_spring_system.hpp"
#include "config.hpp"
#include <cfloat>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto mass_spring_system::mass::clear_force() -> void
{
force = Vector3Zero();
}
auto mass_spring_system::mass::calculate_velocity(const float delta_time) -> void
{
const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS);
const Vector3 temp = Vector3Scale(acceleration, delta_time);
velocity = Vector3Add(velocity, temp);
}
auto mass_spring_system::mass::calculate_position(const float delta_time) -> void
{
previous_position = position;
const Vector3 temp = Vector3Scale(velocity, delta_time);
position = Vector3Add(position, temp);
}
auto mass_spring_system::mass::verlet_update(const float delta_time) -> void
{
const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS);
const Vector3 temp_position = position;
Vector3 displacement = Vector3Subtract(position, previous_position);
const Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time);
// Minimal dampening
displacement = Vector3Scale(displacement, 1.0 - VERLET_DAMPENING);
position = Vector3Add(Vector3Add(position, displacement), accel_term);
previous_position = temp_position;
}
auto mass_spring_system::spring::calculate_spring_force(mass& _a, mass& _b) -> void
{
// TODO: Use a bungee force here instead of springs, since we already have global repulsion?
const Vector3 delta_position = Vector3Subtract(_a.position, _b.position);
const float current_length = Vector3Length(delta_position);
const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity);
const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH);
const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) / current_length;
const Vector3 force_a = Vector3Scale(delta_position, -(hooke + dampening) / current_length);
const Vector3 force_b = Vector3Scale(force_a, -1.0);
_a.force = Vector3Add(_a.force, force_a);
_b.force = Vector3Add(_b.force, force_b);
}
auto mass_spring_system::clear() -> void
{
masses.clear();
springs.clear();
tree.nodes.clear();
}
auto mass_spring_system::add_mass() -> void
{
// Adding all positions to (0, 0, 0) breaks the octree
// Done when adding springs
// Vector3 position{
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100,
// 100)), static_cast<float>(GetRandomValue(-100, 100))
// };
// position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0);
masses.emplace_back(Vector3Zero());
}
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
const mass& mass_a = masses.at(a);
mass& mass_b = masses.at(b);
Vector3 offset{
static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))
};
offset = Vector3Normalize(offset) * REST_LENGTH;
// If the offset moves the mass closer to the current center of mass, flip it
if (!tree.nodes.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
offset = Vector3Negate(offset);
}
}
mass_b.position = mass_a.position + offset;
mass_b.previous_position = mass_b.position;
// infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y,
// mass_a.position.z,
// mass_b.position.x, mass_b.position.y, mass_b.position.z);
springs.emplace_back(a, b);
}
auto mass_spring_system::clear_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (auto& m : masses) {
m.clear_force();
}
}
auto mass_spring_system::calculate_spring_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (const auto s : springs) {
mass& a = masses.at(s.a);
mass& b = masses.at(s.b);
spring::calculate_spring_force(a, b);
}
}
#ifdef THREADPOOL
auto mass_spring_system::set_thread_name(size_t idx) -> void
{
BS::this_thread::set_os_thread_name(std::format("bh-worker-{}", idx));
}
#endif
auto mass_spring_system::build_octree() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
tree.nodes.clear();
tree.nodes.reserve(masses.size() * 2);
// Compute bounding box around all masses
Vector3 min{FLT_MAX, FLT_MAX, FLT_MAX};
Vector3 max{-FLT_MAX, -FLT_MAX, -FLT_MAX};
for (const auto& m : masses) {
min.x = std::min(min.x, m.position.x);
max.x = std::max(max.x, m.position.x);
min.y = std::min(min.y, m.position.y);
max.y = std::max(max.y, m.position.y);
min.z = std::min(min.z, m.position.z);
max.z = std::max(max.z, m.position.z);
}
// Pad the bounding box
constexpr float pad = 1.0;
min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad));
max = Vector3Add(max, Vector3Scale(Vector3One(), pad));
// Make it cubic (so subdivisions are balanced)
const float max_extent = std::max({max.x - min.x, max.y - min.y, max.z - min.z});
max = Vector3Add(min, Vector3Scale(Vector3One(), max_extent));
// Root node spans the entire area
const int root = tree.create_empty_leaf(min, max);
for (size_t i = 0; i < masses.size(); ++i) {
tree.insert(root, static_cast<int>(i), masses[i].position, MASS, 0);
}
}
auto mass_spring_system::calculate_repulsion_forces() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
build_octree();
auto solve_octree = [&](const int i)
{
const Vector3 force = tree.calculate_force(0, masses[i].position);
masses[i].force = Vector3Add(masses[i].force, force);
};
// Calculate forces using Barnes-Hut
#ifdef THREADPOOL
const BS::multi_future<void> loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256);
loop_future.wait();
#else
for (size_t i = 0; i < masses.size(); ++i) {
solve_octree(i);
}
#endif
}
auto mass_spring_system::verlet_update(const float delta_time) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
for (auto& m : masses) {
m.verlet_update(delta_time);
}
}
auto mass_spring_system::center_masses() -> void
{
Vector3 mean = Vector3Zero();
for (const auto& m : masses) {
mean += m.position;
}
mean /= static_cast<float>(masses.size());
for (auto& m : masses) {
m.position -= mean;
}
}

View File

@ -1,118 +0,0 @@
#include "mass_springs.hpp"
#include <cstddef>
#include <raymath.h>
auto Mass::ClearForce() -> void { force = Vector3Zero(); }
auto Mass::CalculateVelocity(const float delta_time) -> void {
if (fixed) {
return;
}
Vector3 acceleration;
Vector3 temp;
acceleration = Vector3Scale(force, 1.0 / mass);
temp = Vector3Scale(acceleration, delta_time);
velocity = Vector3Add(velocity, temp);
}
auto Mass::CalculatePosition(const float delta_time) -> void {
if (fixed) {
return;
}
previous_position = position;
Vector3 temp;
temp = Vector3Scale(velocity, delta_time);
position = Vector3Add(position, temp);
}
auto Mass::VerletUpdate(const float delta_time) -> void {
if (fixed) {
return;
}
Vector3 acceleration = Vector3Scale(force, 1.0 / mass);
Vector3 temp_position = position;
Vector3 displacement = Vector3Subtract(position, previous_position);
Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time);
// Minimal dampening
displacement = Vector3Scale(displacement, 0.99);
position = Vector3Add(Vector3Add(position, displacement), accel_term);
previous_position = temp_position;
}
auto Spring::CalculateSpringForce() -> void {
Vector3 delta_position;
float current_length;
Vector3 delta_velocity;
Vector3 force_a;
Vector3 force_b;
delta_position = Vector3Subtract(massA.position, massB.position);
current_length = Vector3Length(delta_position);
delta_velocity = Vector3Subtract(massA.velocity, massB.velocity);
float hooke = spring_constant * (current_length - rest_length);
float dampening = dampening_constant *
Vector3DotProduct(delta_velocity, delta_position) /
current_length;
force_a = Vector3Scale(delta_position, -(hooke + dampening) / current_length);
force_b = Vector3Scale(force_a, -1.0);
massA.force = Vector3Add(massA.force, force_a);
massB.force = Vector3Add(massB.force, force_b);
}
auto MassSpringSystem::AddMass(float mass, Vector3 position, bool fixed)
-> void {
masses.emplace_back(mass, position, fixed);
}
auto MassSpringSystem::GetMass(const size_t index) -> Mass & {
return masses[index];
}
auto MassSpringSystem::AddSpring(int massA, int massB, float spring_constant,
float dampening_constant, float rest_length)
-> void {
springs.emplace_back(masses[massA], masses[massB], spring_constant,
dampening_constant, rest_length);
}
auto MassSpringSystem::GetSpring(const size_t index) -> Spring & {
return springs[index];
}
auto MassSpringSystem::ClearForces() -> void {
for (auto &mass : masses) {
mass.ClearForce();
}
}
auto MassSpringSystem::CalculateSpringForces() -> void {
for (auto &spring : springs) {
spring.CalculateSpringForce();
}
}
auto MassSpringSystem::EulerUpdate(const float delta_time) -> void {
for (auto &mass : masses) {
mass.CalculateVelocity(delta_time);
mass.CalculatePosition(delta_time);
}
}
auto MassSpringSystem::VerletUpdate(const float delta_time) -> void {
for (auto &mass : masses) {
mass.VerletUpdate(delta_time);
}
}

193
src/octree.cpp Normal file
View File

@ -0,0 +1,193 @@
#include "octree.hpp"
#include "config.hpp"
#include "util.hpp"
#include <raymath.h>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto octree::node::child_count() const -> int
{
int child_count = 0;
for (const int child : children) {
if (child != -1) {
++child_count;
}
}
return child_count;
}
auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int
{
node n;
n.box_min = box_min;
n.box_max = box_max;
nodes.emplace_back(n);
return static_cast<int>(nodes.size() - 1);
}
auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
{
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::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3>
{
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::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
const int depth) -> void
{
// infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y,
// pos.z, node_idx, depth);
if (depth > MAX_DEPTH) {
throw std::runtime_error(std::format("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})",
node_idx, nodes[node_idx].box_min.x, nodes[node_idx].box_min.y,
nodes[node_idx].box_min.z, nodes[node_idx].box_max.x,
nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y,
pos.z));
}
// NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during
// this function
// We can place the particle in the empty leaf
if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) {
nodes[node_idx].mass_id = mass_id;
nodes[node_idx].mass_center = pos;
nodes[node_idx].mass_total = mass;
return;
}
// The leaf is occupied, we need to subdivide
if (nodes[node_idx].leaf) {
const int existing_id = nodes[node_idx].mass_id;
const Vector3 existing_pos = nodes[node_idx].mass_center;
const float existing_mass = nodes[node_idx].mass_total;
// If positions are identical we jitter the particles
const Vector3 diff = Vector3Subtract(pos, existing_pos);
if (diff == Vector3Zero()) {
// warnln("Trying to insert an identical partical into octree (jittering position)");
Vector3 jittered = pos;
jittered.x += 0.001;
jittered.y += 0.001;
insert(node_idx, mass_id, jittered, mass, depth);
return;
// Could also merge them, but that leads to the octree having less leafs than we have
// masses nodes[node_idx].mass_total += mass; return;
}
// Convert the leaf to an internal node
nodes[node_idx].mass_id = -1;
nodes[node_idx].leaf = false;
nodes[node_idx].mass_total = 0.0;
nodes[node_idx].mass_center = Vector3Zero();
// Re-insert the existing mass into a new empty leaf (see above)
const int oct = get_octant(node_idx, existing_pos);
if (nodes[node_idx].children[oct] == -1) {
const auto& [min, max] = get_child_bounds(node_idx, oct);
const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
}
insert(nodes[node_idx].children[oct], existing_id, existing_pos, existing_mass, depth + 1);
}
// Insert the new mass
const int oct = get_octant(node_idx, pos);
if (nodes[node_idx].children[oct] == -1) {
const auto& [min, max] = get_child_bounds(node_idx, oct);
const int child_idx = create_empty_leaf(min, max);
nodes[node_idx].children[oct] = child_idx;
}
insert(nodes[node_idx].children[oct], mass_id, pos, mass, depth + 1);
// Update the center of mass
const float new_mass = nodes[node_idx].mass_total + mass;
nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass;
nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass;
nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass;
nodes[node_idx].mass_total = new_mass;
}
auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Vector3
{
if (node_idx < 0) {
return Vector3Zero();
}
const node& n = nodes[node_idx];
if (std::abs(n.mass_total) <= 0.001f) {
return Vector3Zero();
}
const Vector3 diff = Vector3Subtract(pos, n.mass_center);
float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
// Softening
dist_sq += SOFTENING;
// Barnes-Hut
const float size = n.box_max.x - n.box_min.x;
if (n.leaf || size * size / dist_sq < THETA * THETA) {
const float dist = std::sqrt(dist_sq);
const float force_mag = BH_FORCE * n.mass_total / dist_sq;
return Vector3Scale(diff, force_mag / dist);
}
// Collect child forces
Vector3 force = Vector3Zero();
for (const int child : n.children) {
if (child >= 0) {
const Vector3 child_force = calculate_force(child, pos);
force = Vector3Add(force, child_force);
}
}
return force;
}

76
src/orbit_camera.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "orbit_camera.hpp"
#include "config.hpp"
#include <raylib.h>
#include <raymath.h>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto orbit_camera::rotate(const Vector2 last_mouse, const Vector2 mouse) -> void
{
const auto [dx, dy] = Vector2Subtract(mouse, last_mouse);
angle_x -= dx * ROT_SPEED / 200.0f;
angle_y += dy * ROT_SPEED / 200.0f;
angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping
}
auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void
{
const auto [dx, dy] = Vector2Subtract(mouse, last_mouse);
float speed;
if (IsKeyDown(KEY_LEFT_SHIFT)) {
speed = distance * PAN_SPEED / 1000.0f * PAN_MULTIPLIER;
} else {
speed = distance * PAN_SPEED / 1000.0f;
}
// The panning needs to happen in camera coordinates, otherwise rotating the
// camera breaks it
const Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position));
const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));
const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward));
const Vector3 offset =
Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
target = Vector3Add(target, offset);
}
auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center,
const bool lock, const bool mass_center_lock) -> void
{
if (lock) {
if (mass_center_lock) {
target = Vector3MoveTowards(target, mass_center,
CAMERA_SMOOTH_SPEED * GetFrameTime() *
Vector3Length(Vector3Subtract(target, mass_center)));
} else {
target = Vector3MoveTowards(target, current_target,
CAMERA_SMOOTH_SPEED * GetFrameTime() *
Vector3Length(Vector3Subtract(target, current_target)));
}
}
distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
float actual_distance = distance;
if (projection == CAMERA_ORTHOGRAPHIC) {
actual_distance = MAX_CAMERA_DISTANCE;
}
// Spherical coordinates
const float x = cos(angle_y) * sin(angle_x) * actual_distance;
const float y = sin(angle_y) * actual_distance;
const float z = cos(angle_y) * cos(angle_x) * actual_distance;
fov = Clamp(fov, MIN_FOV, MAX_FOV);
camera.position = Vector3Add(target, Vector3(x, y, z));
camera.target = target;
camera.fovy = fov;
camera.projection = projection;
}

1115
src/puzzle.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,138 +1,230 @@
#include "renderer.hpp" #include "renderer.hpp"
#include <raylib.h>
#include "config.hpp" #include "config.hpp"
auto Renderer::Rotate(const Vector3 &a, const float cos_angle, #include <raylib.h>
const float sin_angle) -> Vector3 { #include <raymath.h>
return Vector3(a.x * cos_angle - a.z * sin_angle, a.y, #include <rlgl.h>
a.x * sin_angle + a.z * cos_angle);
};
auto Renderer::Translate(const Vector3 &a, const float distance) -> Vector3 { #ifdef TRACY
return Vector3(a.x, a.y, a.z + distance); #include <tracy/Tracy.hpp>
}; #endif
auto Renderer::Project(const Vector3 &a) -> Vector2 { auto renderer::update_texture_sizes() -> void
return Vector2(a.x / a.z, a.y / a.z); {
} if (!IsWindowResized()) {
return;
auto Renderer::Map(const Vector2 &a) -> Vector2 {
return Vector2((1.0 + a.x) / 2.0 * width, (1.0 - a.y) * height / 2.0);
}
auto Renderer::Transform(Edge2Set &edges, Vertex2Set &vertices,
const MassSpringSystem &mass_springs,
const float angle, const float distance) -> void {
const float cos_angle = cos(angle);
const float sin_angle = sin(angle);
edges.clear();
for (const auto &spring : mass_springs.springs) {
Vector2 a = Map(Project(Translate(
Rotate(spring.massA.position, cos_angle, sin_angle), distance)));
Vector2 b = Map(Project(Translate(
Rotate(spring.massB.position, cos_angle, sin_angle), distance)));
edges.emplace_back(a, b);
}
// This is duplicated work, but easy to read
vertices.clear();
for (const auto &mass : mass_springs.masses) {
Vector3 a =
Translate(Rotate(mass.position, cos_angle, sin_angle), distance);
Vector2 b = Map(Project(a));
vertices.emplace_back(b.x, b.y, a.z);
}
}
auto Renderer::DrawMassSprings(const Edge2Set &edges,
const Vertex2Set &vertices) -> void {
BeginTextureMode(render_target);
ClearBackground(RAYWHITE);
for (const auto &[a, b] : edges) {
DrawLine(a.x, a.y, b.x, b.y, EDGE_COLOR);
}
for (const auto &a : vertices) {
// Increase the perspective perception by squaring the z-coordinate
const float size = VERTEX_SIZE / (a.z * a.z);
DrawRectangle(a.x - size / 2.0, a.y - size / 2.0, size, size, VERTEX_COLOR);
}
DrawLine(0, 0, 0, height, BLACK);
EndTextureMode();
}
auto Renderer::DrawKlotski(State &state) -> void {
BeginTextureMode(klotski_target);
ClearBackground(RAYWHITE);
// Draw Board
const int board_width = width - 2 * BOARD_PADDING;
const int board_height = height - 2 * BOARD_PADDING;
float block_size;
float x_offset = 0.0;
float y_offset = 0.0;
if (state.width > state.height) {
block_size =
static_cast<float>(board_width) / state.width - 2 * BLOCK_PADDING;
y_offset = (board_height - block_size * state.height -
BLOCK_PADDING * 2 * state.height) /
2.0;
} else {
block_size =
static_cast<float>(board_height) / state.height - 2 * BLOCK_PADDING;
x_offset = (board_width - block_size * state.width -
BLOCK_PADDING * 2 * state.width) /
2.0;
}
DrawRectangle(0, 0, width, height, RAYWHITE);
DrawRectangle(x_offset, y_offset,
board_width - 2 * x_offset + 2 * BOARD_PADDING,
board_height - 2 * y_offset + 2 * BOARD_PADDING, LIGHTGRAY);
for (int x = 0; x < state.width; ++x) {
for (int y = 0; y < state.height; ++y) {
DrawRectangle(x_offset + BOARD_PADDING + x * BLOCK_PADDING * 2 +
BLOCK_PADDING + x * block_size,
y_offset + BOARD_PADDING + y * BLOCK_PADDING * 2 +
BLOCK_PADDING + y * block_size,
block_size, block_size, WHITE);
} }
}
// Draw Blocks UnloadRenderTexture(render_target);
for (Block block : state) { UnloadRenderTexture(klotski_target);
Color c = EDGE_COLOR; UnloadRenderTexture(menu_target);
if (block.target) {
c = RED; const int width = GetScreenWidth() / 2;
const int height = GetScreenHeight() - MENU_HEIGHT;
render_target = LoadRenderTexture(width, height);
klotski_target = LoadRenderTexture(width, height);
menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT);
}
auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
{
#ifdef TRACY
ZoneScoped;
#endif
if (masses.size() != state.get_state_count()) {
// Because the physics run in a different thread, it might need time to catch up
return;
} }
DrawRectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 +
BLOCK_PADDING + block.x * block_size,
y_offset + BOARD_PADDING + block.y * BLOCK_PADDING * 2 +
BLOCK_PADDING + block.y * block_size,
block.width * block_size + block.width * 2 * BLOCK_PADDING -
2 * BLOCK_PADDING,
block.height * block_size + block.height * 2 * BLOCK_PADDING -
2 * BLOCK_PADDING,
c);
}
DrawLine(width - 1, 0, width - 1, height, BLACK); // Prepare connection batching
EndTextureMode(); {
#ifdef TRACY
ZoneNamedN(prepare_masses, "PrepareConnectionsBatching", true);
#endif
connections.clear();
connections.reserve(state.get_target_count());
if (input.connect_solutions) {
for (const size_t& _state : state.get_winning_indices()) {
const Vector3& current_mass = masses.at(state.get_current_index());
const Vector3& winning_mass = masses.at(_state);
connections.emplace_back(current_mass, winning_mass);
DrawLine3D(current_mass, winning_mass, Fade(TARGET_BLOCK_COLOR, 0.5));
}
}
}
// Prepare cube instancing
{
#ifdef TRACY
ZoneNamedN(prepare_masses, "PrepareMassInstancing", true);
#endif
if (masses.size() < DRAW_VERTICES_LIMIT) {
// Don't have to reserve, capacity is already set to DRAW_VERTICES_LIMIT in constructor
transforms.clear();
colors.clear();
size_t mass = 0;
for (const auto& [x, y, z] : masses) {
transforms.emplace_back(MatrixTranslate(x, y, z));
// Normal vertex
Color c = VERTEX_COLOR;
if ((input.mark_solutions || input.mark_path) &&
state.get_winning_indices().contains(mass)) {
// Winning vertex
c = VERTEX_TARGET_COLOR;
} else if ((input.mark_solutions || input.mark_path) &&
state.get_path_indices().contains(mass)) {
// Path vertex
c = VERTEX_PATH_COLOR;
} else if (mass == state.get_starting_index()) {
// Starting vertex
c = VERTEX_START_COLOR;
} else if (state.get_visit_counts().at(mass) > 0) {
// Visited vertex
c = VERTEX_VISITED_COLOR;
}
// Current vertex is drawn as individual cube to increase its size
colors.emplace_back(c);
++mass;
}
}
rlUpdateVertexBuffer(color_vbo_id, colors.data(), colors.size() * sizeof(Color), 0);
}
BeginTextureMode(render_target);
ClearBackground(RAYWHITE);
BeginMode3D(camera.camera);
// Draw springs (batched)
{
#ifdef TRACY
ZoneNamedN(draw_springs, "DrawSprings", true);
#endif
rlBegin(RL_LINES);
for (const auto& [from, to] : state.get_links()) {
if (masses.size() > from && masses.size() > to) {
const auto& [ax, ay, az] = masses.at(from);
const auto& [bx, by, bz] = masses.at(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)
{
#ifdef TRACY
ZoneNamedN(draw_masses, "DrawMasses", true);
#endif
if (masses.size() < DRAW_VERTICES_LIMIT) {
// NOTE: I don't know if drawing all this inside a shader would make it
// much faster... The amount of data sent to the GPU would be
// reduced (just positions instead of matrices), but is this
// noticable for < 100000 cubes?
DrawMeshInstanced(cube_instance, vertex_mat, transforms.data(),
masses.size()); // NOLINT(*-narrowing-conversions)
}
}
// Connect current to winning states (batched)
const auto [r, g, b, a] = Fade(VERTEX_CURRENT_COLOR, 0.3);
rlBegin(RL_LINES);
for (const auto& [from, to] : connections) {
const auto& [ax, ay, az] = from;
const auto& [bx, by, bz] = to;
rlColor4ub(r, g, b, a);
rlVertex3f(ax, ay, az);
rlVertex3f(bx, by, bz);
}
rlEnd();
// Mark current state
const size_t current_index = state.get_current_index();
if (masses.size() > current_index) {
const Vector3& current_mass = masses.at(current_index);
DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
VERTEX_CURRENT_COLOR);
}
EndMode3D();
EndTextureMode();
} }
auto Renderer::DrawTextures() -> void { auto renderer::draw_klotski() const -> void
BeginDrawing(); {
DrawTextureRec(klotski_target.texture, #ifdef TRACY
Rectangle(0, 0, (float)width, -(float)height), Vector2(0, 0), ZoneScoped;
WHITE); #endif
DrawTextureRec(render_target.texture,
Rectangle(0, 0, (float)width, -(float)height), BeginTextureMode(klotski_target);
Vector2(width, 0), WHITE); ClearBackground(RAYWHITE);
DrawFPS(width + 10, 10);
EndDrawing(); gui.draw_puzzle_board();
EndTextureMode();
}
auto renderer::draw_menu() const -> void
{
#ifdef TRACY
ZoneScoped;
#endif
BeginTextureMode(menu_target);
ClearBackground(RAYWHITE);
gui.draw_main_menu();
EndTextureMode();
}
auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
const size_t spring_count) const -> void
{
BeginDrawing();
DrawTextureRec(menu_target.texture,
Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Vector2(0, 0), WHITE);
DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), WHITE);
DrawTextureRec(render_target.texture,
Rectangle(0, 0, render_target.texture.width, -render_target.texture.height),
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
// Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK);
DrawRectangleLinesEx(
Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT),
1.0f, BLACK);
gui.draw(fps, ups, mass_count, spring_count);
EndDrawing();
}
auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups,
const size_t mass_count, const size_t spring_count) -> void
{
update_texture_sizes();
draw_mass_springs(masses);
draw_klotski();
draw_menu();
draw_textures(fps, ups, mass_count, spring_count);
} }

492
src/state_manager.cpp Normal file
View File

@ -0,0 +1,492 @@
#include "state_manager.hpp"
#include "graph_distances.hpp"
#include "util.hpp"
#include <fstream>
#include <ios>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t
{
if (state_indices.contains(state)) {
return state_indices.at(state);
}
const size_t index = state_pool.size();
state_pool.emplace_back(state);
state_indices.emplace(state, index);
visit_counts[index] = 0;
// Queue an update to the physics engine state to keep in sync
physics.add_mass_cmd();
return index;
}
auto state_manager::synced_insert_link(size_t first_index, size_t second_index) -> void
{
links.emplace_back(first_index, second_index);
// Queue an update to the physics engine state to keep in sync
physics.add_spring_cmd(first_index, second_index);
}
auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void
{
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared");
}
for (const puzzle& state : states) {
const size_t index = state_pool.size();
state_pool.emplace_back(state);
state_indices.emplace(state, index);
visit_counts[index] = 0;
}
for (const auto& [from, to] : _links) {
links.emplace_back(from, to);
}
// Queue an update to the physics engine state to keep in sync
physics.add_mass_springs_cmd(state_pool.size(), links);
}
auto state_manager::synced_clear_statespace() -> void
{
// Those are invalid without any states
current_state_index = -1;
previous_state_index = -1;
starting_state_index = -1;
state_pool.clear();
state_indices.clear();
links.clear();
node_target_distances.clear();
winning_indices.clear();
winning_path.clear();
path_indices.clear();
move_history.clear();
visit_counts.clear();
// Queue an update to the physics engine state to keep in sync
physics.clear_cmd();
}
auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool
{
preset_file = _preset_file;
std::ifstream file(preset_file);
if (!file) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
}
std::string line;
std::vector<std::string> comment_lines;
std::vector<std::string> preset_lines;
while (std::getline(file, line)) {
if (line.starts_with("S")) {
preset_lines.push_back(line);
} else if (line.starts_with("#")) {
comment_lines.push_back(line);
}
}
if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
}
preset_states.clear();
for (const auto& preset : preset_lines) {
// Each char is a bit
const puzzle& p = puzzle(preset);
if (const std::optional<std::string>& reason = p.try_get_invalid_reason()) {
preset_states = {puzzle(4, 5, 0, 0, true, false)};
infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason);
return false;
}
preset_states.emplace_back(p);
}
preset_comments = comment_lines;
infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file);
return true;
}
auto state_manager::append_preset_file(const std::string& preset_name) -> bool
{
infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file);
if (get_current_state().try_get_invalid_reason()) {
return false;
}
std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out);
if (!file) {
infoln("Preset file \"{}\" couldn't be loaded.", preset_file);
return false;
}
file << "\n# " << preset_name << "\n" << get_current_state().string_repr() << std::flush;
infoln("Refreshing presets...");
if (parse_preset_file(preset_file)) {
load_preset(preset_states.size() - 1);
}
return true;
}
auto state_manager::load_preset(const size_t preset) -> void
{
clear_graph_and_add_current(preset_states.at(preset));
current_preset = preset;
edited = false;
}
auto state_manager::load_previous_preset() -> void
{
load_preset((preset_states.size() + current_preset - 1) % preset_states.size());
}
auto state_manager::load_next_preset() -> void
{
load_preset((current_preset + 1) % preset_states.size());
}
auto state_manager::update_current_state(const puzzle& p) -> void
{
if (!p.valid()) {
return;
}
const size_t size_before = state_pool.size();
// If state is a duplicate, index will be the existing index,
// if state is new, index will be state_pool.size() - 1
const size_t index = synced_try_insert_state(p);
// Because synced_insert_link does not check for duplicates we do it here,
// if the size grows, it was not a duplicate, and we can add the spring
if (state_pool.size() > size_before) {
// The order is important, as the position of the second mass will be updated depending on
// the first
synced_insert_link(current_state_index, index);
}
previous_state_index = current_state_index;
current_state_index = index;
if (current_state_index != previous_state_index) {
move_history.emplace_back(previous_state_index);
}
if (p.goal_reached()) {
winning_indices.insert(current_state_index);
}
// Adds the element with 0 if it doesn't exist
visit_counts[current_state_index]++;
total_moves++;
// Recalculate distances only if the graph changed
if (state_pool.size() > size_before) {
populate_node_target_distances();
}
populate_winning_path();
}
auto state_manager::edit_starting_state(const puzzle& p) -> void
{
clear_graph_and_add_current(p);
move_history.clear();
total_moves = 0;
for (int& visits : visit_counts | std::views::values) {
visits = 0;
}
visit_counts[current_state_index]++;
edited = true;
}
auto state_manager::goto_starting_state() -> void
{
update_current_state(get_state(starting_state_index));
// Reset previous movement data since we're starting over (because we're fucking stupid)
previous_state_index = current_state_index;
for (int& visits : visit_counts | std::views::values) {
visits = 0;
}
visit_counts[current_state_index]++;
move_history.clear();
total_moves = 0;
}
auto state_manager::goto_optimal_next_state() -> void
{
if (node_target_distances.empty()) {
return;
}
// Already there
if (node_target_distances.distances[current_state_index] == 0) {
return;
}
const size_t parent_index = node_target_distances.parents[current_state_index];
update_current_state(get_state(parent_index));
}
auto state_manager::goto_previous_state() -> void
{
if (move_history.empty()) {
return;
}
update_current_state(get_state(move_history.back()));
// Pop twice because update_current_state adds the state again...
move_history.pop_back();
move_history.pop_back();
}
auto state_manager::goto_most_distant_state() -> void
{
if (node_target_distances.empty()) {
return;
}
int max_distance = 0;
size_t max_distance_index = 0;
for (size_t i = 0; i < node_target_distances.distances.size(); ++i) {
if (node_target_distances.distances.at(i) > max_distance) {
max_distance = node_target_distances.distances.at(i);
max_distance_index = i;
}
}
update_current_state(get_state(max_distance_index));
}
auto state_manager::goto_closest_target_state() -> void
{
if (node_target_distances.empty()) {
return;
}
update_current_state(get_state(node_target_distances.nearest_targets.at(current_state_index)));
}
auto state_manager::populate_graph() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
// Need to make a copy before clearing the state_pool
const puzzle s = get_starting_state();
const puzzle p = get_current_state();
// Clear the graph first so we don't add duplicates somehow
synced_clear_statespace();
// Explore the entire statespace starting from the current state
const auto& [states, _links] = s.explore_state_space();
synced_insert_statespace(states, _links);
current_state_index = state_indices.at(p);
previous_state_index = current_state_index;
starting_state_index = state_indices.at(s);
// Search for cool stuff
populate_winning_indices();
populate_node_target_distances();
populate_winning_path();
}
auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void
{
// We need to make a copy before clearing the state_pool
const puzzle _p = p; // NOLINT(*-unnecessary-copy-initialization)
synced_clear_statespace();
// Re-add the current state
current_state_index = synced_try_insert_state(_p);
// These states are no longer in the graph
previous_state_index = current_state_index;
starting_state_index = current_state_index;
visit_counts[current_state_index]++;
}
auto state_manager::clear_graph_and_add_current() -> void
{
clear_graph_and_add_current(get_current_state());
}
auto state_manager::populate_winning_indices() -> void
{
winning_indices.clear();
for (const auto& [state, index] : state_indices) {
if (state.goal_reached()) {
winning_indices.insert(index);
}
}
}
auto state_manager::populate_node_target_distances() -> void
{
#ifdef TRACY
ZoneScoped;
#endif
if (links.empty() || winning_indices.empty()) {
return;
}
const std::vector<size_t> targets(winning_indices.begin(), winning_indices.end());
node_target_distances.calculate_distances(state_pool.size(), links, targets);
}
auto state_manager::populate_winning_path() -> void
{
if (node_target_distances.empty()) {
return;
}
winning_path = node_target_distances.get_shortest_path(current_state_index);
path_indices.clear();
for (const size_t index : winning_path) {
path_indices.insert(index);
}
}
auto state_manager::get_index(const puzzle& state) const -> size_t
{
return state_indices.at(state);
}
auto state_manager::get_current_index() const -> size_t
{
return current_state_index;
}
auto state_manager::get_starting_index() const -> size_t
{
return starting_state_index;
}
auto state_manager::get_state(const size_t index) const -> const puzzle&
{
return state_pool.at(index);
}
auto state_manager::get_current_state() const -> const puzzle&
{
return get_state(current_state_index);
}
auto state_manager::get_starting_state() const -> const puzzle&
{
return get_state(starting_state_index);
}
auto state_manager::get_state_count() const -> size_t
{
return state_pool.size();
}
auto state_manager::get_target_count() const -> size_t
{
return winning_indices.size();
}
auto state_manager::get_link_count() const -> size_t
{
return links.size();
}
auto state_manager::get_path_length() const -> size_t
{
return winning_path.size();
}
auto state_manager::get_links() const -> const std::vector<std::pair<size_t, size_t>>&
{
return links;
}
auto state_manager::get_winning_indices() const -> const boost::unordered_flat_set<size_t>&
{
return winning_indices;
}
auto state_manager::get_visit_counts() const -> const boost::unordered_flat_map<size_t, int>&
{
return visit_counts;
}
auto state_manager::get_winning_path() const -> const std::vector<size_t>&
{
return winning_path;
}
auto state_manager::get_path_indices() const -> const boost::unordered_flat_set<size_t>&
{
return path_indices;
}
auto state_manager::get_current_visits() const -> int
{
return visit_counts.at(current_state_index);
}
auto state_manager::get_current_preset() const -> size_t
{
return current_preset;
}
auto state_manager::get_preset_count() const -> size_t
{
return preset_states.size();
}
auto state_manager::get_current_preset_comment() const -> const std::string&
{
return preset_comments.at(current_preset);
}
auto state_manager::has_history() const -> bool
{
return !move_history.empty();
}
auto state_manager::has_distances() const -> bool
{
return !node_target_distances.empty();
}
auto state_manager::get_total_moves() const -> size_t
{
return total_moves;
}
auto state_manager::was_edited() const -> bool
{
return edited;
}

194
src/threaded_physics.cpp Normal file
View File

@ -0,0 +1,194 @@
#include "threaded_physics.hpp"
#include "config.hpp"
#include "mass_spring_system.hpp"
#include <chrono>
#include <raylib.h>
#include <raymath.h>
#include <utility>
#include <vector>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto threaded_physics::physics_thread(physics_state& state) -> void
{
#ifdef THREADPOOL
BS::this_thread::set_os_thread_name("physics");
#endif
mass_spring_system mass_springs;
const auto visitor = overloads{
[&](const struct add_mass& am)
{
mass_springs.add_mass();
},
[&](const struct add_spring& as)
{
mass_springs.add_spring(as.a, as.b);
},
[&](const struct clear_graph& cg)
{
mass_springs.clear();
},
};
std::chrono::time_point last = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> physics_accumulator(0);
std::chrono::duration<double> ups_accumulator(0);
int loop_iterations = 0;
while (state.running.load()) {
#ifdef TRACY
FrameMarkStart("PhysicsThread");
#endif
// Time tracking
std::chrono::time_point now = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double> deltatime = now - last;
physics_accumulator += deltatime;
ups_accumulator += deltatime;
last = now;
// Handle queued commands
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
while (!state.pending_commands.empty()) {
command& cmd = state.pending_commands.front();
cmd.visit(visitor);
state.pending_commands.pop();
}
}
if (mass_springs.masses.empty()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
// Physics update
if (physics_accumulator.count() > TIMESTEP) {
mass_springs.clear_forces();
mass_springs.calculate_spring_forces();
mass_springs.calculate_repulsion_forces();
mass_springs.verlet_update(TIMESTEP * SIM_SPEED);
// This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just
// expensive and yields no benefit since we can lock the camera to the center of mass
// cheaply. mass_springs.center_masses();
++loop_iterations;
physics_accumulator -= std::chrono::duration<double>(TIMESTEP);
}
// Publish the positions for the renderer (copy)
#ifdef TRACY
FrameMarkStart("PhysicsThreadProduceLock");
#endif
{
#ifdef TRACY
std::unique_lock<LockableBase(std::mutex)> lock(state.data_mtx);
#else
std::unique_lock<std::mutex> lock(state.data_mtx);
#endif
state.data_consumed_cnd.wait(lock, [&]
{
return state.data_consumed || !state.running.load();
});
if (!state.running.load()) {
// Running turned false while we were waiting for the condition
break;
}
if (ups_accumulator.count() > 1.0) {
// Update each second
state.ups = loop_iterations;
loop_iterations = 0;
ups_accumulator = std::chrono::duration<double>(0);
}
if (mass_springs.tree.nodes.empty()) {
state.mass_center = Vector3Zero();
} else {
state.mass_center = mass_springs.tree.nodes.at(0).mass_center;
}
state.masses.clear();
state.masses.reserve(mass_springs.masses.size());
for (const auto& mass : mass_springs.masses) {
state.masses.emplace_back(mass.position);
}
state.mass_count = mass_springs.masses.size();
state.spring_count = mass_springs.springs.size();
state.data_ready = true;
state.data_consumed = false;
}
// Notify the rendering thread that new data is available
state.data_ready_cnd.notify_all();
#ifdef TRACY
FrameMarkEnd("PhysicsThreadProduceLock");
FrameMarkEnd("PhysicsThread");
#endif
}
}
auto threaded_physics::add_mass_cmd() -> void
{
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
state.pending_commands.emplace(add_mass{});
}
}
auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void
{
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
state.pending_commands.emplace(add_spring{a, b});
}
}
auto threaded_physics::clear_cmd() -> void
{
{
#ifdef TRACY
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
state.pending_commands.emplace(clear_graph{});
}
}
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
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
#else
std::lock_guard<std::mutex> lock(state.command_mtx);
#endif
for (size_t i = 0; i < num_masses; ++i) {
state.pending_commands.emplace(add_mass{});
}
for (const auto& [from, to] : springs) {
state.pending_commands.emplace(add_spring{from, to});
}
}
}

867
src/user_interface.cpp Normal file
View File

@ -0,0 +1,867 @@
#include "user_interface.hpp"
#include "config.hpp"
#include "input_handler.hpp"
#include <raylib.h>
#define RAYGUI_IMPLEMENTATION
#include <raygui.h>
#ifdef TRACY
#include <tracy/Tracy.hpp>
#endif
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height,
const int _columns, const int _rows) -> void
{
x = _x;
y = _y;
width = _width;
height = _height;
columns = _columns;
rows = _rows;
}
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height) -> void
{
x = _x;
y = _y;
width = _width;
height = _height;
}
auto user_interface::grid::update_bounds(const int _x, const int _y) -> void
{
x = _x;
y = _y;
}
auto user_interface::grid::bounds() const -> Rectangle
{
Rectangle bounds{0, 0, static_cast<float>(columns), static_cast<float>(rows)};
bounds.x -= padding;
bounds.y -= padding;
bounds.width += 2 * padding;
bounds.height += 2 * padding;
return bounds;
}
auto user_interface::grid::bounds(const int _x, const int _y, const int _width, const int _height) const -> Rectangle
{
if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) {
throw std::invalid_argument("Grid bounds out of range");
}
const int cell_width = (width - padding) / columns;
const int cell_height = (height - padding) / rows;
return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding,
_height * cell_height - padding);
}
auto user_interface::grid::square_bounds() const -> Rectangle
{
Rectangle bounds = square_bounds(0, 0, columns, rows);
bounds.x -= padding;
bounds.y -= padding;
bounds.width += 2 * padding;
bounds.height += 2 * padding;
return bounds;
}
auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width,
const int _height) const -> Rectangle
{
// Assumes each cell is square, so either width or height are not completely
// filled
if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) {
throw std::invalid_argument("Grid bounds out of range");
}
const int available_width = width - padding * (columns + 1);
const int available_height = height - padding * (rows + 1);
const int cell_size = std::min(available_width / columns, available_height / rows);
const int grid_width = cell_size * columns + padding * (columns + 1);
const int grid_height = cell_size * rows + padding * (rows + 1);
const int x_offset = (width - grid_width) / 2;
const int y_offset = (height - grid_height) / 2;
return Rectangle(x_offset + _x * (cell_size + padding) + padding, y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1));
}
auto user_interface::init() -> void
{
const Font font = LoadFontEx(FONT, FONT_SIZE, nullptr, 0);
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
GuiSetFont(font);
default_style style = get_default_style();
style.text_size = FONT_SIZE;
apply_color(style, GRAY);
set_default_style(style);
}
auto user_interface::apply_color(style& style, const Color color) -> void
{
style.base_color_normal = ColorToInt(Fade(color, 0.8));
style.base_color_focused = ColorToInt(Fade(color, 0.3));
style.base_color_pressed = ColorToInt(Fade(color, 0.8));
style.base_color_disabled = ColorToInt(Fade(color, 0.5));
style.border_color_normal = ColorToInt(Fade(color, 1.0));
style.border_color_focused = ColorToInt(Fade(color, 0.7));
style.border_color_pressed = ColorToInt(Fade(color, 1.0));
style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5));
style.text_color_normal = ColorToInt(Fade(BLACK, 1.0));
style.text_color_focused = ColorToInt(Fade(BLACK, 1.0));
style.text_color_pressed = ColorToInt(Fade(BLACK, 1.0));
style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5));
}
auto user_interface::apply_block_color(style& style, const Color color) -> void
{
style.base_color_normal = ColorToInt(Fade(color, 0.5));
style.base_color_focused = ColorToInt(Fade(color, 0.3));
style.base_color_pressed = ColorToInt(Fade(color, 0.8));
style.base_color_disabled = ColorToInt(Fade(color, 0.5));
style.border_color_normal = ColorToInt(Fade(color, 1.0));
style.border_color_focused = ColorToInt(Fade(color, 0.7));
style.border_color_pressed = ColorToInt(Fade(color, 1.0));
style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5));
}
auto user_interface::apply_text_color(style& style, const Color color) -> void
{
style.text_color_normal = ColorToInt(Fade(color, 1.0));
style.text_color_focused = ColorToInt(Fade(color, 1.0));
style.text_color_pressed = ColorToInt(Fade(color, 1.0));
style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5));
}
auto user_interface::get_default_style() -> default_style
{
// Could've iterated over the values, but then it wouldn't be as nice to
// access...
return {
{
GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL),
GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED),
GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED),
GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED),
GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)
},
GuiGetStyle(DEFAULT, BACKGROUND_COLOR), GuiGetStyle(DEFAULT, LINE_COLOR), GuiGetStyle(DEFAULT, TEXT_SIZE),
GuiGetStyle(DEFAULT, TEXT_SPACING), GuiGetStyle(DEFAULT, TEXT_LINE_SPACING),
GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)
};
}
auto user_interface::set_default_style(const default_style& style) -> void
{
GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, style.border_color_normal);
GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, style.base_color_normal);
GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, style.text_color_normal);
GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, style.border_color_focused);
GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, style.base_color_focused);
GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, style.text_color_focused);
GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, style.border_color_pressed);
GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, style.base_color_pressed);
GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, style.text_color_pressed);
GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, style.border_color_disabled);
GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, style.base_color_disabled);
GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, style.text_color_disabled);
GuiSetStyle(DEFAULT, BACKGROUND_COLOR, style.background_color);
GuiSetStyle(DEFAULT, LINE_COLOR, style.line_color);
GuiSetStyle(DEFAULT, TEXT_SIZE, style.text_size);
GuiSetStyle(DEFAULT, TEXT_SPACING, style.text_spacing);
GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, style.text_line_spacing);
GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, style.text_alignment_vertical);
GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, style.text_wrap_mode);
}
auto user_interface::get_component_style(const int component) -> component_style
{
return {
{
GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)
},
GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT)
};
}
auto user_interface::set_component_style(const int component, const component_style& style) -> void
{
GuiSetStyle(component, BORDER_COLOR_NORMAL, style.border_color_normal);
GuiSetStyle(component, BASE_COLOR_NORMAL, style.base_color_normal);
GuiSetStyle(component, TEXT_COLOR_NORMAL, style.text_color_normal);
GuiSetStyle(component, BORDER_COLOR_FOCUSED, style.border_color_focused);
GuiSetStyle(component, BASE_COLOR_FOCUSED, style.base_color_focused);
GuiSetStyle(component, TEXT_COLOR_FOCUSED, style.text_color_focused);
GuiSetStyle(component, BORDER_COLOR_PRESSED, style.border_color_pressed);
GuiSetStyle(component, BASE_COLOR_PRESSED, style.base_color_pressed);
GuiSetStyle(component, TEXT_COLOR_PRESSED, style.text_color_pressed);
GuiSetStyle(component, BORDER_COLOR_DISABLED, style.border_color_disabled);
GuiSetStyle(component, BASE_COLOR_DISABLED, style.base_color_disabled);
GuiSetStyle(component, TEXT_COLOR_DISABLED, style.text_color_disabled);
GuiSetStyle(component, BORDER_WIDTH, style.border_width);
GuiSetStyle(component, TEXT_PADDING, style.text_padding);
GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment);
}
auto user_interface::popup_bounds() -> Rectangle
{
return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f,
static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, POPUP_WIDTH, POPUP_HEIGHT);
}
auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color,
const bool enabled, const int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
const component_style original_button = get_component_style(BUTTON);
// Change styling
default_style style_default = original_default;
component_style style_button = original_button;
style_default.text_size = font_size;
apply_color(style_button, color);
set_default_style(style_default);
set_component_style(BUTTON, style_button);
const int _state = GuiGetState();
if (!enabled || window_open()) {
GuiSetState(STATE_DISABLED);
}
const int pressed = GuiButton(bounds, label.data());
if (!enabled || window_open()) {
GuiSetState(_state);
}
// Restore original styling
set_default_style(original_default);
set_component_style(BUTTON, original_button);
return pressed;
}
auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height,
const std::string& label, const Color color, const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_button(bounds, label, color, enabled, font_size);
}
auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color, bool enabled,
int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
const component_style original_slider = get_component_style(SLIDER);
const component_style original_toggle = get_component_style(TOGGLE);
// Change styling
default_style style_default = original_default;
component_style style_slider = original_slider;
component_style style_toggle = original_toggle;
style_default.text_size = font_size;
apply_color(style_slider, color);
apply_color(style_toggle, color);
set_default_style(style_default);
set_component_style(SLIDER, style_slider);
set_component_style(TOGGLE, style_toggle);
const int _state = GuiGetState();
if (!enabled || window_open()) {
GuiSetState(STATE_DISABLED);
}
int pressed = GuiToggleSlider(bounds, std::format("{};{}", off_label, on_label).data(), active);
if (!enabled || window_open()) {
GuiSetState(_state);
}
// Restore original styling
set_default_style(original_default);
set_component_style(SLIDER, original_slider);
set_component_style(TOGGLE, original_toggle);
return pressed;
}
auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height,
const std::string& off_label, const std::string& on_label, int* active,
const Color color, const bool enabled, const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size);
}
auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color,
bool enabled, int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
const component_style original_valuebox = get_component_style(VALUEBOX);
const component_style original_button = get_component_style(BUTTON);
// Change styling
default_style style_default = original_default;
component_style style_valuebox = original_valuebox;
component_style style_button = original_button;
style_default.text_size = font_size;
apply_color(style_valuebox, color);
apply_color(style_button, color);
set_default_style(style_default);
set_component_style(VALUEBOX, style_valuebox);
set_component_style(BUTTON, style_button);
const int _state = GuiGetState();
if (!enabled || window_open()) {
GuiSetState(STATE_DISABLED);
}
int pressed = GuiSpinner(bounds, "", label.data(), value, min, max, false);
if (!enabled || window_open()) {
GuiSetState(_state);
}
// Restore original styling
set_default_style(original_default);
set_component_style(VALUEBOX, original_valuebox);
set_component_style(BUTTON, style_button);
return pressed;
}
auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height,
const std::string& label, int* value, const int min, const int max,
const Color color, const bool enabled, const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_spinner(bounds, label, value, min, max, color, enabled, font_size);
}
auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled,
const int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
const component_style original_label = get_component_style(LABEL);
// Change styling
default_style style_default = original_default;
component_style style_label = original_label;
style_default.text_size = font_size;
apply_text_color(style_label, color);
set_default_style(style_default);
set_component_style(LABEL, style_label);
const int _state = GuiGetState();
if (!enabled || window_open()) {
GuiSetState(STATE_DISABLED);
}
const int pressed = GuiLabel(bounds, text.data());
if (!enabled || window_open()) {
GuiSetState(_state);
}
// Restore original styling
set_default_style(original_default);
set_component_style(LABEL, original_label);
return pressed;
}
auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color,
const bool enabled) const -> bool
{
component_style s = get_component_style(BUTTON);
apply_block_color(s, color);
const Rectangle bounds = board_grid.square_bounds(x, y, width, height);
const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds);
const bool pressed = puzzle::block(x, y, width, height, false).covers(input.sel_x, input.sel_y);
// Background to make faded colors work
DrawRectangleRec(bounds, RAYWHITE);
Color base = GetColor(s.base_color_normal);
Color border = GetColor(s.base_color_normal);
if (pressed) {
base = GetColor(s.base_color_pressed);
border = GetColor(s.base_color_pressed);
}
if (focused) {
base = GetColor(s.base_color_focused);
border = GetColor(s.base_color_focused);
}
if (focused && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
base = GetColor(s.base_color_pressed);
border = GetColor(s.base_color_pressed);
}
if (!enabled) {
base = BOARD_COLOR_RESTRICTED;
}
DrawRectangleRec(bounds, base);
if (enabled) {
DrawRectangleLinesEx(bounds, 2.0, border);
}
return focused && enabled;
}
auto user_interface::window_open() const -> bool
{
return save_window || help_window || ok_message || yes_no_message;
}
auto user_interface::draw_menu_header(const Color color) const -> void
{
int preset = static_cast<int>(state.get_current_preset());
draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, static_cast<int>(state.get_preset_count()), color,
!input.editing);
if (preset > static_cast<int>(state.get_current_preset())) {
input.load_next_preset();
} else if (preset < static_cast<int>(state.get_current_preset())) {
input.load_previous_preset();
}
draw_menu_button(1, 0, 1, 1, std::format("{}: {}/{} Blocks",
state.was_edited()
? "Modified"
: std::format("\"{}\"", state.get_current_preset_comment().substr(2)),
state.get_current_state().block_count(), puzzle::MAX_BLOCKS), color);
int editing = input.editing;
draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color);
if (editing != input.editing) {
input.toggle_editing();
}
}
auto user_interface::draw_graph_info(const Color color) const -> void
{
draw_menu_button(0, 1, 1, 1, std::format("Found {} States ({} Winning)", state.get_state_count(),
state.get_target_count()), color);
draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color);
draw_menu_button(2, 1, 1, 1, std::format("{} Moves to Nearest Solution",
state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), color);
}
auto user_interface::draw_graph_controls(const Color color) const -> void
{
if (draw_menu_button(0, 2, 1, 1, "Populate Graph (G)", color)) {
input.populate_graph();
}
// int mark_path = input.mark_path;
// DrawMenuToggleSlider(2, 2, 1, 1, "Path Hidden (U)", "Path Shown (U)",
// &mark_path, color);
// if (mark_path != input.mark_path) {
// input.ToggleMarkPath();
// }
if (draw_menu_button(1, 2, 1, 1, "Clear Graph (C)", color)) {
input.clear_graph();
}
int mark_solutions = input.mark_solutions;
draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", &mark_solutions, color);
if (mark_solutions != input.mark_solutions) {
input.toggle_mark_solutions();
}
input.mark_path = input.mark_solutions;
}
auto user_interface::draw_camera_controls(const Color color) const -> void
{
int lock_camera = input.camera_lock;
draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, color);
if (lock_camera != input.camera_lock) {
input.toggle_camera_lock();
}
int lock_camera_mass_center = input.camera_mass_center_lock;
draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color,
input.camera_lock);
if (lock_camera_mass_center != input.camera_mass_center_lock) {
input.toggle_camera_mass_center_lock();
}
int projection = camera.projection == CAMERA_ORTHOGRAPHIC;
draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, color);
if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) {
input.toggle_camera_projection();
}
}
auto user_interface::draw_puzzle_controls(const Color color) const -> void
{
auto nth = [&](const int n)
{
if (n == 11 || n == 12 || n == 13) {
return "th";
}
if (n % 10 == 1) {
return "st";
}
if (n % 10 == 2) {
return "nd";
}
if (n % 10 == 3) {
return "rd";
}
return "th";
};
const int visits = state.get_current_visits();
draw_menu_button(0, 4, 1, 1, std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits,
nth(visits)), color);
if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) {
input.goto_optimal_next_state();
}
if (draw_menu_button(2, 4, 1, 1, "Undo Last Move (Backspace)", color, state.has_history())) {
input.goto_previous_state();
}
if (draw_menu_button(0, 5, 1, 1, "Go to Nearest Solution (B)", color, state.has_distances())) {
input.goto_closest_target_state();
}
if (draw_menu_button(1, 5, 1, 1, "Go to Worst State (V)", color, state.has_distances())) {
input.goto_most_distant_state();
}
if (draw_menu_button(2, 5, 1, 1, "Go to Starting State (R)", color,
state.get_current_index() != state.get_starting_index())) {
input.goto_starting_state();
}
}
auto user_interface::draw_edit_controls(const Color color) const -> void
{
const puzzle& current = state.get_current_state();
// Toggle Target Block
if (draw_menu_button(0, 4, 1, 1, "Toggle Target Block (T)", color)) {
input.toggle_target_block();
}
// Toggle Wall Block
if (draw_menu_button(0, 5, 1, 1, "Toggle Wall Block (Y)", color)) {
input.toggle_wall_block();
}
// Toggle Restricted/Free Block Movement
int free = !current.get_restricted();
draw_menu_toggle_slider(1, 4, 1, 1, "Restricted (F)", "Free (F)", &free, color);
if (free != !current.get_restricted()) {
input.toggle_restricted_movement();
}
// Clear Goal
if (draw_menu_button(1, 5, 1, 1, "Clear Goal (X)", color)) {}
// Column Count Spinner
int columns = current.get_width();
draw_menu_spinner(2, 4, 1, 1, "Cols: ", &columns, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color);
if (columns > current.get_width()) {
input.add_board_column();
} else if (columns < current.get_width()) {
input.remove_board_column();
}
// Row Count Spinner
int rows = current.get_height();
draw_menu_spinner(2, 5, 1, 1, "Rows: ", &rows, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color);
if (rows > current.get_height()) {
input.add_board_row();
} else if (rows < current.get_height()) {
input.remove_board_row();
}
}
auto user_interface::draw_menu_footer(const Color color) -> void
{
draw_menu_button(0, 6, 2, 1, state.get_current_state().string_repr().data(), color);
if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) {
if (const std::optional<std::string>& reason = state.get_current_state().try_get_invalid_reason()) {
message_title = "Can't Save Preset";
message_message = std::format("Invalid Board: {}.", *reason);
ok_message = true;
} else {
save_window = true;
}
}
}
auto user_interface::get_background_color() -> Color
{
return GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR));
}
auto user_interface::help_popup() -> void {}
auto user_interface::draw_save_preset_popup() -> void
{
if (!save_window) {
return;
}
// Returns the pressed button index
const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", "Ok;Cancel",
preset_name.data(), 255, nullptr);
if (button == 1) {
state.append_preset_file(preset_name.data());
}
if (button == 0 || button == 1 || button == 2) {
save_window = false;
TextCopy(preset_name.data(), "\0");
}
}
auto user_interface::draw_ok_message_box() -> void
{
if (!ok_message) {
return;
}
const int button = GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Ok");
if (button == 0 || button == 1) {
message_title = "";
message_message = "";
ok_message = false;
}
}
auto user_interface::draw_yes_no_message_box() -> void
{
if (!yes_no_message) {
return;
}
const int button = GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Yes;No");
if (button == 1) {
yes_no_handler();
}
if (button == 0 || button == 1 || button == 2) {
message_title = "";
message_message = "";
yes_no_message = false;
}
}
auto user_interface::draw_main_menu() -> void
{
menu_grid.update_bounds(0, 0, GetScreenWidth(), MENU_HEIGHT);
draw_menu_header(GRAY);
draw_graph_info(ORANGE);
draw_graph_controls(RED);
draw_camera_controls(DARKGREEN);
if (input.editing) {
draw_edit_controls(PURPLE);
} else {
draw_puzzle_controls(BLUE);
}
draw_menu_footer(GRAY);
}
auto user_interface::draw_puzzle_board() -> void
{
const puzzle& current = state.get_current_state();
board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.get_width(),
current.get_height());
// Draw outer border
const Rectangle bounds = board_grid.square_bounds();
DrawRectangleRec(bounds, current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED);
// Draw inner borders
DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING,
bounds.height - 2 * BOARD_PADDING,
current.get_restricted() ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
// Draw target opening
// TODO: Only draw single direction (in corner) if restricted (use target block principal
// direction)
const std::optional<puzzle::block> target_block = current.try_get_target_block();
const int target_x = current.get_goal_x();
const int target_y = current.get_goal_y();
if (current.get_goal() && target_block) {
auto [x, y, width, height] = board_grid.square_bounds(target_x, target_y, target_block->get_width(),
target_block->get_height());
const Color opening_color = Fade(current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3);
if (target_x == 0) {
// Left opening
DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, RAYWHITE);
DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, opening_color);
}
if (target_x + target_block->get_width() == current.get_width()) {
// Right opening
DrawRectangle(x + width, y, BOARD_PADDING, height, RAYWHITE);
DrawRectangle(x + width, y, BOARD_PADDING, height, opening_color);
}
if (target_y == 0) {
// Top opening
DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, RAYWHITE);
DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, opening_color);
}
if (target_y + target_block->get_height() == current.get_height()) {
// Bottom opening
DrawRectangle(x, y + height, width, BOARD_PADDING, RAYWHITE);
DrawRectangle(x, y + height, width, BOARD_PADDING, opening_color);
}
}
// Draw empty cells. Also set hovered blocks
input.hov_x = -1;
input.hov_y = -1;
for (int x = 0; x < board_grid.columns; ++x) {
for (int y = 0; y < board_grid.rows; ++y) {
DrawRectangleRec(board_grid.square_bounds(x, y, 1, 1), RAYWHITE);
Rectangle hov_bounds = board_grid.square_bounds(x, y, 1, 1);
hov_bounds.x -= BOARD_PADDING;
hov_bounds.y -= BOARD_PADDING;
hov_bounds.width += BOARD_PADDING;
hov_bounds.height += BOARD_PADDING;
if (CheckCollisionPointRec(GetMousePosition() - Vector2(0, MENU_HEIGHT), hov_bounds)) {
input.hov_x = x;
input.hov_y = y;
}
}
}
// Draw blocks
for (const puzzle::block b : current.block_view()) {
Color c = BLOCK_COLOR;
if (b.get_target()) {
c = TARGET_BLOCK_COLOR;
} else if (b.get_immovable()) {
c = WALL_COLOR;
}
const auto [x, y, w, h, t, i] = b.unpack_repr();
draw_board_block(x, y, w, h, c, !i);
}
// Draw block placing
if (input.editing && input.has_block_add_xy) {
if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && input.hov_y >=
input.block_add_y) {
bool collides = false;
for (const puzzle::block b : current.block_view()) {
if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, false))) {
collides = true;
break;
}
}
if (!collides) {
draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, PURPLE);
}
}
}
// TODO: In edit mode
// - Clear Goal button doesn't work
// - Toggle Target Block button throws "Grid bounds out of range"
// - Clicking the goal to remove it throws "Grid bounds out of range"
// Draw goal boundaries when editing
if (input.editing && current.get_goal() && target_block) {
DrawRectangleLinesEx(
board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), 2.0,
TARGET_BLOCK_COLOR);
}
}
auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void
{
graph_overlay_grid.update_bounds(GetScreenWidth() / 2, MENU_HEIGHT);
debug_overlay_grid.update_bounds(GetScreenWidth() / 2, GetScreenHeight() - 75);
draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), std::format("Dist: {:0>7.2f}", camera.distance), BLACK);
draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), BLACK);
draw_label(graph_overlay_grid.bounds(0, 2, 1, 1), std::format("FPS: {:0>3}", fps), LIME);
draw_label(graph_overlay_grid.bounds(0, 3, 1, 1), std::format("UPS: {:0>3}", ups), ORANGE);
// Debug
draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Physics Debug:"), BLACK);
draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), BLACK);
draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), BLACK);
}
auto user_interface::draw(const int fps, const int ups, const size_t mass_count, const size_t spring_count) -> void
{
const auto visitor = overloads{
[&](const show_ok_message& msg)
{
message_title = msg.title;
message_message = msg.message;
ok_message = true;
},
[&](const show_yes_no_message& msg)
{
message_title = msg.title;
message_message = msg.message;
yes_no_handler = msg.on_yes;
yes_no_message = true;
},
[&](const show_save_preset_window& msg)
{
save_window = true;
}
};
while (!input.ui_commands.empty()) {
const ui_command& cmd = input.ui_commands.front();
cmd.visit(visitor);
input.ui_commands.pop();
}
input.disable = window_open();
draw_graph_overlay(fps, ups, mass_count, spring_count);
draw_save_preset_popup();
draw_ok_message_box();
draw_yes_no_message_box();
}

267
test/bits.cpp Normal file
View File

@ -0,0 +1,267 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_template_test_macros.hpp>
#include <cstdint>
#include "util.hpp"
// =============================================================================
// Catch2
// =============================================================================
//
// 1. TEST_CASE(name, tags)
// The basic unit of testing in Catch2. Each TEST_CASE is an independent test
// function. The first argument is a descriptive name (must be unique), and
// the second is a string of tags in square brackets (e.g. "[set_bits]")
// used to filter and group tests when running.
//
// 2. SECTION(name)
// Sections allow multiple subtests within a single TEST_CASE. Each SECTION
// runs the TEST_CASE from the top, so any setup code before the SECTION is
// re-executed fresh for every section. This gives each section an isolated
// starting state without needing separate TEST_CASEs or explicit teardown.
// Sections can also be nested.
//
// 3. REQUIRE(expression)
// The primary assertion macro. If the expression evaluates to false, the
// test fails immediately and Catch2 reports the actual values of both sides
// of the comparison (e.g. "0xF5 == 0xF0" on failure). There is also
// CHECK(), which records a failure but continues executing the rest of the
// test; REQUIRE() aborts the current test on failure.
//
// 4. TEMPLATE_TEST_CASE(name, tags, Type1, Type2, ...)
// A parameterised test that is instantiated once for each type listed.
// Inside the test body, the alias `TestType` refers to the current type.
// This avoids duplicating identical logic for uint8_t, uint16_t, uint32_t,
// and uint64_t. Catch2 automatically appends the type name to the test name
// in the output so you can see which instantiation failed.
//
// 5. Tags (e.g. "[create_mask]", "[round-trip]")
// Tags let you selectively run subsets of tests from the command line.
// For example:
// ./tests "[set_bits]" -- runs only tests tagged [set_bits]
// ./tests "~[round-trip]" -- runs everything except [round-trip]
// ./tests "[get_bits],[set_bits]" -- runs tests matching either tag
//
// =============================================================================
// ---------------------------------------------------------------------------
// create_mask
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]",
uint8_t, uint16_t, uint32_t, uint64_t)
{
SECTION("single bit mask at bit 0") {
auto m = create_mask<TestType>(0, 0);
REQUIRE(m == TestType{0b1});
}
SECTION("single bit mask at bit 3") {
auto m = create_mask<TestType>(3, 3);
REQUIRE(m == TestType{0b1000});
}
SECTION("mask spanning bits 0..7 gives 0xFF") {
auto m = create_mask<TestType>(0, 7);
REQUIRE(m == TestType{0xFF});
}
SECTION("mask spanning bits 4..7") {
auto m = create_mask<TestType>(4, 7);
REQUIRE(m == TestType{0xF0});
}
SECTION("full-width mask returns all ones") {
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
auto m = create_mask<TestType>(0, last);
REQUIRE(m == static_cast<TestType>(~TestType{0}));
}
}
TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") {
REQUIRE(create_mask<uint32_t>(0, 15) == 0x0000FFFF);
REQUIRE(create_mask<uint32_t>(0, 31) == 0xFFFFFFFF);
REQUIRE(create_mask<uint32_t>(16, 31) == 0xFFFF0000);
}
// ---------------------------------------------------------------------------
// clear_bits
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
{
SECTION("clear all bits") {
TestType val = static_cast<TestType>(~TestType{0});
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
clear_bits(val, 0, last);
REQUIRE(val == TestType{0});
}
SECTION("clear lower nibble") {
TestType val = static_cast<TestType>(0xFF);
clear_bits(val, 0, 3);
REQUIRE(val == static_cast<TestType>(0xF0));
}
SECTION("clear upper nibble") {
TestType val = static_cast<TestType>(0xFF);
clear_bits(val, 4, 7);
REQUIRE(val == static_cast<TestType>(0x0F));
}
SECTION("clear single bit") {
TestType val = static_cast<TestType>(0xFF);
clear_bits(val, 0, 0);
REQUIRE(val == static_cast<TestType>(0xFE));
}
SECTION("clearing already-zero bits is a no-op") {
TestType val = TestType{0};
clear_bits(val, 0, 3);
REQUIRE(val == TestType{0});
}
}
// ---------------------------------------------------------------------------
// set_bits
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
{
SECTION("set lower nibble on zero") {
TestType val = TestType{0};
set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0xA));
REQUIRE(val == static_cast<TestType>(0x0A));
}
SECTION("set upper nibble on zero") {
TestType val = TestType{0};
set_bits(val, uint8_t{4}, uint8_t{7}, static_cast<TestType>(0xB));
REQUIRE(val == static_cast<TestType>(0xB0));
}
SECTION("set_bits replaces existing bits") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{0}, uint8_t{3}, static_cast<TestType>(0x5));
REQUIRE(val == static_cast<TestType>(0xF5));
}
SECTION("set single bit to 1") {
TestType val = TestType{0};
set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(1));
REQUIRE(val == static_cast<TestType>(0x08));
}
SECTION("set single bit to 0") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{3}, uint8_t{3}, static_cast<TestType>(0));
REQUIRE(val == static_cast<TestType>(0xF7));
}
SECTION("setting value 0 clears the range") {
TestType val = static_cast<TestType>(0xFF);
set_bits(val, uint8_t{0}, uint8_t{7}, static_cast<TestType>(0));
REQUIRE(val == TestType{0});
}
}
TEST_CASE("set_bits with different value type (U != T)", "[set_bits]") {
uint32_t val = 0;
constexpr uint8_t small_val = 0x3F;
set_bits(val, uint8_t{8}, uint8_t{13}, small_val);
REQUIRE(val == (uint32_t{0x3F} << 8));
}
TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") {
uint32_t val = 0xDEADBEEF;
set_bits(val, uint8_t{8}, uint8_t{15}, uint32_t{0x42});
REQUIRE(val == 0xDEAD42EF);
}
// ---------------------------------------------------------------------------
// get_bits
// ---------------------------------------------------------------------------
TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]",
uint8_t, uint16_t, uint32_t, uint64_t)
{
SECTION("get lower nibble") {
TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, uint8_t{0}, uint8_t{3});
REQUIRE(result == TestType{0xB});
}
SECTION("get upper nibble") {
TestType val = static_cast<TestType>(0xAB);
auto result = get_bits(val, uint8_t{4}, uint8_t{7});
REQUIRE(result == TestType{0xA});
}
SECTION("get single bit that is set") {
TestType val = static_cast<TestType>(0x08);
auto result = get_bits(val, uint8_t{3}, uint8_t{3});
REQUIRE(result == TestType{1});
}
SECTION("get single bit that is clear") {
TestType val = static_cast<TestType>(0xF7);
auto result = get_bits(val, uint8_t{3}, uint8_t{3});
REQUIRE(result == TestType{0});
}
SECTION("get all bits") {
TestType val = static_cast<TestType>(~TestType{0});
constexpr uint8_t last = sizeof(TestType) * 8 - 1;
auto result = get_bits(val, uint8_t{0}, last);
REQUIRE(result == val);
}
SECTION("get from zero returns zero") {
TestType val = TestType{0};
auto result = get_bits(val, uint8_t{0}, uint8_t{7});
REQUIRE(result == TestType{0});
}
}
TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") {
constexpr uint32_t val = 0xDEADBEEF;
REQUIRE(get_bits(val, uint8_t{0}, uint8_t{7}) == 0xEF);
REQUIRE(get_bits(val, uint8_t{8}, uint8_t{15}) == 0xBE);
REQUIRE(get_bits(val, uint8_t{16}, uint8_t{23}) == 0xAD);
REQUIRE(get_bits(val, uint8_t{24}, uint8_t{31}) == 0xDE);
}
// ---------------------------------------------------------------------------
// Round-trip: set then get
// ---------------------------------------------------------------------------
TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") {
uint32_t reg = 0;
set_bits(reg, uint8_t{4}, uint8_t{11}, uint32_t{0xAB});
REQUIRE(get_bits(reg, uint8_t{4}, uint8_t{11}) == 0xAB);
REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{3}) == 0x0);
REQUIRE(get_bits(reg, uint8_t{12}, uint8_t{31}) == 0x0);
}
TEST_CASE("multiple set_bits on different ranges", "[round-trip]") {
uint32_t reg = 0;
set_bits(reg, uint8_t{0}, uint8_t{7}, uint32_t{0x01});
set_bits(reg, uint8_t{8}, uint8_t{15}, uint32_t{0x02});
set_bits(reg, uint8_t{16}, uint8_t{23}, uint32_t{0x03});
set_bits(reg, uint8_t{24}, uint8_t{31}, uint32_t{0x04});
REQUIRE(reg == 0x04030201);
}
TEST_CASE("64-bit round-trip", "[round-trip]") {
uint64_t reg = 0;
set_bits(reg, uint8_t{32}, uint8_t{63}, uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, uint8_t{32}, uint8_t{63}) == uint64_t{0xCAFEBABE});
REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{31}) == uint64_t{0});
}

66
valgrind.supp Normal file
View File

@ -0,0 +1,66 @@
{
glib_malloc
Memcheck:Leak
...
obj:*/libglib-2.0.so*
}
{
gtk_leaks
Memcheck:Leak
...
obj:*/libgtk-3.so*
}
{
mesa_leaks
Memcheck:Leak
...
obj:*/libGLX*
}
{
glfw_leaks
Memcheck:Leak
...
obj:*/libglfw.so*
}
{
wayland_leaks
Memcheck:Leak
...
obj:*/libwayland*.so*
}
{
dbus_leaks
Memcheck:Leak
...
obj:*/libdbus*.so*
}
{
egl_leaks
Memcheck:Leak
...
obj:*/libEGL*.so*
}
{
x11_leaks
Memcheck:Leak
...
obj:*/libX11*
}
{
fontconfig_leaks
Memcheck:Leak
...
obj:*/libfontconfig.so*
}
{
pango_leaks
Memcheck:Leak
...
obj:*/libpango*.so*
}
{
nvidia_leaks
Memcheck:Leak
...
obj:*/libnvidia*.so*
}