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<FString>
  - 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
This commit is contained in:
axiomlogicnexus 2026-06-10 04:13:37 +00:00
parent c4e4606d14
commit 4359e572c9
17 changed files with 2464 additions and 0 deletions

View file

@ -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 <mutex>
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<FString> UHyperTwistSolverLibrary::SolveClassicState(
const FString& FaceletString,
int32 TimeLimitMs,
int32 MaxLength,
int32 NumSolutions
)
{
TArray<FString> 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<std::vector<int>> 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<int>& 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;
}

View file

@ -0,0 +1,256 @@
#include "coord.h"
#include <algorithm>
#include <bitset>
#include <cstring>
#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<cubie::edge::COUNT>(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);
}
}

View file

@ -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

View file

@ -0,0 +1,138 @@
#include "cubie.h"
#include <algorithm>
#include <random>
#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<int>(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<int>(0, coord::N_TWIST - 1)(gen));
coord::set_flip(c, std::uniform_int_distribution<int>(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);
}
}

View file

@ -0,0 +1,87 @@
/**
* Cubie definitions, cubie-cube representation + methods for manipulating it
*/
#ifndef __CUBIE__
#define __CUBIE__
#include <string>
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

View file

@ -0,0 +1,88 @@
#include "face.h"
#include <string>
#include <unordered_map>
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<int, std::pair<int, int>> corners;
std::unordered_map<int, std::pair<int, int>> 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);
}
}

View file

@ -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 <string>
#include <unordered_map>
#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<char, int> 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

View file

@ -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<int>& mseq, std::vector<int>& 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<int>& mseq) {
std::vector<int> 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<int>& mseq, int cost[]) {
std::vector<int> comp;
compress1(mseq, comp);
int res = 0;
for (int m : comp)
res += cost[m];
return res;
}
int len_ht(const std::vector<int>& 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<int>& 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<int>& 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<int>& 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);
}
}

View file

@ -0,0 +1,75 @@
/**
* All kinds of moveset definitions and setup
*/
#ifndef __MOVE__
#define __MOVE__
#include <cstdint>
#include <string>
#include <vector>
#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<int>& mseq);
/* Compute solution lengths in different metrics */
int len_ht(const std::vector<int>& mseq);
int len_axht(const std::vector<int>& mseq);
int len_qt(const std::vector<int>& mseq);
int len_axqt(const std::vector<int>& mseq);
void init();
}
#endif

View file

@ -0,0 +1,413 @@
#include "prun.h"
#include <bitset>
#include <iostream>
#include <cstring>
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;
}
}

View file

@ -0,0 +1,36 @@
/**
* Pruning table generation and lookup.
*/
#ifndef __PRUN__
#define __PRUN__
#include <cstdint>
#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

View file

@ -0,0 +1,305 @@
#include "solve.h"
#include <algorithm>
#include <cstring>
#include <thread>
#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<int>(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<std::vector<int>>& 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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
}
}

View file

@ -0,0 +1,73 @@
#ifndef __SOLVE__
#define __SOLVE__
#include <condition_variable>
#include <mutex>
#include <queue>
#include <utility>
#include <thread>
# include "move.h"
namespace solve {
using searchres = std::pair<std::vector<int>, 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<searchres, std::vector<searchres>, decltype(&cmp)> sols {cmp}; // already found solutions
std::vector<std::thread> 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<std::vector<int>>& 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

View file

@ -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();
}
}

View file

@ -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

View file

@ -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<FString> 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);
};

View file

@ -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<FString> 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<FString> 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;
}