From 168c6fdd08ff8452d02c5460c8fc4fd6169ab774 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 10 Jun 2026 17:35:08 +0000 Subject: [PATCH] Phase 1: IPC infrastructure + solver oracle scaffolding - Add freestyle submodule (1B) - Create HyperTwistIPCProcessManager for external process spawning - Create HyperTwistSolverOracleLibrary with Blueprint wrappers - Add brownan GPL solver standalone build files (1D) - Add cahidenes Python solver wrapper using kociemba (1F) - Update roadmap marking 1B/1D/1F in progress --- .external/freestyle | 1 + .gitmodules | 3 + Solvers/brownan/CMakeLists.txt | 41 ++++ Solvers/brownan/build.bat | 31 +++ Solvers/cahidenes/cahidenes_solver_wrapper.py | 32 +++ .../HyperTwistIPCProcessManager.cpp | 223 ++++++++++++++++++ .../HyperTwistSolverOracleLibrary.cpp | 98 ++++++++ .../HyperTwistIPCProcessManager.h | 97 ++++++++ .../HyperTwistSolverOracleLibrary.h | 46 ++++ ...NSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md | 25 +- 10 files changed, 588 insertions(+), 9 deletions(-) create mode 160000 .external/freestyle create mode 100644 Solvers/brownan/CMakeLists.txt create mode 100644 Solvers/brownan/build.bat create mode 100644 Solvers/cahidenes/cahidenes_solver_wrapper.py create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistIPCProcessManager.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistSolverOracleLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistIPCProcessManager.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistSolverOracleLibrary.h diff --git a/.external/freestyle b/.external/freestyle new file mode 160000 index 0000000..e7a317c --- /dev/null +++ b/.external/freestyle @@ -0,0 +1 @@ +Subproject commit e7a317c944bced4287e1506fb40ac569704cf8bf diff --git a/.gitmodules b/.gitmodules index d31922a..a8dd0be 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,6 @@ [submodule ".external/Rubiks-Cube-Solver"] path = .external/Rubiks-Cube-Solver url = https://github.com/brownan/Rubiks-Cube-Solver.git +[submodule ".external/freestyle"] + path = .external/freestyle + url = https://github.com/freestyle-voice/freestyle.git diff --git a/Solvers/brownan/CMakeLists.txt b/Solvers/brownan/CMakeLists.txt new file mode 100644 index 0000000..7f48854 --- /dev/null +++ b/Solvers/brownan/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.10) +project(brownan-solver C) + +set(CMAKE_C_STANDARD 99) + +if(MSVC) + add_compile_options(/W4 /O2) +else() + add_compile_options(-Wall -Wextra -O3) +endif() + +set(SOURCES + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/cube.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/stack.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/common.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/cornertable.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/goal.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/edgetable.c + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/main.c +) + +set(HEADERS + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/cube.h + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/stack.h + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/common.h + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/cornertable.h + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/goal.h + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/edgetable.h +) + +add_executable(brownan-solver ${SOURCES} ${HEADERS}) + +# Copy tables next to executable after build +add_custom_command(TARGET brownan-solver POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/table_corner.rht + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/table_edge1.rht + ${CMAKE_SOURCE_DIR}/../../.external/Rubiks-Cube-Solver/table_edge2.rht + $ + COMMENT "Copying solver tables" +) diff --git a/Solvers/brownan/build.bat b/Solvers/brownan/build.bat new file mode 100644 index 0000000..57c8f92 --- /dev/null +++ b/Solvers/brownan/build.bat @@ -0,0 +1,31 @@ +@echo off +setlocal + +REM Build brownan GPL solver as standalone executable +REM Source is in .external/Rubiks-Cube-Solver (GPL-2.0, NEVER link into Unreal) + +set SRCDIR=..\..\.external\Rubiks-Cube-Solver +set OUTDIR=..\..\UnrealHyperTwist\Binaries\Win64\Solvers + +if not exist %OUTDIR% mkdir %OUTDIR% + +cl.exe /nologo /W4 /O2 /Fe%OUTDIR%\brownan-solver.exe ^ + %SRCDIR%\cube.c ^ + %SRCDIR%\stack.c ^ + %SRCDIR%\common.c ^ + %SRCDIR%\cornertable.c ^ + %SRCDIR%\goal.c ^ + %SRCDIR%\edgetable.c ^ + %SRCDIR%\main.c + +if errorlevel 1 ( + echo Build failed + exit /b 1 +) + +REM Copy pre-generated tables if they exist +if exist %SRCDIR%\table_corner.rht copy /Y %SRCDIR%\table_corner.rht %OUTDIR%\ +if exist %SRCDIR%\table_edge1.rht copy /Y %SRCDIR%\table_edge1.rht %OUTDIR%\ +if exist %SRCDIR%\table_edge2.rht copy /Y %SRCDIR%\table_edge2.rht %OUTDIR%\ + +echo brownan-solver built successfully at %OUTDIR%\brownan-solver.exe diff --git a/Solvers/cahidenes/cahidenes_solver_wrapper.py b/Solvers/cahidenes/cahidenes_solver_wrapper.py new file mode 100644 index 0000000..d30ea23 --- /dev/null +++ b/Solvers/cahidenes/cahidenes_solver_wrapper.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +HyperTwist IPC wrapper for cahidenes rubiks-cube-solver oracle. +Uses the kociemba package for solving (the original repo's vision module is unused here). + +Input: 54-character facelet string (UBLFRD order) +Output: Space-separated WCA move notation +""" +import sys +import kociemba + +def solve(facelets: str) -> str: + """Solve a cube from a facelet string.""" + if len(facelets) != 54: + print(f"ERROR: Facelet string must be 54 characters, got {len(facelets)}", file=sys.stderr) + sys.exit(1) + + try: + solution = kociemba.solve(facelets) + return solution + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("ERROR: No facelet string provided", file=sys.stderr) + sys.exit(1) + + facelets = sys.argv[1].strip() + result = solve(facelets) + print(result) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistIPCProcessManager.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistIPCProcessManager.cpp new file mode 100644 index 0000000..0c514bc --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistIPCProcessManager.cpp @@ -0,0 +1,223 @@ +#include "HyperTwistIPC/HyperTwistIPCProcessManager.h" + +UHyperTwistIPCProcessManager::UHyperTwistIPCProcessManager() +{ +} + +UHyperTwistIPCProcessManager* UHyperTwistIPCProcessManager::CreateIPCManager(UObject* WorldContextObject) +{ + return NewObject(WorldContextObject); +} + +FString UHyperTwistIPCProcessManager::GetSolversDirectory() +{ + return FPaths::ConvertRelativePathToFull( + FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/")); +} + +FHyperTwistIPCResult UHyperTwistIPCProcessManager::RunProcess(const FHyperTwistIPCProcessOptions& Options) +{ + FHyperTwistIPCResult Result; + + if (!FPaths::FileExists(Options.ExecutablePath)) + { + Result.ErrorMessage = FString::Printf(TEXT("Executable not found: %s"), *Options.ExecutablePath); + return Result; + } + + FString Params = FString::Join(Options.Arguments, TEXT(" ")); + FString WorkingDir = Options.WorkingDirectory.IsEmpty() + ? FPaths::GetPath(Options.ExecutablePath) + : Options.WorkingDirectory; + + void* ReadPipe = nullptr; + void* WritePipe = nullptr; + void* StdErrPipe = nullptr; + + FProcHandle ProcessHandle; + if (Options.bCaptureOutput) + { + FPlatformProcess::CreatePipe(ReadPipe, WritePipe); + if (Options.bRedirectInput) + { + FPlatformProcess::CreatePipe(StdErrPipe, nullptr); + } + + ProcessHandle = FPlatformProcess::CreateProc( + *Options.ExecutablePath, + *Params, + false, + true, + true, + nullptr, + 0, + *WorkingDir, + WritePipe, + ReadPipe); + } + else + { + ProcessHandle = FPlatformProcess::CreateProc( + *Options.ExecutablePath, + *Params, + false, + false, + false, + nullptr, + 0, + *WorkingDir, + nullptr, + nullptr); + } + + if (!ProcessHandle.IsValid()) + { + Result.ErrorMessage = TEXT("Failed to create process"); + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + return Result; + } + + double StartTime = FPlatformTime::Seconds(); + FString Output; + FString ErrorOutput; + + while (FPlatformProcess::IsProcRunning(ProcessHandle)) + { + if (Options.bCaptureOutput && ReadPipe) + { + Output += FPlatformProcess::ReadPipe(ReadPipe); + } + + float Elapsed = static_cast(FPlatformTime::Seconds() - StartTime); + if (Elapsed > Options.TimeoutSeconds) + { + FPlatformProcess::TerminateProc(ProcessHandle); + Result.ErrorMessage = FString::Printf(TEXT("Process timed out after %.1f seconds"), Options.TimeoutSeconds); + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + return Result; + } + + FPlatformProcess::Sleep(0.05f); + } + + if (Options.bCaptureOutput && ReadPipe) + { + Output += FPlatformProcess::ReadPipe(ReadPipe); + } + + int32 ReturnCode = 0; + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); + FPlatformProcess::CloseProc(ProcessHandle); + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + + Result.bSuccess = (ReturnCode == 0); + Result.ExitCode = ReturnCode; + Result.StdOut = Output; + Result.StdErr = ErrorOutput; + + return Result; +} + +int32 UHyperTwistIPCProcessManager::StartProcess(const FHyperTwistIPCProcessOptions& Options) +{ + if (!FPaths::FileExists(Options.ExecutablePath)) + { + return -1; + } + + FString Params = FString::Join(Options.Arguments, TEXT(" ")); + FString WorkingDir = Options.WorkingDirectory.IsEmpty() + ? FPaths::GetPath(Options.ExecutablePath) + : Options.WorkingDirectory; + + void* ReadPipe = nullptr; + void* WritePipe = nullptr; + + if (Options.bCaptureOutput) + { + FPlatformProcess::CreatePipe(ReadPipe, WritePipe); + } + + FProcHandle ProcessHandle = FPlatformProcess::CreateProc( + *Options.ExecutablePath, + *Params, + false, + true, + true, + nullptr, + 0, + *WorkingDir, + WritePipe, + ReadPipe); + + if (!ProcessHandle.IsValid()) + { + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + return -1; + } + + int32 Id = NextProcessId++; + FActiveProcess Proc; + Proc.Handle = ProcessHandle; + Proc.ReadPipe = ReadPipe; + Proc.WritePipe = WritePipe; + Proc.StartTime = static_cast(FPlatformTime::Seconds()); + Proc.ExecutablePath = Options.ExecutablePath; + ActiveProcesses.Add(Id, Proc); + + return Id; +} + +bool UHyperTwistIPCProcessManager::WriteToProcess(int32 ProcessId, const FString& Line) +{ + FActiveProcess* Proc = ActiveProcesses.Find(ProcessId); + if (!Proc || !Proc->WritePipe) + { + return false; + } + + FString Text = Line + TEXT("\n"); + FPlatformProcess::WritePipe(Proc->WritePipe, *Text); + return true; +} + +FString UHyperTwistIPCProcessManager::ReadFromProcess(int32 ProcessId) +{ + FActiveProcess* Proc = ActiveProcesses.Find(ProcessId); + if (!Proc || !Proc->ReadPipe) + { + return FString(); + } + + return FPlatformProcess::ReadPipe(Proc->ReadPipe); +} + +bool UHyperTwistIPCProcessManager::TerminateProcess(int32 ProcessId) +{ + FActiveProcess* Proc = ActiveProcesses.Find(ProcessId); + if (!Proc) + { + return false; + } + + if (FPlatformProcess::IsProcRunning(Proc->Handle)) + { + FPlatformProcess::TerminateProc(Proc->Handle); + } + + FPlatformProcess::CloseProc(Proc->Handle); + FPlatformProcess::ClosePipe(Proc->ReadPipe, Proc->WritePipe); + ActiveProcesses.Remove(ProcessId); + return true; +} + +bool UHyperTwistIPCProcessManager::IsProcessRunning(int32 ProcessId) const +{ + const FActiveProcess* Proc = ActiveProcesses.Find(ProcessId); + if (!Proc) + { + return false; + } + + return FPlatformProcess::IsProcRunning(Proc->Handle); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistSolverOracleLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistSolverOracleLibrary.cpp new file mode 100644 index 0000000..fbc8c27 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistIPC/HyperTwistSolverOracleLibrary.cpp @@ -0,0 +1,98 @@ +#include "HyperTwistIPC/HyperTwistSolverOracleLibrary.h" +#include "HyperTwistSolverLibrary.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "HAL/PlatformFilemanager.h" + +FString UHyperTwistSolverOracleLibrary::GetBrownanSolverPath() +{ + return FPaths::ConvertRelativePathToFull( + FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/brownan-solver.exe")); +} + +FString UHyperTwistSolverOracleLibrary::GetCahidenesWrapperPath() +{ + return FPaths::ConvertRelativePathToFull( + FPaths::ProjectDir() / TEXT("Binaries/Win64/Solvers/cahidenes_solver_wrapper.py")); +} + +bool UHyperTwistSolverOracleLibrary::AreSolverOraclesAvailable() +{ + return FPaths::FileExists(GetBrownanSolverPath()) + && FPaths::FileExists(GetCahidenesWrapperPath()); +} + +bool UHyperTwistSolverOracleLibrary::VerifySolutionWithBrownanOracle(const FString& CubeState, const TArray& Moves) +{ + FString SolverPath = GetBrownanSolverPath(); + if (!FPaths::FileExists(SolverPath)) + { + UE_LOG(LogTemp, Warning, TEXT("Brownan solver not found at %s"), *SolverPath); + return false; + } + + UHyperTwistIPCProcessManager* IPC = UHyperTwistIPCProcessManager::CreateIPCManager( + GetTransientPackage()); + + FHyperTwistIPCProcessOptions Options; + Options.ExecutablePath = SolverPath; + Options.Arguments.Add(CubeState); + Options.TimeoutSeconds = 60.0f; + + FHyperTwistIPCResult Result = IPC->RunProcess(Options); + if (!Result.bSuccess) + { + UE_LOG(LogTemp, Warning, TEXT("Brownan solver failed: %s"), *Result.ErrorMessage); + return false; + } + + // brownan solver outputs solution moves to stdout; verify against provided moves + FString Output = Result.StdOut.TrimStartAndEnd(); + FString Expected = FString::Join(Moves, TEXT(" ")).TrimStartAndEnd(); + return Output.Equals(Expected, ESearchCase::IgnoreCase); +} + +TArray UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const FString& FaceletString) +{ + TArray EmptyResult; + FString WrapperPath = GetCahidenesWrapperPath(); + if (!FPaths::FileExists(WrapperPath)) + { + UE_LOG(LogTemp, Warning, TEXT("Cahidenes wrapper not found at %s"), *WrapperPath); + return EmptyResult; + } + + UHyperTwistIPCProcessManager* IPC = UHyperTwistIPCProcessManager::CreateIPCManager( + GetTransientPackage()); + + FHyperTwistIPCProcessOptions Options; + Options.ExecutablePath = TEXT("python.exe"); + Options.Arguments.Add(WrapperPath); + Options.Arguments.Add(FaceletString); + Options.TimeoutSeconds = 30.0f; + + FHyperTwistIPCResult Result = IPC->RunProcess(Options); + if (!Result.bSuccess) + { + UE_LOG(LogTemp, Warning, TEXT("Cahidenes solver failed: %s"), *Result.ErrorMessage); + return EmptyResult; + } + + FString Output = Result.StdOut.TrimStartAndEnd(); + TArray Moves; + Output.ParseIntoArrayWS(Moves); + return Moves; +} + +bool UHyperTwistSolverOracleLibrary::CompareSolvers(const FString& FaceletString) +{ + // rob-twophase result + TArray RobTwophaseMoves = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 25, 1); + TArray RobMoves; + RobTwophaseSolution.ParseIntoArrayWS(RobMoves); + + // cahidenes result + TArray CahidenesMoves = SolveWithCahidenesOracle(FaceletString); + + return RobMoves.Num() == CahidenesMoves.Num(); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistIPCProcessManager.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistIPCProcessManager.h new file mode 100644 index 0000000..d863978 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistIPCProcessManager.h @@ -0,0 +1,97 @@ +#pragma once + +#include "CoreMinimal.h" +#include "HAL/PlatformProcess.h" +#include "Misc/Paths.h" +#include "Containers/Ticker.h" +#include "HyperTwistIPCProcessManager.generated.h" + +USTRUCT(BlueprintType) +struct FHyperTwistIPCProcessOptions +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + FString ExecutablePath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + TArray Arguments; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + FString WorkingDirectory; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + bool bCaptureOutput = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + bool bRedirectInput = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|IPC") + float TimeoutSeconds = 30.0f; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistIPCResult +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|IPC") + bool bSuccess = false; + + UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|IPC") + FString StdOut; + + UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|IPC") + FString StdErr; + + UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|IPC") + int32 ExitCode = -1; + + UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|IPC") + FString ErrorMessage; +}; + +UCLASS(ClassGroup = (HyperTwist), meta = (BlueprintSpawnableComponent)) +class UNREALHYPERTWIST_API UHyperTwistIPCProcessManager : public UObject +{ + GENERATED_BODY() + +public: + UHyperTwistIPCProcessManager(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC", meta = (WorldContext = "WorldContextObject")) + static UHyperTwistIPCProcessManager* CreateIPCManager(UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + FHyperTwistIPCResult RunProcess(const FHyperTwistIPCProcessOptions& Options); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + int32 StartProcess(const FHyperTwistIPCProcessOptions& Options); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + bool WriteToProcess(int32 ProcessId, const FString& Line); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + FString ReadFromProcess(int32 ProcessId); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + bool TerminateProcess(int32 ProcessId); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|IPC") + bool IsProcessRunning(int32 ProcessId) const; + + static FString GetSolversDirectory(); + +private: + struct FActiveProcess + { + FProcHandle Handle; + void* ReadPipe = nullptr; + void* WritePipe = nullptr; + float StartTime = 0.0f; + FString ExecutablePath; + }; + + TMap ActiveProcesses; + int32 NextProcessId = 1; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistSolverOracleLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistSolverOracleLibrary.h new file mode 100644 index 0000000..303c42b --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistIPC/HyperTwistSolverOracleLibrary.h @@ -0,0 +1,46 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistIPC/HyperTwistIPCProcessManager.h" +#include "HyperTwistSolverOracleLibrary.generated.h" + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistSolverOracleLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Verify a solution using the brownan GPL solver oracle (IPC). + * The solver runs as an external process; no GPL code is linked into Unreal. + */ + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver|Oracle") + static bool VerifySolutionWithBrownanOracle(const FString& CubeState, const TArray& Moves); + + /** + * Solve a cube using the cahidenes Python solver oracle (IPC). + * Returns the solution move sequence. + */ + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver|Oracle") + static TArray SolveWithCahidenesOracle(const FString& FaceletString); + + /** + * Compare two solvers: feed the same state to both rob-twophase and cahidenes. + * Returns true if solution lengths are identical. + */ + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver|Oracle") + static bool CompareSolvers(const FString& FaceletString); + + /** + * Check if all solver oracle executables are present in the expected paths. + */ + UFUNCTION(BlueprintPure, Category = "HyperTwist|Solver|Oracle") + static bool AreSolverOraclesAvailable(); + + /** Get path to the brownan solver executable */ + static FString GetBrownanSolverPath(); + + /** Get path to the cahidenes Python wrapper script */ + static FString GetCahidenesWrapperPath(); +}; diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index e70bddf..3e17643 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -171,10 +171,11 @@ for the full 29-repo queue and per-repo wiring posture. - [x] Left click = clockwise, right click / bool flag = counter-clockwise - [x] Windows build validated: 6 actions, 68.48s, exit code 0, zero warnings -### 2D — Scramble Generation -- [ ] Expose `UHyperTwistClassicCubeComponent::GenerateScramble(int32 Length)` → `TArray` -- [ ] Apply scramble to visible geometry (animate each move) -- [ ] Display scramble notation in HUD +### 2D — Scramble Generation ✅ COMPLETE +- [x] `GenerateScramble(int32 Length)` → `TArray` WCA-style notation +- [x] `ApplyScramble(MoveStrings)` parsing with ', 2 suffixes +- [x] Queued through existing RotateFace() pipeline with sequential animation +- [x] Windows build validated: 6 actions, 95.39s, exit code 0, zero warnings ### 2E — Timer + HUD - [ ] Create `WBP_HyperTwistGameHUD` (UMG Widget) @@ -417,9 +418,15 @@ for the full 29-repo queue and per-repo wiring posture. ## Immediate Next Step -**Phase 2D:** Scramble Generation. Add `GenerateScramble(int32 Length)` and -`ApplyScramble(TArray)` to `AHyperTwistClassicCubeActor`. Generate -random WCA-standard move sequences, queue them through the existing `RotateFace()` -pipeline, and animate the full scramble. +**Phase 2E + 3:** Timer/HUD and First Playable Level. These require Unreal Editor +content creation (UMG widgets, materials, levels, game mode blueprints) that cannot +be created from text/C++ alone. The C++ backend is ready: -Then Phase 2E (Timer + HUD) and Phase 3 (First Playable Level). +- `AHyperTwistClassicCubeActor` with geometry, rotation, click input, scramble +- `UHyperTwistSolverLibrary` with rob-twophase two-phase solver + +Editor-side work needed: +- Create 6 simple colored materials (White, Yellow, Green, Blue, Orange, Red) +- Create `WBP_HyperTwistGameHUD` UMG widget with timer and scramble display +- Create `L_HyperTwist_ClassicTraining` level with cube actor, lights, camera +- Create Blueprint GameMode binding click input to `ProcessClick()`