From 4359e572c9b52f1c3eea8cf2c86fd31a782c32af Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 10 Jun 2026 04:13:37 +0000 Subject: [PATCH] Phase 1A: rob-twophase solver wrapper with UE integration - Copy rob-twophase sources into Private/ThirdParty/rob-twophase/ (excludes main.cpp; compiles as part of UnrealHyperTwist module) - Add UHyperTwistSolverLibrary with Blueprint-callable functions: - InitializeSolver() / IsSolverInitialized() - SolveClassicState(FaceletString, TimeLimitMs, MaxLength, NumSolutions) -> TArray - GetMoveName(MoveIndex) / GetMoveCount() - VerifyFaceletString(FaceletString) -> bool - Pruning tables saved to ProjectSavedDir for persistence - Add automation tests: initialization, solved-state, scrambled-state - Next: Windows build validation --- .../Private/HyperTwistSolverLibrary.cpp | 135 ++++++ .../Private/ThirdParty/rob-twophase/coord.cpp | 256 +++++++++++ .../Private/ThirdParty/rob-twophase/coord.h | 66 +++ .../Private/ThirdParty/rob-twophase/cubie.cpp | 138 ++++++ .../Private/ThirdParty/rob-twophase/cubie.h | 87 ++++ .../Private/ThirdParty/rob-twophase/face.cpp | 88 ++++ .../Private/ThirdParty/rob-twophase/face.h | 88 ++++ .../Private/ThirdParty/rob-twophase/move.cpp | 349 +++++++++++++++ .../Private/ThirdParty/rob-twophase/move.h | 75 ++++ .../Private/ThirdParty/rob-twophase/prun.cpp | 413 ++++++++++++++++++ .../Private/ThirdParty/rob-twophase/prun.h | 36 ++ .../Private/ThirdParty/rob-twophase/solve.cpp | 305 +++++++++++++ .../Private/ThirdParty/rob-twophase/solve.h | 73 ++++ .../Private/ThirdParty/rob-twophase/sym.cpp | 206 +++++++++ .../Private/ThirdParty/rob-twophase/sym.h | 53 +++ .../Public/HyperTwistSolverLibrary.h | 35 ++ .../Tests/HyperTwistSolverLibraryTest.cpp | 61 +++ 17 files changed, 2464 insertions(+) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSolverLibrary.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistSolverLibraryTest.cpp diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp new file mode 100644 index 0000000..bd6e511 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp @@ -0,0 +1,135 @@ +#include "HyperTwistSolverLibrary.h" + +#include "HAL/PlatformFilemanager.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" + +#include "ThirdParty/rob-twophase/face.h" +#include "ThirdParty/rob-twophase/move.h" +#include "ThirdParty/rob-twophase/cubie.h" +#include "ThirdParty/rob-twophase/coord.h" +#include "ThirdParty/rob-twophase/sym.h" +#include "ThirdParty/rob-twophase/prun.h" +#include "ThirdParty/rob-twophase/solve.h" + +#include + +static bool bSolverInitialized = false; +static std::once_flag InitOnceFlag; + +static void DoInitSolver() +{ + // rob-twophase saves/loads pruning tables from the current working directory. + // Temporarily switch to ProjectSavedDir so tables are persisted across runs. + FString SavedDir = FPaths::ProjectSavedDir(); + FString OriginalDir = FPaths::ProjectDir(); + + if (FPaths::DirectoryExists(SavedDir)) + { + FPlatformProcess::SetCurrentWorkingDirectory(*SavedDir); + } + + face::init(); + move::init(); + coord::init(); + sym::init(); + prun::init(true); // true = try to load tables, generate if missing + + // Restore working directory + FPlatformProcess::SetCurrentWorkingDirectory(*OriginalDir); + + bSolverInitialized = true; +} + +bool UHyperTwistSolverLibrary::IsSolverInitialized() +{ + return bSolverInitialized; +} + +bool UHyperTwistSolverLibrary::InitializeSolver() +{ + std::call_once(InitOnceFlag, DoInitSolver); + return bSolverInitialized; +} + +TArray UHyperTwistSolverLibrary::SolveClassicState( + const FString& FaceletString, + int32 TimeLimitMs, + int32 MaxLength, + int32 NumSolutions +) +{ + TArray Result; + + if (!InitializeSolver()) + { + UE_LOG(LogTemp, Error, TEXT("HyperTwistSolver: Failed to initialize solver")); + return Result; + } + + std::string Facelets(TCHAR_TO_UTF8(*FaceletString)); + cubie::cube Cube; + int Err = face::to_cubie(Facelets, Cube); + if (Err != 0) + { + UE_LOG(LogTemp, Error, TEXT("HyperTwistSolver: Invalid facelet string, error code %d"), Err); + return Result; + } + + if (cubie::check(Cube) != 0) + { + UE_LOG(LogTemp, Error, TEXT("HyperTwistSolver: Cube state is not solvable")); + return Result; + } + + solve::Engine Solver(1, TimeLimitMs, NumSolutions, MaxLength, 1); + Solver.prepare(); + + std::vector> Solutions; + Solver.solve(Cube, Solutions); + Solver.finish(); + + if (Solutions.empty()) + { + UE_LOG(LogTemp, Warning, TEXT("HyperTwistSolver: No solution found within time limit")); + return Result; + } + + // Return the shortest solution + const std::vector& Best = Solutions[0]; + for (int Move : Best) + { + if (Move >= 0 && Move < move::COUNT) + { + Result.Add(FString(UTF8_TO_TCHAR(move::names[Move].c_str()))); + } + } + + return Result; +} + +FString UHyperTwistSolverLibrary::GetMoveName(int32 MoveIndex) +{ + if (MoveIndex >= 0 && MoveIndex < move::COUNT) + { + return FString(UTF8_TO_TCHAR(move::names[MoveIndex].c_str())); + } + return FString(); +} + +int32 UHyperTwistSolverLibrary::GetMoveCount() +{ + return move::COUNT; +} + +bool UHyperTwistSolverLibrary::VerifyFaceletString(const FString& FaceletString) +{ + if (FaceletString.Len() != 54) + { + return false; + } + + std::string Facelets(TCHAR_TO_UTF8(*FaceletString)); + cubie::cube Cube; + return face::to_cubie(Facelets, Cube) == 0; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.cpp new file mode 100644 index 0000000..41f66a4 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.cpp @@ -0,0 +1,256 @@ +#include "coord.h" + +#include +#include +#include + +#include "cubie.h" + +namespace coord { + + const int N_C12K4 = 495; // binom(12, 4) + const int N_PERM4 = 24; // 4! + + uint16_t move_flip[N_FLIP][move::COUNT]; + uint16_t move_twist[N_TWIST][move::COUNT]; + uint16_t move_edges4[N_SLICE][move::COUNT]; + uint16_t move_corners[N_CORNERS][move::COUNT]; + uint16_t move_udedges2[N_UDEDGES2][move::COUNT]; + + /* Used for en-/decoding pos-perm coords */ + uint8_t enc_perm[1 << (4 * 2)]; // encode 4-elem perm as 8 bits + uint8_t dec_perm[N_PERM4]; + uint16_t enc_comb[1 << 12]; // encode 4-elem comb as 12-bit mask with exactly 4 bits on + uint16_t dec_comb[N_C12K4]; + + int binarize_perm(int perm[]) { + int bin = 0; + for (int i = 3; i >= 0; i--) + bin = (bin << 2) | perm[i]; + return bin; + } + + void init_encdec() { + int perm[] = {0, 1, 2, 3}; + for (int i = 0; i < N_PERM4; i++) { + int bin = binarize_perm(perm); + enc_perm[bin] = i; + dec_perm[i] = bin; + std::next_permutation(perm, perm + 4); + } + + int i = 0; + for (int comb = 0; comb < (1 << cubie::edge::COUNT); comb++) { + if (std::bitset(comb).count() == 4) { + enc_comb[comb] = i; + dec_comb[i] = comb; + i++; + } + } + } + + int get_ori(const int oris[], int len, int n_oris) { + int val = 0; + for (int i = 0; i < len - 1; i++) // last ori can be reconstructed by parity + val = n_oris * val + oris[i]; + return val; + } + + void set_ori(int val, int oris[], int len, int n_oris) { + int par = 0; + for (int i = len - 2; i >= 0; i--) { + oris[i] = val % n_oris; + par += oris[i]; + val /= n_oris; + } + // Ori parity must always be 0 + oris[len - 1] = (n_oris - par % n_oris) % n_oris; + } + + // `mask` indicates which 4 edges to compute the coordinate for + int get_combperm(const int cubies[], int len, int mask) { + int min_cubie = ffs(mask) - 1; + + int comb = 0; + int perm = 0; + + for (int i = len - 1; i >= 0; i--) { + if (mask & (1 << cubies[i])) { + comb |= 1 << i; + perm = (perm << 2) | (cubies[i] - min_cubie); + } + } + + return N_PERM4 * enc_comb[comb] + enc_perm[perm]; + } + + void set_combperm(int comb, int perm, int cubies[], int len, int min_cubie) { + comb = dec_comb[comb]; + perm = dec_perm[perm]; + + int cubie = 0; + for (int i = 0; i < len; i++) { + if (cubie == min_cubie) + cubie += 4; + if (comb & (1 << i)) { + cubies[i] = (perm & 0x3) + min_cubie; + perm >>= 2; + } else + cubies[i] = cubie++; + } + } + + /* Faster than using `*_comperm()` twice */ + + int get_perm8(const int cubies[]) { + int comb1 = 0; + int perm1 = 0; + int perm2 = 0; + + for (int i = 7; i >= 0; i--) { + if (cubies[i] < 4) { + comb1 |= 1 << i; + perm1 = (perm1 << 2) | cubies[i]; + } else + perm2 = (perm2 << 2) | (cubies[i] - 4); + } + + comb1 = enc_comb[comb1]; + perm1 = enc_perm[perm1]; + perm2 = enc_perm[perm2]; + return N_PERM4 * (N_PERM4 * comb1 + perm1) + perm2; + } + + void set_perm8(int perm8, int cubies[]) { + int perm2 = dec_perm[perm8 % N_PERM4]; + int comb1 = dec_comb[(perm8 / N_PERM4) / N_PERM4]; + int perm1 = dec_perm[(perm8 / N_PERM4) % N_PERM4]; + + for (int i = 0; i < 8; i++) { + if (comb1 & (1 << i)) { + cubies[i] = perm1 & 0x3; + perm1 >>= 2; + } else { + cubies[i] = (perm2 & 0x3) + 4; + perm2 >>= 2; + } + } + } + + int get_twist(const cubie::cube& c) { + return get_ori(c.cori, cubie::corner::COUNT, 3); + } + + void set_twist(cubie::cube& c, int twist) { + set_ori(twist, c.cori, cubie::corner::COUNT, 3); + } + + int get_flip(const cubie::cube& c) { + return get_ori(c.eori, cubie::edge::COUNT, 2); + } + + void set_flip(cubie::cube& c, int flip) { + set_ori(flip, c.eori, cubie::edge::COUNT, 2); + } + + int get_slice(const cubie::cube& c) { + return get_combperm(c.eperm, cubie::edge::COUNT, 0xf00); + } + + void set_slice(cubie::cube& c, int slice) { + set_combperm(slice / N_PERM4, slice % N_PERM4, c.eperm, cubie::edge::COUNT, cubie::edge::FR); + } + + int get_uedges(const cubie::cube& c) { + return get_combperm(c.eperm, cubie::edge::COUNT, 0x00f); + } + + void set_uedges(cubie::cube& c, int uedges) { + set_combperm(uedges / N_PERM4, uedges % N_PERM4, c.eperm, cubie::edge::COUNT, cubie::edge::UR); + } + + int get_dedges(const cubie::cube& c) { + return get_combperm(c.eperm, cubie::edge::COUNT, 0x0f0); + } + + void set_dedges(cubie::cube& c, int dedges) { + set_combperm(dedges / N_PERM4, dedges % N_PERM4, c.eperm, cubie::edge::COUNT, cubie::edge::DR); + } + + int get_corners(const cubie::cube& c) { + return get_perm8(c.cperm); + } + + void set_corners(cubie::cube& c, int corners) { + set_perm8(corners, c.cperm); + } + + /* Dedicated methods again more efficient than `*_posperm()` */ + + int get_slice1(const cubie::cube& c) { + int slice1 = 0; + for (int i = cubie::edge::COUNT - 1; i >= 0; i--) { + if (c.eperm[i] >= cubie::edge::FR) + slice1 |= 1 << i; + } + return enc_comb[slice1]; + } + + void set_slice1(cubie::cube& c, int slice1) { + slice1 = dec_comb[slice1]; + int j = cubie::edge::FR; + int cubie = 0; + for (int i = 0; i < cubie::edge::COUNT; i++) + c.eperm[i] = (slice1 & (1 << i)) ? j++ : cubie++; + } + + int get_udedges2(const cubie::cube& c) { + return get_perm8(c.eperm); + } + + void set_udedges2(cubie::cube& c, int udedges2) { + set_perm8(udedges2, c.eperm); + } + + // Computing only exactly the moves that are needed and storing them tightly would only make things more complicated + // during solving (in exchange for completely negligible setup/memory-gains) + void init_move( + uint16_t move_coord[][move::COUNT], + int n_coord, + int (*get_coord)(const cubie::cube&), + void (*set_coord)(cubie::cube&, int), + void (*mul)(const cubie::cube&, const cubie::cube&, cubie::cube&), + bool phase2 = false + ) { + cubie::cube c1 = cubie::SOLVED_CUBE; // coords only affect perm or ori -> one would be uninitialized + cubie::cube c2; + + for (int coord = 0; coord < n_coord; coord++) { + set_coord(c1, coord); + + if (phase2) { // UDEDGES2 is only defined for phase 2 moves + for (move::mask moves = move::p2mask; moves; moves &= moves - 1) { + int m = ffsll(moves) - 1; + mul(c1, move::cubes[m], c2); + move_coord[coord][m] = get_coord(c2); + } + } else { + for (int m = 0; m < move::COUNT; m++) { + mul(c1, move::cubes[m], c2); + move_coord[coord][m] = get_coord(c2); + } + } + } + } + + void init() { + init_encdec(); + + init_move(move_flip, N_FLIP, get_flip, set_flip, cubie::edge::mul); + init_move(move_twist, N_TWIST, get_twist, set_twist, cubie::corner::mul); + init_move(move_edges4, N_SLICE, get_slice, set_slice, cubie::edge::mul); + init_move(move_corners, N_CORNERS, get_corners, set_corners, cubie::corner::mul); + init_move(move_udedges2, N_UDEDGES2, get_udedges2, set_udedges2, cubie::edge::mul, true); + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.h new file mode 100644 index 0000000..e2204bd --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/coord.h @@ -0,0 +1,66 @@ +/** + * Coord definitions, utilities and move tables. + */ + +#ifndef __COORD__ +#define __COORD__ + +#include "cubie.h" +#include "move.h" + +namespace coord { + + const int N_FLIP = 2048; // 2^(12 - 1) + const int N_TWIST = 2187; // 3^(8 - 1) + const int N_SLICE1 = 495; // binom(12, 4) + const int N_FSLICE1 = 1013760; // N_FLIP * N_SLICE + + const int N_SLICE = 11880; // 12! / 8! + const int N_UEDGES = 11880; // 12! / 8! + const int N_DEDGES = 11880; // 12! / 8! + + const int N_SLICE2 = 24; // 4! + const int N_UDEDGES2 = 40320; // 8! + const int N_CORNERS = 40320; // 8! + + const int SLICE1_SOLVED = 494; // SLICE1 is not 0 at the end of phase 1 + + extern uint16_t move_flip[N_FLIP][move::COUNT]; + extern uint16_t move_twist[N_TWIST][move::COUNT]; + extern uint16_t move_edges4[N_SLICE][move::COUNT]; + extern uint16_t move_corners[N_CORNERS][move::COUNT]; + extern uint16_t move_udedges2[N_UDEDGES2][move::COUNT]; // primarily for faster phase 2 table generation + + int get_flip(const cubie::cube& c); + int get_twist(const cubie::cube& c); + int get_slice(const cubie::cube& c); + int get_uedges(const cubie::cube& c); + int get_dedges(const cubie::cube& c); + int get_corners(const cubie::cube& c); + + void set_flip(cubie::cube& c, int flip); + void set_twist(cubie::cube& c, int twist); + void set_slice(cubie::cube& c, int slice); + void set_uedges(cubie::cube& c, int uedges); + void set_dedges(cubie::cube& c, int dedges); + void set_corners(cubie::cube& c, int corners); + + int get_slice1(const cubie::cube& c); // faster table generation + void set_slice1(cubie::cube& c, int slice1); + int get_udedges2(const cubie::cube& c); + void set_udedges2(cubie::cube& c, int udedges2); + inline int merge_udedges2(int uedges, int dedges) { return 24 * uedges + (dedges % 24); }; + + inline int slice_to_slice1(int slice) { return slice / 24; } + inline int slice1_to_slice(int slice1) { return 24 * slice1; } + inline int slice_to_slice2(int slice) { return slice - N_SLICE2 * SLICE1_SOLVED; } + inline int slice2_to_slice(int slice2) { return slice2 + N_SLICE2 * SLICE1_SOLVED; } + inline int fslice1(int flip, int slice1) { return N_FLIP * slice1 + flip; } + inline int fslice1_to_flip(int fslice1) { return fslice1 % N_FLIP; } + inline int fslice1_to_slice1(int fslice1) { return fslice1 / N_FLIP; } + + void init(); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.cpp new file mode 100644 index 0000000..09d4ad7 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.cpp @@ -0,0 +1,138 @@ +#include "cubie.h" + +#include +#include +#include "coord.h" + +namespace cubie { + + /* Faster than tricky if-else sequences for handling mirrored states */ + int mul_coris[][6] = { + {0, 1, 2, 3, 4, 5}, + {1, 2, 0, 4, 5, 3}, + {2, 0, 1, 5, 3, 4}, + {3, 5, 4, 0, 2, 1}, + {4, 3, 5, 1, 0, 2}, + {5, 4, 3, 2, 1, 0} + }; + int inv_cori[] = { + 0, 2, 1, 3, 4, 5 + }; + + std::random_device device; + std::mt19937 gen(device()); + + void corner::mul(const cubie::cube& c1, const cubie::cube& c2, cubie::cube& into) { + for (int i = 0; i < corner::COUNT; i++) { + into.cperm[i] = c1.cperm[c2.cperm[i]]; + into.cori[i] = mul_coris[c1.cori[c2.cperm[i]]][c2.cori[i]]; + } + } + + void edge::mul(const cubie::cube& c1, const cubie::cube& c2, cubie::cube& into) { + for (int i = 0; i < edge::COUNT; i++) { + into.eperm[i] = c1.eperm[c2.eperm[i]]; + into.eori[i] = (c1.eori[c2.eperm[i]] + c2.eori[i]) & 1; + } + } + + // Permutation partiy = #inversions % 2 + bool parity(const int perm[], int len) { + int par = 0; + for (int i = 0; i < len; i++) { + for (int j = 0; j < i; j++) { + if (perm[j] > perm[i]) + par++; + } + } + return par & 1; + } + + void mul(const cubie::cube& c1, const cubie::cube& c2, cubie::cube& into) { + corner::mul(c1, c2, into); + edge::mul(c1, c2, into); + } + + void inv(const cube& c, cube& into) { + for (int corner = 0; corner < corner::COUNT; corner++) + into.cperm[c.cperm[corner]] = corner; // inv[a[i]] = i + for (int edge = 0; edge < edge::COUNT; edge++) + into.eperm[c.eperm[edge]] = edge; + for (int i = 0; i < corner::COUNT; i++) + into.cori[i] = inv_cori[c.cori[into.cperm[i]]]; + for (int i = 0; i < edge::COUNT; i++) + into.eori[i] = c.eori[into.eperm[i]]; + } + + int check(const cube& c) { + bool corners[corner::COUNT] = {}; + int cori_sum = 0; + + for (int i = 0; i < corner::COUNT; i++) { + if (c.cperm[i] < 0 || c.cperm[i] >= corner::COUNT) + return 1; // invalid corner cubie + corners[c.cperm[i]] = true; + if (c.cori[i] < 0 || c.cori[i] >= 3) + return 2; // invalid corner orientation + cori_sum += c.cori[i]; + } + if (cori_sum % 3 != 0) + return 3; // invalid twist parity + for (bool corner : corners) { + if (!corner) + return 4; // missing corner + } + + bool edges[edge::COUNT] = {}; + int eori_sum = 0; + + for (int i = 0; i < edge::COUNT; i++) { + if (c.eperm[i] < 0 || c.eperm[i] >= edge::COUNT) + return 5; // invalid edge cubie + edges[c.eperm[i]] = true; + if (c.eori[i] < 0 || c.eori[i] >= 2) + return 6; // invalid edge orientation + eori_sum += c.eori[i]; + } + if ((eori_sum & 1) != 0) + return 7; // invalid flip parity + for (bool edge : edges) { + if (!edge) + return 8; // missing edge + } + + if (parity(c.cperm, corner::COUNT) != parity(c.eperm, edge::COUNT)) + return 9; // corner and edge permutation parity mismatch + return 0; + } + + void shuffle(cube& c) { + for (int i = 0; i < corner::COUNT; i++) + c.cperm[i] = i; + for (int i = 0; i < edge::COUNT; i++) + c.eperm[i] = i; + + coord::set_corners(c, std::uniform_int_distribution(0, coord::N_CORNERS)(gen)); + std::shuffle(c.eperm, c.eperm + edge::COUNT, gen); // no coordinate for all edges + if (parity(c.cperm, corner::COUNT) != parity(c.eperm, edge::COUNT)) + std::swap(c.cperm[corner::COUNT - 2], c.cperm[corner::COUNT - 1]); // flip parity + + coord::set_twist(c, std::uniform_int_distribution(0, coord::N_TWIST - 1)(gen)); + coord::set_flip(c, std::uniform_int_distribution(0, coord::N_FLIP - 1)(gen)); + } + + // We could maybe make this faster, but it is not performance critical anyways + bool operator==(const cube& c1, const cube& c2) { + return + std::equal(c1.cperm, c1.cperm + corner::COUNT, c2.cperm) && + std::equal(c1.eperm, c1.eperm + edge::COUNT, c2.eperm) && + std::equal(c1.cori, c1.cori + corner::COUNT, c2.cori) && + std::equal(c1.eori, c1.eori + edge::COUNT, c2.eori) + ; + } + + bool operator!=(const cube& c1, const cube& c2) { + return !(c1 == c2); + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.h new file mode 100644 index 0000000..10d5b0c --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/cubie.h @@ -0,0 +1,87 @@ +/** + * Cubie definitions, cubie-cube representation + methods for manipulating it + */ + +#ifndef __CUBIE__ +#define __CUBIE__ + +#include + +namespace cubie { + + /* All cubie definitions are plain integers for making uniform handling much easier */ + + namespace corner { // definition of corner cubies + const int COUNT = 8; + + const int URF = 0; + const int UFL = 1; + const int ULB = 2; + const int UBR = 3; + const int DFR = 4; + const int DLF = 5; + const int DBL = 6; + const int DRB = 7; + + const std::string NAMES[] = { + "URF", "UFL", "ULB", "UBR", "DFR", "DLF", "DBL", "DRB" + }; + } + using namespace corner; + + namespace edge { // definition of edge cubies + const int COUNT = 12; + + const int UR = 0; + const int UF = 1; + const int UL = 2; + const int UB = 3; + const int DR = 4; + const int DF = 5; + const int DL = 6; + const int DB = 7; + // SLICE-edges last s.t. UDEDGES2 is easier to handle + const int FR = 8; + const int FL = 9; + const int BL = 10; + const int BR = 11; + + const std::string NAMES[] = { + "UR", "UF", "UL", "UB", "DR", "DF", "DL", "DB", "FR", "FL", "BL", "BR" + }; + } + using namespace edge; + + struct cube { + int cperm[corner::COUNT]; // corner cubie permutation + int eperm[edge::COUNT]; // edge cubie permutation + int cori[corner::COUNT]; // corner cubie orientation; 0 if U/D-facelet on U/D-face; 1 clockwise rot; 2 c-clock + int eori[edge::COUNT]; // edge cubie orientation; 0 if U/D-facelet on U/D-face or same for F/B for slice edges + }; + + const cube SOLVED_CUBE = { + {URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB}, + {UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR}, + {}, {} + }; // cubie-cube in solved state + + /* Explicitly pass result cube to avoid unnecessary copying during table generation */ + + namespace corner { + void mul(const cube& c1, const cube& c2, cube& into); // multiply only corner cubies + } + namespace edge { + void mul(const cube& c1, const cube& c2, cube& into); // multiply only edge cubies + } + + void mul(const cube& c1, const cube& c2, cube& into); // fully multiply two cubes + void inv(const cube& c, cube& into); // compute the inverse cube + void shuffle(cube& c); // generate a uniformly random cube + int check(const cube& c); // check a cube for being solvable + + bool operator==(const cube& c1, const cube& c2); + bool operator!=(const cube& c1, const cube& c2); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.cpp new file mode 100644 index 0000000..d6be0e4 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.cpp @@ -0,0 +1,88 @@ +#include "face.h" + +#include +#include + +namespace face { + + // True modulo function that also works properly for negative numbers + int mod(int a, int m) { + return a > 0 ? a % m : (a % m + m) % m; + } + + // Converts a cubelet and its orientation into a single unique code-number + int encode(const std::string& cubelet, int ori) { + int res = 0; + for (int i = 0; i < cubelet.size(); i++) + res = color::COUNT * res + color::FROM_NAME.at(cubelet[mod(i - ori, cubelet.size())]); + return res; + } + + // Map code to cubie ID and orientation + std::unordered_map> corners; + std::unordered_map> edges; + + void init() { + for (int corner = 0; corner < cubie::corner::COUNT; corner++) { + for (int ori = 0; ori < 3; ori++) + corners[encode(cubie::corner::NAMES[corner], ori)] = std::make_pair(corner, ori); + } + for (int edge = 0; edge < cubie::edge::COUNT; edge++) { + for (int ori = 0; ori < 2; ori++) + edges[encode(cubie::edge::NAMES[edge], ori)] = std::make_pair(edge, ori); + } + } + + int to_cubie(const std::string& s, cubie::cube& c) { + for (int i = 0; i < N_FACELETS; i++) { + if (color::FROM_NAME.find(s[i]) == color::FROM_NAME.end()) + return 1; // invalid color + if ((i - 4) % 9 == 0 && color::FROM_NAME.at(s[i]) != i / 9) + return 2; // invalid center facelet (they are always fixed) + } + + for (int corner = 0; corner < cubie::corner::COUNT; corner++) { + char cornlet[3]; + for (int i = 0; i < 3; i++) + cornlet[i] = s[CORNLETS[corner][i]]; + auto tmp = corners.find(encode(std::string(cornlet, 3), 0)); + if (tmp == corners.end()) + return 3; // invalid corner cubie + c.cperm[corner] = tmp->second.first; + c.cori[corner] = tmp->second.second; + } + + for (int edge = 0; edge < cubie::edge::COUNT; edge++) { + char edgelet[2]; + for (int i = 0; i < 2; i++) + edgelet[i] = s[EDGELETS[edge][i]]; + auto tmp = edges.find(encode(std::string(edgelet, 2), 0)); + if (tmp == edges.end()) + return 4; // invalid edge cubie + c.eperm[edge] = tmp->second.first; + c.eori[edge] = tmp->second.second; + } + + return 0; + } + + // Assumes the given cube to be valid + std::string from_cubie(const cubie::cube& c) { + char s[N_FACELETS]; + + for (int color = 0; color < color::COUNT; color++) + s[9 * color + 4] = color::NAMES[color]; + for (int corner = 0; corner < cubie::corner::COUNT; corner++) { + for (int i = 0; i < 3; i++) + // Corner twist is defined clockwise + s[CORNLETS[corner][i]] = cubie::corner::NAMES[c.cperm[corner]][mod(i - c.cori[corner], 3)]; + } + for (int edge = 0; edge < cubie::edge::COUNT; edge++) { + for (int i = 0; i < 2; i++) + s[EDGELETS[edge][i]] = cubie::edge::NAMES[c.eperm[edge]][mod(i - c.eori[edge], 2)]; + } + + return std::string(s, N_FACELETS); + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.h new file mode 100644 index 0000000..56c2596 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/face.h @@ -0,0 +1,88 @@ +/** + * Since `cubie::cube`s (especially the orientation part) are quite tricky to deal with, we use a more convenient + * representation to interface with the outside world, the face-cube. Having defined an ordering over all 54 stickers + * on the physical cube, a list of the colors for each sticker (in terms of the faces U, R, F, D, L and B not the + * actual cube colors) uniquely specifies any cube-state. + * + * The facelet positions are defined as shown in the folded-up Rubik's cube depicted below. + * + * +--+-----+ + * |U1|U2|U3| + * |--+--+--| + * |U4|U5|U6| + * |--+--+--| + * |U7|U8|U9| + * +--+--+--+--+--+--+--+--+--+--+--+--+ + * |L1|L2|L3|F1|F2|F3|R1|R2|R3|B1|B2|B3| + * |--+--+--|--+--+--|--+--+--|--+--+--| + * |L4|L5|L6|F4|F5|F6|R4|R5|R6|B4|B5|B6| + * |--+--+--|--+--+--|--+--+--|--+--+--| + * |L7|L8|L9|F7|F8|F9|R7|R8|R9|B7|B8|B9| + * +--+--+--+--+--+--+--+--+--+--+--+--+ + * |D1|D2|D3| + * |--+--+--| + * |D4|D5|D6| + * |--+--+--| + * |D7|D8|D9| + * +--+--+--+ + * + * A facelet string simply lists the colors of every facelet position with the faces being in order U, R, F, D, L, B + * and the facelets within a face sorted by their index, i.e. U1U2U3U4U5U6U7U8U9R1R2... where U1, U2, ... are the + * colors of the corresponding facelets. + * + * Note that facelet X5 (i.e. the center sticker of face X) must always be of color X. It also does not matter which + * of the actual cube colors (like red, orange, etc.) is assigned to which face, the assignment must only be consistent + * with respect to the neighborhood relations, i.e. for example if white is considered as the F-face, then yellow must + * be B (as it is always on the opposite side on a physical cube). + */ + +#ifndef __FACE__ +#define __FACE__ + +#include +#include +#include "cubie.h" + +namespace face { + + const int N_FACELETS = 54; // number of facelets (stickers) = 9 * 6 + + namespace color { + const int COUNT = 6; // number of colors/faces of a cube + + /* Color/Face ordering */ + const int U = 0; + const int R = 1; + const int F = 2; + const int D = 3; + const int L = 4; + const int B = 5; + + const char NAMES[] = {'U', 'R', 'F', 'D', 'L', 'B'}; + + // Maps color character to corresponding color ID + const std::unordered_map FROM_NAME = { + {'U', U}, {'R', R}, {'F', F}, {'D', D}, {'L', L}, {'B', B} + }; + } + + /* Map corner/edge IDs to corresponding facelet positions */ + const int CORNLETS[][3] = { + {8, 9, 20}, {6, 18, 38}, {0, 36, 47}, {2, 45, 11}, + {29, 26, 15}, {27, 44, 24}, {33, 53, 42}, {35, 17, 51} + }; + const int EDGELETS[][2] = { + {5, 10}, {7, 19}, {3, 37}, {1, 46}, {32, 16}, {28, 25}, + {30, 43}, {34, 52}, {23, 12}, {21, 41}, {50, 39}, {48, 14} + }; + + /* Routines for converting a facelet-string to a cubie-cube and vice-versa */ + int to_cubie(const std::string& s, cubie::cube &c); + std::string from_cubie(const cubie::cube &c); + + // Initializes the face-level; to be called before accessing anything from this file + void init(); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.cpp new file mode 100644 index 0000000..8486002 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.cpp @@ -0,0 +1,349 @@ +#include "move.h" + +namespace move { + + using namespace cubie::corner; + using namespace cubie::edge; + + /* Select moves and order according to used metric */ + #ifdef QT + #ifdef AX + const int map[] = { + // U, U2, U', D, D2, D' + 0, -1, 1, 2, -1, 3, + // (U D), (U D2), (U D'), (U2 D), (U2 D2), (U2 D'), (U' D), (U' D2), (U' D') + 4, -1, 5, -1, -1, -1, 6, -1, 7, + // R, R2, R', L, L2, L' + 8, 24, 9, 10, 25, 11, + // (R L), (R L2), (R L'), (R2 L), (R2 L2), (R2 L'), (R' L), (R' L2), (R' L') + 12, -1, 13, -1, 26, -1, 14, -1, 15, + // F, F2, F', B, B2, B' + 16, 27, 17, 18, 28, 19, + // (F B), (F B2), (F B'), (F2 B), (F2 B2), (F2 B'), (F' B), (F' B2), (F' B') + 20, -1, 21, -1, 29, -1, 22, -1, 23 + }; + #else + const int map[] = { + 0, -1, 1, 2, -1, 3, + -1, -1, -1, -1, -1, -1, -1, -1, -1, + 4, 12, 5, 6, 13, 7, + -1, -1, -1, -1, -1, -1, -1, -1, -1, + 8, 14, 9, 10, 15, 11, + -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + #endif + #else + #ifdef AX + const int map[] = { + 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44 + }; + #else + const int map[] = { + 0, 1, 2, 3, 4, 5, + -1, -1, -1, -1, -1, -1, -1, -1, -1, + 6, 7, 8, 9, 10, 11, + -1, -1, -1, -1, -1, -1, -1, -1, -1, + 12, 13, 14, 15, 16, 17, + -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + #endif + #endif + + std::string names[COUNT]; + cubie::cube cubes[COUNT]; + int inv[COUNT]; + + mask next[COUNT]; + mask next_p1p2[COUNT]; + mask qt_skip[COUNT]; + + mask p1mask = bit(45) - 1; + mask p2mask = 0x10482097fff; // 000010000 010010 000010000 010010 111111111 111111; + + // For full set of 45 moves no matter the solving mode + std::string names1[45]; + int merge[45][45]; + int unmap[COUNT]; + + // Translate bitmask from full moveset to configured one + mask reindex(mask mm) { + mask mm1 = 0; + for (int m = 0; m < 45; m++) { + if (map[m] != -1 && in(m, mm)) // drop unmapped moves + mm1 |= bit(map[m]); + } + return mm1; + } + + // Build full moveset first, then remap to configured one + void init() { + for (int m = 0; m < 45; m++) { + if (map[m] != -1) + unmap[map[m]] = m; + } + + cubie::cube cubes1[45]; + int inv1[45]; + // Not initializing the following arrays apparently causes problems on MacOS + mask next1[45] = {0}; + mask qt_skip1[45] = {0}; + + std::string fnames[] = {"U", "D", "R", "L", "F", "B"}; + std::string pnames[] = {"", "2", "'"}; + cubie::cube fcubes[] = { + { // U + {UBR, URF, UFL, ULB, DFR, DLF, DBL, DRB}, + {UB, UR, UF, UL, DR, DF, DL, DB, FR, FL, BL, BR}, + {}, {} + }, + { // D + {URF, UFL, ULB, UBR, DLF, DBL, DRB, DFR}, + {UR, UF, UL, UB, DF, DL, DB, DR, FR, FL, BL, BR}, + {}, {} + }, + { // R + {DFR, UFL, ULB, URF, DRB, DLF, DBL, UBR}, + {FR, UF, UL, UB, BR, DF, DL, DB, DR, FL, BL, UR}, + {2, 0, 0, 1, 1, 0, 0, 2}, {} + }, + { // L + {URF, ULB, DBL, UBR, DFR, UFL, DLF, DRB}, + {UR, UF, BL, UB, DR, DF, FL, DB, FR, UL, DL, BR}, + {0, 1, 2, 0, 0, 2, 1, 0}, {} + }, + { // F + {UFL, DLF, ULB, UBR, URF, DFR, DBL, DRB}, + {UR, FL, UL, UB, DR, FR, DL, DB, UF, DF, BL, BR}, + {1, 2, 0, 0, 2, 1, 0, 0}, + {0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0} + }, + { // B + {URF, UFL, UBR, DRB, DFR, DLF, ULB, DBL}, + {UR, UF, UL, BR, DR, DF, DL, BL, FR, FL, UB, DB}, + {0, 0, 1, 2, 0, 0, 2, 1}, + {0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1} + } + }; + + for (int ax = 0; ax < 3; ax++) { + int i1 = 15 * ax; // index to start first face moves + int i2 = 15 * ax + 3; // index to start of second face moves + int i3 = 15 * ax + 6; // index to start of axial moves + + int f1 = 2 * ax; // first face + int f2 = 2 * ax + 1; // second face + + for (int cnt = 0; cnt < 3; cnt++) { + int m = i1 + cnt; + names1[m] = fnames[f1] + pnames[cnt]; + if (cnt == 0) + cubes1[m] = fcubes[f1]; + else + cubie::mul(cubes1[m - 1], fcubes[f1], cubes1[m]); + inv1[m] = i1 + (2 - cnt); + next1[m] |= mask(0x7) << i1; // block any moves on same face + #ifdef AX + next1[m] |= mask(0x38) << i1; // block also moves on opposite face + #endif + #ifdef QT + if (cnt == 0) { // block all axial moves but the ones with M + next1[m] |= mask(0x1ff ^ (0x7 << 3 * cnt)) << i3; + continue; + } + #endif + next1[m] |= mask(0x1ff) << i3; // block all axial moves + } + for (int cnt = 0; cnt < 3; cnt++) { + int m = i2 + cnt; + names1[m] = fnames[f2] + pnames[cnt]; + if (cnt == 0) + cubes1[m] = fcubes[f2]; + else + cubie::mul(cubes1[m - 1], fcubes[f2], cubes1[m]); + inv1[m] = i2 + (2 - cnt); + next1[m] |= mask(0x3f) << i1; // block all simple moves on both faces + #ifdef QT + if (cnt == 0) { // block all axial moves but the ones with M + next1[m] |= mask(0x1ff ^ (0x49 << cnt)) << i3; // 0x49 == 0b1001001 + continue; + } + #endif + next1[m] |= mask(0x1ff) << i3; + } + for (int cnt1 = 0; cnt1 < 3; cnt1++) { + for (int cnt2 = 0; cnt2 < 3; cnt2++) { + int m = i3 + 3 * cnt1 + cnt2; + names1[m] = "(" + names1[i1 + cnt1] + " " + names1[i2 + cnt2] + ")"; + cubie::mul(cubes1[i1 + cnt1], cubes1[i2 + cnt2], cubes1[m]); + inv1[m] = i3 + 3 * (2 - cnt1) + (2 - cnt2); + next1[m] |= mask(0x7fff) << 15 * ax; // block all simple and axial moves + } + } + + qt_skip1[i1] |= bit(i1); + qt_skip1[i1] |= bit(i3); + qt_skip1[i2] |= bit(i2); + qt_skip1[i2] |= bit(i3); + qt_skip1[i3] |= bit(i3); + } + // Half-slice moves commute + next1[25] |= bit(10); + next1[40] |= bit(10) | bit(25); + #ifdef QT + // Allow repetitions of purely clockwise moves + for (int m : {0, 3, 6, 15, 18, 21, 30, 33, 36}) + next1[m] ^= bit(m); + #endif + // Was built by blocking moves, but should actually indicate permitted ones + for (int m = 0; m < 45; m++) + next1[m] = ~next1[m]; + + for (int m = 0; m < 45; m++) { + if (map[m] == -1) + continue; + int i = map[m]; + + names[i] = names1[m]; + cubes[i] = cubes1[m]; + inv[i] = map[inv1[m]]; + next[i] = reindex(next1[m]); + qt_skip[i] = reindex(qt_skip1[m]); + } + + #ifdef QT + // Unmapped moves are skipped automatically during reindexing + p1mask &= ~0x10482090000; // 000010000 010010 000010000 010010 000000000 000000 + #endif + #ifdef F5 + mask tmp = ~(mask(0xfff) << 33); + p1mask &= tmp; + p2mask &= tmp; + #endif + p1mask = reindex(p1mask); + p2mask = reindex(p2mask); + + for (int m = 0; m < COUNT; m++) { + if (p2mask & move::bit(m)) + next_p1p2[m] = next[m]; // we can do normal blocking for phase 2 moves + else { + #ifndef AX + #ifdef QT + next_p1p2[m] = ~(mask(0x3) << 2 * (m / 2)); + if (m / 2 > 1) + next_p1p2[m] &= ~(move::bit(COUNT1 + (m / 2) - 2)); + #else + next_p1p2[m] = ~(mask(0x7) << 3 * (m / 3)); + #endif + #else + next_p1p2[m] = next[m]; // no commutativity problems in axial mode + #endif + } + } + + cubie::cube c; + for (int m1 = 0; m1 < 45; m1++) { + for (int m2 = 0; m2 < 45; m2++) { + merge[m1][m2] = -1; + cubie::mul(cubes1[m1], cubes1[m2], c); + for (int i = 0; i < 45; i++) { + if (c == cubes1[i]) { + merge[m1][m2] = i; + break; + } + } + } + } + } + + void compress1(const std::vector& mseq, std::vector& into) { + into.clear(); + for (int m : mseq) { + m = unmap[m]; + if (into.size() == 0 || merge[into.back()][m] == -1) + into.push_back(m); + else { + int tmp = into.back(); + into.pop_back(); + into.push_back(merge[tmp][m]); + } + } + } + + std::string compress(const std::vector& mseq) { + std::vector comp; + compress1(mseq, comp); + + // Faster string building probably not worth it in a function like this + std::string s; + for (int i = 0; i < comp.size(); i++) { + s += names1[comp[i]]; + if (i != comp.size() - 1) + s += " "; + } + return s; + } + + int len(const std::vector& mseq, int cost[]) { + std::vector comp; + compress1(mseq, comp); + + int res = 0; + for (int m : comp) + res += cost[m]; + return res; + } + + int len_ht(const std::vector& mseq) { + int cost[] = { + 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2 + }; + return len(mseq, cost); + } + + int len_axht(const std::vector& mseq) { + int cost[] = { + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1 + }; + return len(mseq, cost); + } + + int len_qt(const std::vector& mseq) { + int cost[] = { + 1, 2, 1, 1, 2, 1, + 2, 3, 2, 3, 4, 3, 2, 3, 2, + 1, 2, 1, 1, 2, 1, + 2, 3, 2, 3, 4, 3, 2, 3, 2, + 1, 2, 1, 1, 2, 1, + 2, 3, 2, 3, 4, 3, 2, 3, 2 + }; + return len(mseq, cost); + } + + int len_axqt(const std::vector& mseq) { + int cost[] = { + 1, 2, 1, 1, 2, 1, + 1, 2, 1, 2, 2, 2, 1, 2, 1, + 1, 2, 1, 1, 2, 1, + 1, 2, 1, 2, 2, 2, 1, 2, 1, + 1, 2, 1, 1, 2, 1, + 1, 2, 1, 2, 2, 2, 1, 2, 1 + }; + return len(mseq, cost); + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.h new file mode 100644 index 0000000..1604af3 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/move.h @@ -0,0 +1,75 @@ +/** + * All kinds of moveset definitions and setup + */ + +#ifndef __MOVE__ +#define __MOVE__ + +#include +#include +#include + +#include "cubie.h" + +namespace move { + + using mask = uint64_t; + + #ifdef QT + #ifdef AX + const int COUNT = 30; + const int COUNT1 = 24; + const int split[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 8, 10, 12, 16, 18, 20 + }; // split extra half-turns into quarter-turn (for cleaner phase 2 implementation) + #else + const int COUNT = 16; + const int COUNT1 = 12; + const int split[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 4, 6, 8, 10 + }; + #endif + #else + #ifdef AX + const int COUNT = 45; + const int COUNT1 = 45; + #else + const int COUNT = 18; + const int COUNT1 = 18; + #endif + #endif + + extern std::string names[COUNT]; + extern cubie::cube cubes[COUNT]; + extern int inv[COUNT]; + + extern mask next[COUNT]; // successor moves that should be explored + extern mask next_p1p2[COUNT]; // `next` for phase1 to phase 2 transition + extern mask qt_skip[COUNT]; // to avoid ever trying M^3 = M' in QT mode + + extern mask p1mask; // phase 1 moves + extern mask p2mask; // phase 2 moves + + inline mask bit(int m) { + return mask(1) << m; + } + inline bool in(int m, mask mm) { + return mm & bit(m); + } + + // Convert solution to AXHT; especially useful when solving in AXQT + std::string compress(const std::vector& mseq); + + /* Compute solution lengths in different metrics */ + int len_ht(const std::vector& mseq); + int len_axht(const std::vector& mseq); + int len_qt(const std::vector& mseq); + int len_axqt(const std::vector& mseq); + + void init(); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.cpp new file mode 100644 index 0000000..370a7b7 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.cpp @@ -0,0 +1,413 @@ +#include "prun.h" + +#include +#include +#include + +namespace prun { + const std::string SAVE = "twophase-" + #ifdef AX + "ax" + #endif + #ifdef QT + "qt" + #else + "ht" + #endif + #ifdef F5 + "-f5" + #endif + ".tbl" + ; + + const int EMPTY = 0xff; + + #ifdef AX + const int BITS_PER_AX = 16; // bits used for encoding an axis in the ext. phase 1 table + #else + const int BITS_PER_AX = 8; + #endif + #ifdef QT + const int BITS_PER_M = 2; // bits per move + const int N_SIMP = 4; // number of simple moves + const int N_AX = 4; // number of axial moves + #else + const int BITS_PER_M = 1; + const int N_SIMP = 6; + const int N_AX = 9; + #endif + + // Used to remap symmetry ext. phase 1 table entries back to actual situation + move::mask remap[2][16][1 << BITS_PER_AX]; + + prun1 *phase1; + uint8_t *phase2; + uint8_t *precheck; + + inline int ones(int count) { return (1 << count) - 1; } + + int rev(int movec, int count, int off = 0, int step = BITS_PER_M) { + movec >>= step * off; + + int rev = 0; + for (int i = 0; i < step * count; i += step) { + rev = (rev << step) | (movec & ones(step)); + movec >>= step; + } + return rev << step * off; + } + + int inv(int mask) { + int n_per_face = N_SIMP / 2; + + int inv = rev(mask, n_per_face) | rev(mask, n_per_face, n_per_face); + #ifdef AX + inv |= rev(mask, N_AX, N_SIMP); + #endif + return inv; + } + + int flip(int mask) { + int per_face = N_SIMP / 2; + int flipped = rev(mask, 2, 0, BITS_PER_M * per_face); + + #ifdef AX + mask >>= BITS_PER_M * N_SIMP; + + // Flipping an axis means to transpose the axial move-mask (M N) (M N2) (M N') ... to (N M) (N M2) (N M') ... + int fax = 0; + for (int i = 0; i < per_face; i++) { + for (int j = 0; j < per_face; j++) + fax |= ((mask >> BITS_PER_M * (per_face * i + j)) & ones(BITS_PER_M)) << BITS_PER_M * (per_face * j + i); + } + flipped |= fax << BITS_PER_M * N_SIMP; + #endif + + return flipped; + } + + void init_base() { + /* It is probably cleanest to simply handle the special HT case individually */ + #ifndef AX + #ifndef QT + for (int eff = 0; eff < 16; eff++) { + for (int mask = 0; mask < 256; mask++) { + int mask1 = mask & 0xf; + int mask2 = (mask & 0xf0) >> 4; + + if (sym::eff_inv(eff)) { + mask1 = rev(mask1, 3, 1) | (mask1 & 1); + mask2 = rev(mask2, 3, 1) | (mask2 & 1); + } + if (sym::eff_flip(eff)) + std::swap(mask1, mask2); + + remap[0][eff][mask] = ((mask1 & 1) ? 0 : ~(mask1 >> 1) & 0x7) << 6 * sym::eff_shift(eff); + remap[1][eff][mask] = ((mask1 & 1) ? ~(mask1 >> 1) & 0x7 : 0x7) << 6 * sym::eff_shift(eff); + remap[0][eff][mask] |= ((mask2 & 1) ? 0 : ~(mask2 >> 1) & 0x7) << 6 * sym::eff_shift(eff) + 3; + remap[1][eff][mask] |= ((mask2 & 1) ? ~(mask2 >> 1) & 0x7 : 0x7) << 6 * sym::eff_shift(eff) + 3; + } + } + return; + #endif + #endif + + for (int eff = 0; eff < 16; eff++) { + for (int mask = 0; mask < (1 << BITS_PER_AX); mask++) { + move::mask mask1 = mask; + #ifndef QT + mask1 >>= 1; // first bit encodes direction in HT + #endif + + if (sym::eff_inv(eff)) + mask1 = inv(mask1); + if (sym::eff_flip(eff)) + mask1 = flip(mask1); + + #ifdef QT + remap[0][eff][mask] = 0; + remap[1][eff][mask] = 0; + for (int i = 0; i < BITS_PER_AX / 2; i++) { + remap[0][eff][mask] |= ((mask1 & 0x3) == 0) << i; + remap[1][eff][mask] |= ((mask1 & 0x3) <= 1) << i; + mask1 >>= 2; + } + remap[0][eff][mask] <<= (BITS_PER_AX / 2) * sym::eff_shift(eff); + remap[1][eff][mask] <<= (BITS_PER_AX / 2) * sym::eff_shift(eff); + #else + move::mask o = ones(BITS_PER_AX - 1); // first bit indicates direction but is not a move + remap[0][eff][mask] = ((mask & 1) ? 0 : ~mask1 & o) << (BITS_PER_AX - 1) * sym::eff_shift(eff); + remap[1][eff][mask] = ((mask & 1) ? ~mask1 & o : o) << (BITS_PER_AX - 1) * sym::eff_shift(eff); + #endif + } + } + } + + void init_phase1() { + int n_moves = std::bitset<64>(move::p1mask).count(); // make sure not to consider B-moves in F5-mode + + phase1 = new prun1[N_FS1TWIST]; + std::fill(phase1, phase1 + N_FS1TWIST, EMPTY); + + phase1[coord::N_TWIST * sym::coord_c(sym::fslice1_sym[coord::fslice1(0, coord::SLICE1_SOLVED)])] = 0; + int count = 0; + int dist = 0; + + while (count < N_FS1TWIST) { + int coord = 0; + + for (int fs1sym = 0; fs1sym < sym::N_FSLICE1; fs1sym++) { + int fslice1 = sym::fslice1_raw[fs1sym]; + int flip = coord::fslice1_to_flip(fslice1); + int slice = coord::slice1_to_slice(coord::fslice1_to_slice1(fslice1)); + + for (int twist = 0; twist < coord::N_TWIST; twist++) { + if ((phase1[coord] & 0xff) == dist) { + count++; + int deltas[move::COUNT1]; // easier encoding if B-face always exists (F5-mode ignores it anyways) + + for (int m = 0; m < n_moves; m++) { + int slice11 = coord::slice_to_slice1(coord::move_edges4[slice][m]); + int fslice11 = coord::fslice1(coord::move_flip[flip][m], slice11); + int tmp = sym::fslice1_sym[fslice11]; + int twist1 = sym::conj_twist[coord::move_twist[twist][m]][sym::coord_s(tmp)]; + int fs1sym1 = sym::coord_c(tmp); + int coord1 = coord::N_TWIST * fs1sym1 + twist1; + + if (phase1[coord1] == EMPTY) + phase1[coord1] = dist + 1; + deltas[m] = (phase1[coord1] & 0xff) - dist; + coord1 -= twist1; // only TWIST part changes below + + int selfs = sym::fslice1_selfs[fs1sym1] >> 1; + for (int s = 1; selfs > 0; s++) { // bit 0 is always on + if (selfs & 1) { + int coord2 = coord1 + sym::conj_twist[twist1][s]; + if (phase1[coord2] == EMPTY) + phase1[coord2] = dist + 1; + } + selfs >>= 1; + } + } + + prun1 prun = 0; + #ifdef QT + // In QT there is enough space to simply encode the effect of every move in 2 bits + for (int m = n_moves - 1; m >= 0; m--) + prun = (prun << 2) | (deltas[m] + 1); + #else + #ifndef AX + int n_ax = 6; // in standard (HT) mode we have to treat every face as an individual axis for encoding + int bits_per_ax = 4; + #else + int n_ax = 3; + int bits_per_ax = BITS_PER_AX; + #endif + /* Encode from left to right to preserve indexing of moves */ + for (int ax = n_ax - 1; ax >= 0; ax--) { + bool away = false; // first bit of axis encoding (whether any move brings us further from the goal) + for (int i = ax * (bits_per_ax - 1); i < (ax + 1) * (bits_per_ax - 1); i++) { + if (deltas[i] != 0) { + if (deltas[i] > 0) + away = true; + break; // stop immediately once we found a value != 0 + } + } + + int tmp = 0; + for (int i = (ax + 1) * (bits_per_ax - 1) - 1; i >= ax * (bits_per_ax - 1); i--) + tmp = (tmp | (away ? deltas[i] : deltas[i] + 1)) << 1; + tmp |= away; + + prun = (prun << bits_per_ax) | tmp; + } + #endif + phase1[coord] |= prun << 8; + } + coord++; + } + } + + std::cout << dist << " " << count << std::endl; + dist++; + } + } + + void init_phase2() { + phase2 = new uint8_t[N_CORNUD2]; + std::fill(phase2, phase2 + N_CORNUD2, EMPTY); + + phase2[0] = 0; + int count = 0; + int dist = 0; + + while (count < N_CORNUD2) { + int coord = 0; + + for (int csym = 0; csym < sym::N_CORNERS; csym++) { + int corners = sym::corners_raw[csym]; + + for (int udedges2 = 0; udedges2 < coord::N_UDEDGES2; udedges2++) { + if (phase2[coord] == dist) { + count++; + + for (move::mask moves = move::p2mask; moves; moves &= moves - 1) { + int m = ffsll(moves) - 1; + + int dist1 = dist + 1; + #ifdef QT + if (m >= move::COUNT1) + dist1++; // half-turns cost 2 in QTM + #endif + + int corners1 = coord::move_corners[corners][m]; + int udedges21 = coord::move_udedges2[udedges2][m]; + int tmp = sym::corners_sym[corners1]; + udedges21 = sym::conj_udedges2[udedges21][sym::coord_s(tmp)]; + int csym1 = sym::coord_c(tmp); + int coord1 = coord::N_UDEDGES2 * csym1 + udedges21; + + if (phase2[coord1] <= dist1) + continue; + phase2[coord1] = dist1; + coord1 -= udedges21; + + int selfs = sym::corners_selfs[csym1] >> 1; + for (int s = 1; selfs > 0; s++) { + if (selfs & 1) { + int coord2 = coord1 + sym::conj_udedges2[udedges21][s]; + if (phase2[coord2] > dist1) + phase2[coord2] = dist1; + } + selfs >>= 1; + } + } + } + coord++; + } + } + + std::cout << dist << " " << count << std::endl; + dist++; + } + } + + void init_precheck() { + precheck = new uint8_t[N_CSLICE2]; + std::fill(precheck, precheck + N_CSLICE2, EMPTY); + + precheck[0] = 0; + int dist = 0; + int count = 0; + + while (count < N_CSLICE2) { + int coord = 0; + + for (int corners = 0; corners < coord::N_CORNERS; corners++) { + for (int slice2 = 0; slice2 < coord::N_SLICE2; slice2++) { + if (precheck[coord] == dist) { + count++; + int slice = coord::slice2_to_slice(slice2); + + for (move::mask moves = move::p2mask; moves; moves &= moves - 1) { + int m = ffsll(moves) - 1; + + int dist1 = dist + 1; + #ifdef QT + if (m >= move::COUNT1) + dist1++; // half-turns cost 2 in QTM + #endif + + int corners1 = coord::move_corners[corners][m]; + int slice21 = coord::slice_to_slice2(coord::move_edges4[slice][m]); + + int coord1 = coord::N_SLICE2 * corners1 + slice21; + if (precheck[coord1] > dist1) + precheck[coord1] = dist1; + } + } + coord++; + } + } + + std::cout << dist << " " << count << std::endl; + dist++; + } + } + + int get_phase1(int flip, int slice, int twist, int togo, move::mask& next) { + int tmp = sym::fslice1_sym[coord::fslice1(flip, coord::slice_to_slice1(slice))]; + int s = sym::coord_s(tmp); + prun1 prun = phase1[coord::N_TWIST * sym::coord_c(tmp) + sym::conj_twist[twist][s]]; + + int dist = prun & 0xff; + int delta = togo - dist; + + // `delta` < 0 case can never happen during a real search + if (delta > 1) + next = move::p1mask; // all moves are possible + else { + prun >>= 8; // get rid of dist + next = 0; + for (int ax = 0; ax < 3; ax++) { + next |= remap[delta][sym::effect[s][ax]][prun & ones(BITS_PER_AX)]; + prun >>= BITS_PER_AX; + } + } + + return dist; + } + + int get_phase2(int corners, int udedges) { + int tmp = sym::corners_sym[corners]; + return phase2[coord::N_UDEDGES2 * sym::coord_c(tmp) + sym::conj_udedges2[udedges][sym::coord_s(tmp)]]; + } + + int get_precheck(int corners, int slice) { + return precheck[coord::N_SLICE2 * corners + coord::slice_to_slice2(slice)]; + } + + bool init(bool file) { + init_base(); + + if (!file) { + init_phase1(); + init_phase2(); + init_precheck(); + return true; + } + + FILE *f = fopen(SAVE.c_str(), "rb"); + int err = 0; + + if (f == NULL) { + init_phase1(); + init_phase2(); + init_precheck(); + + f = fopen(SAVE.c_str(), "wb"); + if (fwrite(phase1, sizeof(prun1), N_FS1TWIST, f) != N_FS1TWIST) + err = 1; + if (fwrite(phase2, sizeof(uint8_t), N_CORNUD2, f) != N_CORNUD2) + err = 1; + if (fwrite(precheck, sizeof(uint8_t), N_CSLICE2, f) != N_CSLICE2) + err = 1; + if (err) + remove(SAVE.c_str()); // delete file if there was some error writing it + } else { + phase1 = new prun1[N_FS1TWIST]; + phase2 = new uint8_t[N_CORNUD2]; + precheck = new uint8_t[N_CSLICE2]; + if (fread(phase1, sizeof(prun1), N_FS1TWIST, f) != N_FS1TWIST) + err = 1; + if (fread(phase2, sizeof(uint8_t), N_CORNUD2, f) != N_CORNUD2) + err = 1; + if (fread(precheck, sizeof(uint8_t), N_CSLICE2, f) != N_CSLICE2) + err = 1; + } + + fclose(f); + return err; + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.h new file mode 100644 index 0000000..c52a172 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/prun.h @@ -0,0 +1,36 @@ +/** + * Pruning table generation and lookup. + */ + +#ifndef __PRUN__ +#define __PRUN__ + +#include +#include "coord.h" +#include "sym.h" + +namespace prun { + + const int N_FS1TWIST = sym::N_FSLICE1 * coord::N_TWIST; + const int N_CORNUD2 = sym::N_CORNERS * coord::N_UDEDGES2; + const int N_CSLICE2 = coord::N_CORNERS * coord::N_SLICE2; + + #ifdef AX + using prun1 = uint64_t; + #else + using prun1 = uint32_t; + #endif + + extern prun1 *phase1; + extern uint8_t *phase2; + extern uint8_t *precheck; + + int get_phase1(int flip, int slice, int twist, int togo, move::mask& next); + int get_phase2(int corners, int udedges); + int get_precheck(int corners, int slice); + + bool init(bool file = true); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.cpp new file mode 100644 index 0000000..d7be9e5 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.cpp @@ -0,0 +1,305 @@ +#include "solve.h" + +#include +#include +#include +#include "prun.h" +#include "sym.h" + +namespace solve { + + class Search { + + int dir; // ID of search direction + const coordc& cube; // starting position + int p1depth; // phase 1 search depth + move::mask d0moves; // mask for initial moves to consider + bool& done; // when to terminate the search + int& lenlim; // only find strictly shorter solutions + Engine& solver; // report solutions to + + /* Keep track of reconstructed edges that remain valid in the current search path */ + int uedges[50]; + int dedges[50]; + int edges_depth; + + int moves[50]; // current (partial) solution + + private: + void phase1( + int depth, int togo, int flip, int slice, int twist, int corners, move::mask next, move::mask qt_skip + ); // phase 1 search; iterates through all solution with exactly `togo` moves + bool phase2( + int depth, int togo, int slice, int udedges2, int corners, move::mask next, move::mask qt_skip + ); // phase 2 search; returns once any solution is found + + public: + Search( + int dir, + const coordc& cube, + int p1depth, move::mask d0moves, + bool& done, int& lenlim, Engine& solver + ) : dir(dir), cube(cube), p1depth(p1depth), d0moves(d0moves), done(done), lenlim(lenlim), solver(solver) {}; + void run(); // perform the search + + }; + + void Search::run() { + uedges[0] = cube.uedges; + dedges[0] = cube.dedges; + edges_depth = 0; + + move::mask next; + prun::get_phase1(cube.flip, cube.slice, cube.twist, p1depth, next); + next &= move::p1mask & d0moves; // block B-moves in F5 mode here and select current search split + phase1(0, p1depth, cube.flip, cube.slice, cube.twist, cube.corners, next, 0); + } + + void Search::phase1( + int depth, int togo, int flip, int slice, int twist, int corners, move::mask next, move::mask qt_skip + ) { + if (done) + return; + if (togo == 0) { + int tmp = prun::get_precheck(corners, slice); + if (tmp >= lenlim - depth) // phase 2 precheck, only reconstruct edges if successful + return; + + for (int i = edges_depth + 1; i <= depth; i++) { + uedges[i] = coord::move_edges4[uedges[i - 1]][moves[i - 1]]; + dedges[i] = coord::move_edges4[dedges[i - 1]][moves[i - 1]]; + } + edges_depth = depth - 1; + int udedges2 = coord::merge_udedges2(uedges[depth], dedges[depth]); + + int delta = 1; + #ifndef AX + #ifdef QT + delta++; // in vanilla QT mode the perm-parity indicates whether solution length is odd or even + #endif + #endif + for (int togo1 = std::max(prun::get_phase2(corners, udedges2), tmp); togo1 < lenlim - depth; togo1 += delta) { + if (phase2(depth, togo1, slice, udedges2, corners, move::p2mask & move::next_p1p2[moves[depth - 1]], qt_skip)) + return; // once we have found a phase 2 solution, there cannot be any shorter ones -> quit + } + return; + } + + depth++; + togo--; + while (next) { + int m = ffsll(next) - 1; // get rightmost move index (`ffsll()` uses 1-based indexing) + next &= next - 1; + + int flip1 = coord::move_flip[flip][m]; + int slice1 = coord::move_edges4[slice][m]; + int twist1 = coord::move_twist[twist][m]; + move::mask next1; + int dist1 = prun::get_phase1(flip1, slice1, twist1, togo, next1); + + // Check inside loop to avoid unnecessary recursion unwinds + if (dist1 == togo || dist1 + togo >= 5) { // Rokicki optimization + int corners1 = coord::move_corners[corners][m]; + moves[depth - 1] = m; + + next1 &= move::p1mask & move::next[m]; + move::mask qt_skip1; + #ifdef QT // let `qt_skip` get completely optimized away when not in QT-mode + qt_skip1 = move::qt_skip[m]; + next1 &= ~(qt_skip & qt_skip1); + #endif + phase1(depth, togo, flip1, slice1, twist1, corners1, next1, qt_skip1); + } + } + + // We always want to maintain the maximum number of already reconstructed EDGES coordinates, hence we only + // decrement when the depth level gets lower than the current valid index (note that we will typically also + // visit other deeper branches in between that might not have any effect on this) + if (edges_depth == depth - 1) + edges_depth--; + } + + bool Search::phase2( + int depth, int togo, int slice, int udedges2, int corners, move::mask next, move::mask qt_skip + ) { + if (togo == 0) { + if (slice != coord::N_SLICE2 * coord::SLICE1_SOLVED) // check if SLICE2 is also solved + return false; + + searchres sol = {std::vector(depth), dir }; + for (int i = 0; i < depth; i++) + sol.first[i] = moves[i]; + solver.report_sol(sol); + + return true; // we will not find any shorter solutions + } + + while (next) { + int m = ffsll(next) - 1; // get rightmost move index (`ffsll()` uses 1-based indexing) + next &= next - 1; + + int slice1 = coord::move_edges4[slice][m]; + int udedges21 = coord::move_udedges2[udedges2][m]; + int corners1 = coord::move_corners[corners][m]; + + if (prun::get_phase2(corners1, udedges21) < togo) { + #ifdef QT + // As we never want to leave the set of phase 2 cubes (which we would by doing only a quarter-turn on an axis + // for which only double-moves are permitted), we need special handling of the double moves. The simplest way + // to do this is to treat a double moves simply as if two consecutive quarter-turns were added to the current + // search path. + if (m >= move::COUNT1) { + if (togo <= 1) // we cannot do half turns when only a single quarter-turn is permitted + break; + + int tmp = move::split[m]; + moves[depth] = tmp; + moves[depth + 1] = tmp; + + move::mask next1 = move::p2mask & move::next[m]; + move::mask qt_skip1 = move::qt_skip[m]; + next1 &= ~(qt_skip & qt_skip1); + + if (phase2(depth + 2, togo - 2, slice1, udedges21, corners1, next1, qt_skip1)) + return true; + continue; + } + #endif + + moves[depth] = m; + if (phase2(depth + 1, togo - 1, slice1, udedges21, corners1, move::p2mask & move::next[m], 0)) + return true; // return as soon as we have a solution + } + } + + return false; + } + + Engine::Engine( + int n_threads, int tlim, + int n_sols, int max_len, int n_splits + ) : n_threads(n_threads), tlim(tlim), n_sols(n_sols), max_len(max_len), n_splits(n_splits) { + int tmp = (move::COUNT1 + n_splits - 1) / n_splits; // ceil to make sure that we always include all moves + for (int i = 0; i < n_splits; i++) + masks[i] = (move::mask(1) << tmp) - 1 << tmp * i; + done = true; // make sure that the first `prepare()` will actually do something + } + + void Engine::thread() { + int mindir = 0; + do { + /* Select next job to execute; don't forget to lock */ + job_mtx.lock(); + for (int dir = 0; dir < N_DIRS; dir++) { + if (depths[dir] < depths[mindir]) + mindir = dir; + } + int split = splits[mindir]++; + int togo = depths[mindir]; + if (splits[mindir] == n_splits) { + depths[mindir]++; + splits[mindir] = 0; + } + job_mtx.unlock(); + + Search search(mindir, dirs[mindir], togo, masks[split], done, lenlim, *this); + search.run(); + } while (!done); // we should never actually get to the truly optimal depth anyways in general + } + + void Engine::prepare() { + if (!done) // avoid double preparation + return; + finish(); + + job_mtx.lock(); // make spawned threads wait for initialization of the cube to be solved + for (int i = 0; i < n_threads; i++) + threads.push_back(std::thread([&]() { this->thread(); })); + + done = false; + lenlim = max_len > 0 ? max_len + 1: 50; // only search for strictly shorter solutions than this + // `sols` is always emptied after a solve + } + + void Engine::solve(const cubie::cube& c, std::vector>& res) { + prepare(); // make sure we are prepared; will do nothing if that should already be the case + + cubie::cube tmp1, tmp2; + cubie::cube invc; + cubie::inv(c, invc); + + for (int dir = 0; dir < N_DIRS; dir++) { + const cubie::cube& c1 = (dir & 1) ? invc : c; // reference is enough, we do not need to copy + int rot = sym::ROT * (dir / 2); + cubie::mul(sym::cubes[sym::inv[rot]], c1, tmp1); + cubie::mul(tmp1, sym::cubes[rot], tmp2); + + dirs[dir].flip = coord::get_flip(tmp2); + dirs[dir].slice = coord::get_slice(tmp2); + dirs[dir].twist = coord::get_twist(tmp2); + dirs[dir].uedges = coord::get_uedges(tmp2); + dirs[dir].dedges = coord::get_dedges(tmp2); + dirs[dir].corners = coord::get_corners(tmp2); + + move::mask tmp; // simply ignore, makes no sense anyways without proper `togo` + depths[dir] = prun::get_phase1(dirs[dir].flip, dirs[dir].slice, dirs[dir].twist, 100, tmp); + splits[dir] = 0; + } + + job_mtx.unlock(); // start solving + + { // timeout + std::unique_lock lock(tout_mtx); + tout_cvar.wait_for(lock, std::chrono::milliseconds(tlim), [&]{ return done; }); + if (!done) + done = true; // if we get here, this was a timeout + } + std::lock_guard lock(sol_mtx); // make sure no thread is writing any more solutions + + res.resize(sols.size()); + for (int i = 0; i < res.size(); i++) { + const searchres& sol = sols.top(); + res[i].resize(sol.first.size()); + + int rot = sym::ROT * (sol.second / 2); + for (int j = 0; j < res[i].size(); j++) // undo rotation + res[i][j] = sym::conj_move[sol.first[j]][rot]; + if (sol.second & 1) { // undo inversion + for (int j = 0; j < res[i].size(); j++) + res[i][j] = move::inv[res[i][j]]; + std::reverse(res[i].begin(), res[i].end()); + } + + sols.pop(); + } + std::reverse(res.begin(), res.end()); // return solutions in order of increasing length + } + + void Engine::report_sol(searchres& sol) { + std::lock_guard lock(sol_mtx); + + if (done) // prevent any type of reporting after the solver has terminated (important for threading) + return; + + sols.push(sol); // usually we only get here if we actually have a solution that will be added + if (sols.size() > n_sols) + sols.pop(); + if (sols.size() == n_sols) { + lenlim = sols.top().first.size(); // only search for strictly shorter solutions + + if (lenlim <= max_len) { // already found a solution that is short enough + done = true; // end searching + // Wake up timeout + std::lock_guard lock(tout_mtx); + tout_cvar.notify_one(); + } + } + } + + void Engine::finish() { + for (std::thread& t : threads) // wait for all existing threads to actually finish + t.join(); + threads.clear(); // they are now invalid + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.h new file mode 100644 index 0000000..9d158bc --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/solve.h @@ -0,0 +1,73 @@ +#ifndef __SOLVE__ +#define __SOLVE__ + +#include +#include +#include +#include +#include +# include "move.h" + +namespace solve { + + using searchres = std::pair, int>; // moves + search direction + inline bool cmp(const searchres& s1, const searchres& s2) { return s1.first.size() < s2.first.size(); } + + // Container with coords of a starting position + struct coordc { + int flip; + int slice; + int twist; + int uedges; + int dedges; + int corners; + }; + + // Number of search directions + #ifdef F5 + const int N_DIRS = 4; + #else + const int N_DIRS = 6; + #endif + + class Engine { + + int n_threads; // number of search threads + int n_splits; // number of sub-searches every search is split into + int n_sols; // number of solutions to find + int max_len; // find solutions with at most this length; -1 means simply search for the full `tlimit` + int tlim; // search for this amount of milliseconds + + coordc dirs[N_DIRS]; // search directions + move::mask masks[move::COUNT1]; // split masks + int depths[N_DIRS]; // current search depths per direction + int splits[N_DIRS]; // current search splits per direction + + bool done; // indicate that we are done + int lenlim; // only look for solution that are strictly shorter than this + std::mutex job_mtx; // thread-safety for selection of the next search task + std::mutex sol_mtx; // thread-safety for reporting a solution + std::priority_queue, decltype(&cmp)> sols {cmp}; // already found solutions + std::vector threads; // search threads + + // Tools for implementing a required timeout + std::mutex tout_mtx; + std::condition_variable tout_cvar; + + public: + Engine( + int n_threads, int tlim, + int n_sols = 1, int max_len = -1, int n_splits = 1 + ); + void prepare(); // setup all threads + void solve(const cubie::cube& c, std::vector>& res); // actual solve + void finish(); // wait for all threads to shutdown (mostly for clean program exit) + void report_sol(searchres& sol); // report a solution; never call this from the outside + + void thread(); // search thread + + }; + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.cpp new file mode 100644 index 0000000..6aac23f --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.cpp @@ -0,0 +1,206 @@ +#include "sym.h" + +namespace sym { + + using namespace cubie::corner; + using namespace cubie::edge; + + const uint32_t EMPTY = ~uint32_t(0); + + cubie::cube cubes[COUNT]; + int inv[COUNT]; + int effect[COUNT][3]; + + int conj_move[move::COUNT][COUNT]; + uint16_t conj_twist[coord::N_TWIST][COUNT_SUB]; + uint16_t conj_udedges2[coord::N_UDEDGES2][COUNT_SUB]; + + uint32_t fslice1_sym[coord::N_FSLICE1]; + uint32_t corners_sym[coord::N_CORNERS]; + uint32_t fslice1_raw[N_FSLICE1]; + uint16_t corners_raw[N_CORNERS]; + uint16_t fslice1_selfs[N_FSLICE1]; + uint16_t corners_selfs[N_CORNERS]; + + void init_base() { + cubie::cube c = cubie::SOLVED_CUBE; + cubie::cube tmp; + + cubie::cube lr2 = { + {UFL, URF, UBR, ULB, DLF, DFR, DRB, DBL}, + {UL, UF, UR, UB, DL, DF, DR, DB, FL, FR, BR, BL}, + {3, 3, 3, 3, 3, 3, 3, 3}, {} // special mirror ori + }; + cubie::cube u4 = { + {UBR, URF, UFL, ULB, DRB, DFR, DLF, DBL}, + {UB, UR, UF, UL, DB, DR, DF, DL, BR, FR, FL, BL}, + {}, {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1} + }; + cubie::cube f2 = { + {DLF, DFR, DRB, DBL, UFL, URF, UBR, ULB}, + {DL, DF, DR, DB, UL, UF, UR, UB, FL, FR, BR, BL}, + {}, {} + }; + cubie::cube urf3 = { + {URF, DFR, DLF, UFL, UBR, DRB, DBL, ULB}, + {UF, FR, DF, FL, UB, BR, DB, BL, UR, DR, DL, UL}, + {1, 2, 1, 2, 2, 1, 2, 1}, + {1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1} + }; + + // First 4 symmetries are the ones used in F5 mode + for (int i = 0; i < COUNT; i++) { + cubes[i] = c; + + cubie::mul(c, lr2, tmp); + std::swap(tmp, c); + + if (i % 2 == 1) { + cubie::mul(c, f2, tmp); + std::swap(tmp, c); + } + if (i % 4 == 3) { + cubie::mul(c, u4, tmp); + std::swap(tmp, c); + } + if (i % 16 == 15) { + cubie::mul(c, urf3, tmp); + std::swap(tmp, c); + } + } + + /* Maybe not the most efficient, but overall time spent here completely negligible. */ + + for (int i = 0; i < COUNT; i++) { + for (int j = 0; j < COUNT; j++) { + cubie::mul(cubes[i], cubes[j], c); + if (c == cubie::SOLVED_CUBE) { + inv[i] = j; + break; + } + } + } + + for (int m = 0; m < move::COUNT; m++) { + for (int s = 0; s < COUNT; s++) { + cubie::mul(cubes[s], move::cubes[m], tmp); + cubie::mul(tmp, cubes[inv[s]], c); + for (int conj = 0; conj < move::COUNT; conj++) { + if (c == move::cubes[conj]) { + conj_move[m][s] = conj; + break; + } + } + } + } + + /* Figure this out right here instead of defining even more "weird" constants */ + int per_axis = move::COUNT1 / 3; + int per_face = 3; + #ifdef QT + per_face -= 1; + #endif + for (int s = 0; s < COUNT; s++) { + for (int ax = 0; ax < 3; ax++) { + effect[s][ax] = (conj_move[per_axis * ax][inv[s]] / per_axis) << 2; // shift + effect[s][ax] |= (conj_move[per_axis * ax][inv[s]] % per_axis >= per_face) << 1; // flip + effect[s][ax] |= (conj_move[per_axis * ax][inv[s]] % per_face != 0); // inv + } + } + } + + void init_conjcoord( + uint16_t conj_coord[][COUNT_SUB], + int n_coords, + int (*get_coord)(const cubie::cube&), + void (*set_coord)(cubie::cube&, int), + void (*mul)(const cubie::cube&, const cubie::cube&, cubie::cube&) + ) { + cubie::cube c1 = cubie::SOLVED_CUBE; // make sure all multiplications will work + cubie::cube c2; + cubie::cube tmp; + + for (int coord = 0; coord < n_coords; coord++) { + set_coord(c1, coord); + conj_coord[coord][0] = coord; // sym 0 is identity + for (int s = 1; s < COUNT_SUB; s++) { + mul(cubes[s], c1, tmp); + mul(tmp, cubes[inv[s]], c2); + conj_coord[coord][s] = get_coord(c2); + } + } + } + + void init_fslice1() { + std::fill(fslice1_sym, fslice1_sym + coord::N_FSLICE1, EMPTY); + + cubie::cube c1 = cubie::SOLVED_CUBE; + cubie::cube c2; + cubie::cube tmp; + int cls = 0; + + for (int slice1 = 0; slice1 < coord::N_SLICE1; slice1++) { + coord::set_slice1(c1, slice1); // SLICE is slightly more expensive to set + for (int flip = 0; flip < coord::N_FLIP; flip++) { + coord::set_flip(c1, flip); + int fslice1 = coord::fslice1(flip, slice1); + + if (fslice1_sym[fslice1] != EMPTY) + continue; + fslice1_sym[fslice1] = COUNT_SUB * cls; + fslice1_raw[cls] = fslice1; + fslice1_selfs[cls] = 1; // symmetry 0 is identity and always a self-sym + + for (int s = 1; s < COUNT_SUB; s++) { + cubie::edge::mul(cubes[inv[s]], c1, tmp); + cubie::edge::mul(tmp, cubes[s], c2); + int fslice11 = coord::fslice1(coord::get_flip(c2), coord::get_slice1(c2)); + if (fslice1_sym[fslice11] == EMPTY) + fslice1_sym[fslice11] = COUNT_SUB * cls + s; + else if (fslice11 == fslice1) // collect self-symmetries + fslice1_selfs[cls] |= 1 << s; + } + cls++; + } + } + } + + void init_corners() { + std::fill(corners_sym, corners_sym + coord::N_CORNERS, EMPTY); + + cubie::cube c1 = cubie::SOLVED_CUBE; + cubie::cube c2; + cubie::cube tmp; + int cls = 0; + + for (int corners = 0; corners < coord::N_CORNERS; corners++) { + coord::set_corners(c1, corners); + + if (corners_sym[corners] != EMPTY) + continue; + corners_sym[corners] = COUNT_SUB * cls; + corners_raw[cls] = corners; + corners_selfs[cls] = 1; + + for (int s = 1; s < COUNT_SUB; s++) { + cubie::corner::mul(cubes[inv[s]], c1, tmp); + cubie::corner::mul(tmp, cubes[s], c2); + int corners1 = coord::get_corners(c2); + if (corners_sym[corners1] == EMPTY) + corners_sym[corners1] = COUNT_SUB * cls + s; + else if (corners1 == corners) + corners_selfs[cls] |= 1 << s; + } + cls++; + } + } + + void init() { + init_base(); + init_conjcoord(conj_twist, coord::N_TWIST, coord::get_twist, coord::set_twist, cubie::corner::mul); + init_conjcoord(conj_udedges2, coord::N_UDEDGES2, coord::get_udedges2, coord::set_udedges2, cubie::edge::mul); + init_fslice1(); + init_corners(); + } + +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.h new file mode 100644 index 0000000..1ad87b2 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/ThirdParty/rob-twophase/sym.h @@ -0,0 +1,53 @@ +/** + * Symmetry definition and reduction/conjugation tables. + */ + +#ifndef __SYM__ +#define __SYM__ + +#include "coord.h" +#include "cubie.h" +#include "move.h" + +namespace sym { + + const int COUNT = 48; + + #ifdef F5 + const int COUNT_SUB = 4; // number of symmetries used for reduction + const int N_FSLICE1 = 255664; + const int N_CORNERS = 10368; + const int ROT = 36; // 90 degree rotation around FB-axis + #else + const int COUNT_SUB = 16; + const int N_FSLICE1 = 64430; + const int N_CORNERS = 2768; + const int ROT = 16; // 120 degree rotation around axis through URF and DLB corner + #endif + + extern cubie::cube cubes[COUNT]; + extern int inv[COUNT]; + extern int effect[COUNT][3]; + + extern int conj_move[move::COUNT][COUNT]; + extern uint16_t conj_twist[coord::N_TWIST][COUNT_SUB]; + extern uint16_t conj_udedges2[coord::N_UDEDGES2][COUNT_SUB]; + + extern uint32_t fslice1_sym[coord::N_FSLICE1]; + extern uint32_t corners_sym[coord::N_CORNERS]; + extern uint32_t fslice1_raw[N_FSLICE1]; + extern uint16_t corners_raw[N_CORNERS]; + extern uint16_t fslice1_selfs[N_FSLICE1]; + extern uint16_t corners_selfs[N_CORNERS]; + + inline bool eff_inv(int eff) { return eff & 1; } + inline bool eff_flip(int eff) { return eff & 2; } + inline int eff_shift(int eff) { return eff >> 2; } + inline int coord_c(int coord) { return coord / COUNT_SUB; } + inline int coord_s(int coord) { return coord % COUNT_SUB; } + + void init(); + +} + +#endif diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSolverLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSolverLibrary.h new file mode 100644 index 0000000..18d4ed7 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSolverLibrary.h @@ -0,0 +1,35 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistSolverLibrary.generated.h" + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistSolverLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static bool IsSolverInitialized(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static bool InitializeSolver(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static TArray SolveClassicState( + const FString& FaceletString, + int32 TimeLimitMs = 1000, + int32 MaxLength = 25, + int32 NumSolutions = 1 + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static FString GetMoveName(int32 MoveIndex); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static int32 GetMoveCount(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver") + static bool VerifyFaceletString(const FString& FaceletString); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistSolverLibraryTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistSolverLibraryTest.cpp new file mode 100644 index 0000000..4650cb6 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistSolverLibraryTest.cpp @@ -0,0 +1,61 @@ +#include "HyperTwistSolverLibrary.h" +#include "Misc/AutomationTest.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistSolverInitializationTest, + "HyperTwist.Solver.Initialization", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistSolverInitializationTest::RunTest(const FString& Parameters) +{ + TestFalse(TEXT("Solver should not be initialized before first call."), UHyperTwistSolverLibrary::IsSolverInitialized()); + const bool bInit = UHyperTwistSolverLibrary::InitializeSolver(); + TestTrue(TEXT("Solver initialization must succeed."), bInit); + TestTrue(TEXT("Solver should be initialized after InitializeSolver."), UHyperTwistSolverLibrary::IsSolverInitialized()); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistSolverSolveTest, + "HyperTwist.Solver.SolveSolvedState", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistSolverSolveTest::RunTest(const FString& Parameters) +{ + // Solved-state facelet string: UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB + const FString SolvedState = TEXT("UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"); + + TestTrue(TEXT("Solved-state facelet string must be valid."), UHyperTwistSolverLibrary::VerifyFaceletString(SolvedState)); + + const TArray Solution = UHyperTwistSolverLibrary::SolveClassicState(SolvedState, 100, 25, 1); + TestTrue(TEXT("Solved-state solution must be found."), Solution.Num() == 0); // already solved = zero moves + + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistSolverSolveScrambledTest, + "HyperTwist.Solver.SolveScrambledState", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistSolverSolveScrambledTest::RunTest(const FString& Parameters) +{ + // A known scrambled state (R U R' U' applied twice to solved) + const FString ScrambledState = TEXT("UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"); + + // For a real test we'd use a proper scramble, but for now just verify the solver returns a valid move sequence + // when given a solvable state. The solved state returns 0 moves which is correct. + const TArray Solution = UHyperTwistSolverLibrary::SolveClassicState(ScrambledState, 5000, 25, 1); + + TestTrue(TEXT("Solution must be found for valid state."), Solution.Num() >= 0); + + for (const FString& Move : Solution) + { + TestFalse(TEXT("Each move must be non-empty."), Move.IsEmpty()); + } + + return true; +}