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
This commit is contained in:
parent
86a0068117
commit
168c6fdd08
10 changed files with 588 additions and 9 deletions
1
.external/freestyle
Submodule
1
.external/freestyle
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e7a317c944bced4287e1506fb40ac569704cf8bf
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
41
Solvers/brownan/CMakeLists.txt
Normal file
41
Solvers/brownan/CMakeLists.txt
Normal file
|
|
@ -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
|
||||
$<TARGET_FILE_DIR:brownan-solver>
|
||||
COMMENT "Copying solver tables"
|
||||
)
|
||||
31
Solvers/brownan/build.bat
Normal file
31
Solvers/brownan/build.bat
Normal file
|
|
@ -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
|
||||
32
Solvers/cahidenes/cahidenes_solver_wrapper.py
Normal file
32
Solvers/cahidenes/cahidenes_solver_wrapper.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
#include "HyperTwistIPC/HyperTwistIPCProcessManager.h"
|
||||
|
||||
UHyperTwistIPCProcessManager::UHyperTwistIPCProcessManager()
|
||||
{
|
||||
}
|
||||
|
||||
UHyperTwistIPCProcessManager* UHyperTwistIPCProcessManager::CreateIPCManager(UObject* WorldContextObject)
|
||||
{
|
||||
return NewObject<UHyperTwistIPCProcessManager>(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<float>(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<float>(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);
|
||||
}
|
||||
|
|
@ -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<FString>& 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<FString> UHyperTwistSolverOracleLibrary::SolveWithCahidenesOracle(const FString& FaceletString)
|
||||
{
|
||||
TArray<FString> 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<FString> Moves;
|
||||
Output.ParseIntoArrayWS(Moves);
|
||||
return Moves;
|
||||
}
|
||||
|
||||
bool UHyperTwistSolverOracleLibrary::CompareSolvers(const FString& FaceletString)
|
||||
{
|
||||
// rob-twophase result
|
||||
TArray<FString> RobTwophaseMoves = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 25, 1);
|
||||
TArray<FString> RobMoves;
|
||||
RobTwophaseSolution.ParseIntoArrayWS(RobMoves);
|
||||
|
||||
// cahidenes result
|
||||
TArray<FString> CahidenesMoves = SolveWithCahidenesOracle(FaceletString);
|
||||
|
||||
return RobMoves.Num() == CahidenesMoves.Num();
|
||||
}
|
||||
|
|
@ -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<FString> 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<int32, FActiveProcess> ActiveProcesses;
|
||||
int32 NextProcessId = 1;
|
||||
};
|
||||
|
|
@ -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<FString>& Moves);
|
||||
|
||||
/**
|
||||
* Solve a cube using the cahidenes Python solver oracle (IPC).
|
||||
* Returns the solution move sequence.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Solver|Oracle")
|
||||
static TArray<FString> 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();
|
||||
};
|
||||
|
|
@ -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<FString>`
|
||||
- [ ] Apply scramble to visible geometry (animate each move)
|
||||
- [ ] Display scramble notation in HUD
|
||||
### 2D — Scramble Generation ✅ COMPLETE
|
||||
- [x] `GenerateScramble(int32 Length)` → `TArray<FString>` 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<FString>)` 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()`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue