Add first-party cubing alg Bound 1 surface
This commit is contained in:
parent
d34e923d8a
commit
fe75513126
17 changed files with 2584 additions and 26 deletions
|
|
@ -0,0 +1,180 @@
|
|||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmParser.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h"
|
||||
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistAlgorithmCanonicalizationAutomationTest,
|
||||
"HyperTwist.Algorithm.Bound1.Canonicalization",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistAlgorithmRoundTripAutomationTest,
|
||||
"HyperTwist.Algorithm.Bound1.RoundTrip",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistAlgorithmStructuredJsonAutomationTest,
|
||||
"HyperTwist.Algorithm.Bound1.StructuredJson",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FHyperTwistAlgorithmCanonicalizationAutomationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
struct FCanonicalCase
|
||||
{
|
||||
const TCHAR* Input;
|
||||
const TCHAR* Expected;
|
||||
};
|
||||
|
||||
const TArray<FCanonicalCase> Cases = {
|
||||
{ TEXT("R1"), TEXT("R") },
|
||||
{ TEXT(". . ."), TEXT("...") },
|
||||
{ TEXT("R\r\nU"), TEXT("R\nU") },
|
||||
{ TEXT("R// note\r\nU"), TEXT("R // note\nU") },
|
||||
{ TEXT("R/* block */U"), TEXT("R /* block */ U") },
|
||||
{ TEXT("3Rw"), TEXT("3Rw") },
|
||||
{ TEXT("2-3Uw"), TEXT("2-3Uw") },
|
||||
{ TEXT("[R, U]2"), TEXT("[R, U]2") },
|
||||
{ TEXT("[R: U]'"), TEXT("[R: U]'") },
|
||||
{ TEXT("(R U)'"), TEXT("(R U)'") }
|
||||
};
|
||||
|
||||
for (const FCanonicalCase& Case : Cases)
|
||||
{
|
||||
FString Canonical;
|
||||
FString ErrorMessage;
|
||||
int32 ErrorPosition = -1;
|
||||
const bool bSucceeded = UHyperTwistCoreLibrary::TryCanonicalizeAlgorithmNotation(
|
||||
Case.Input,
|
||||
Canonical,
|
||||
ErrorMessage,
|
||||
ErrorPosition
|
||||
);
|
||||
|
||||
TestTrue(FString::Printf(TEXT("Canonicalization succeeded for '%s'"), Case.Input), bSucceeded);
|
||||
if (!bSucceeded)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("Canonicalization failed for '%s' at %d: %s"),
|
||||
Case.Input,
|
||||
ErrorPosition,
|
||||
*ErrorMessage));
|
||||
continue;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("Canonical output for '%s'"), Case.Input),
|
||||
Canonical,
|
||||
FString(Case.Expected));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FHyperTwistAlgorithmRoundTripAutomationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
const TArray<FString> Cases = {
|
||||
TEXT("R U R' U'"),
|
||||
TEXT("(R U R' U')3"),
|
||||
TEXT("[R, U]2 [R: U]'"),
|
||||
TEXT("...\n// note\n/* block */\n2-3Uw R1"),
|
||||
TEXT("3Rw . [R: U2] (R U)'")
|
||||
};
|
||||
|
||||
for (const FString& Input : Cases)
|
||||
{
|
||||
FString Reserialized;
|
||||
bool bStructurallyEqual = false;
|
||||
const bool bRoundTripSucceeded =
|
||||
UHyperTwistAlgorithmLibrary::ValidateRoundTrip(Input, Reserialized, bStructurallyEqual);
|
||||
|
||||
TestTrue(FString::Printf(TEXT("Round-trip succeeded for '%s'"), *Input), bRoundTripSucceeded);
|
||||
TestTrue(FString::Printf(TEXT("Round-trip structural equality for '%s'"), *Input), bStructurallyEqual);
|
||||
|
||||
if (!bRoundTripSucceeded || !bStructurallyEqual)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistAlgorithmParseResult OriginalParse =
|
||||
UHyperTwistAlgorithmParser::ParseAlgorithm(Input);
|
||||
const FHyperTwistAlgorithmParseResult ReserializedParse =
|
||||
UHyperTwistAlgorithmParser::ParseAlgorithm(Reserialized);
|
||||
TestTrue(TEXT("Original parse succeeded"), OriginalParse.bSuccess);
|
||||
TestTrue(TEXT("Reserialized parse succeeded"), ReserializedParse.bSuccess);
|
||||
if (OriginalParse.bSuccess && ReserializedParse.bSuccess)
|
||||
{
|
||||
const FString OriginalCanonical =
|
||||
UHyperTwistAlgorithmSerializer::SerializeAlgorithm(OriginalParse.Sequence);
|
||||
const FString ReserializedCanonical =
|
||||
UHyperTwistAlgorithmSerializer::SerializeAlgorithm(ReserializedParse.Sequence);
|
||||
TestEqual(TEXT("Canonical form remains stable after round-trip"), ReserializedCanonical, OriginalCanonical);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FHyperTwistAlgorithmStructuredJsonAutomationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
const TArray<FString> Cases = {
|
||||
TEXT("R U R' U'"),
|
||||
TEXT("[R, U]2 [R: U]'"),
|
||||
TEXT("...\n// note\n/* block */\n2-3Uw R1")
|
||||
};
|
||||
|
||||
for (const FString& Input : Cases)
|
||||
{
|
||||
const FHyperTwistAlgorithmParseResult ParseResult =
|
||||
UHyperTwistAlgorithmParser::ParseAlgorithm(Input);
|
||||
TestTrue(FString::Printf(TEXT("Text parse succeeded for '%s'"), *Input), ParseResult.bSuccess);
|
||||
if (!ParseResult.bSuccess)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString JsonText;
|
||||
const bool bSerializeSucceeded =
|
||||
UHyperTwistAlgorithmSerializer::TrySerializeToJson(ParseResult.Sequence, JsonText);
|
||||
TestTrue(TEXT("Structured JSON serialization succeeded"), bSerializeSucceeded);
|
||||
if (!bSerializeSucceeded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestTrue(TEXT("Structured JSON carries owned schema marker"), JsonText.Contains(TEXT("\"schema\":\"ht-alg/v1\"")));
|
||||
|
||||
FHyperTwistAlgorithmSequence JsonSequence;
|
||||
FString JsonError;
|
||||
const bool bParseFromJsonSucceeded =
|
||||
UHyperTwistAlgorithmParser::TryParseFromJson(JsonText, JsonSequence, JsonError);
|
||||
TestTrue(TEXT("Structured JSON parse succeeded"), bParseFromJsonSucceeded);
|
||||
if (!bParseFromJsonSucceeded)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("Structured JSON parse failed: %s"), *JsonError));
|
||||
continue;
|
||||
}
|
||||
|
||||
TestTrue(
|
||||
TEXT("Structured JSON round-trip preserves structure"),
|
||||
UHyperTwistAlgorithmLibrary::AreAlgorithmsStructurallyEqual(ParseResult.Sequence, JsonSequence));
|
||||
|
||||
const FString JsonCanonical = UHyperTwistAlgorithmSerializer::SerializeAlgorithm(JsonSequence);
|
||||
const FString TextCanonical = UHyperTwistAlgorithmSerializer::SerializeAlgorithm(ParseResult.Sequence);
|
||||
TestEqual(TEXT("Structured JSON preserves canonical text"), JsonCanonical, TextCanonical);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
// Model: Claude (Model B, Bound 1 contribution)
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmParser.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h"
|
||||
|
||||
namespace HyperTwistAlgorithmLibraryInternal
|
||||
{
|
||||
const TCHAR* AlgorithmSubsystemProvenance =
|
||||
TEXT("HyperTwist first-party algorithm-language subsystem. ")
|
||||
TEXT("Clean-room implementation informed by cubing/alg.js (GPL-3.0-or-later) ")
|
||||
TEXT("architectural concepts only. No source code inspection. ")
|
||||
TEXT("Implemented 2026-05-15 via Phase 5R-A clean-room workflow.");
|
||||
|
||||
FString NodeTypeToString(EHyperTwistAlgorithmNodeType NodeType)
|
||||
{
|
||||
switch (NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove: return TEXT("BlockMove");
|
||||
case EHyperTwistAlgorithmNodeType::Group: return TEXT("Group");
|
||||
case EHyperTwistAlgorithmNodeType::Commutator: return TEXT("Commutator");
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate: return TEXT("Conjugate");
|
||||
case EHyperTwistAlgorithmNodeType::Pause: return TEXT("Pause");
|
||||
case EHyperTwistAlgorithmNodeType::Newline: return TEXT("Newline");
|
||||
case EHyperTwistAlgorithmNodeType::Comment: return TEXT("Comment");
|
||||
case EHyperTwistAlgorithmNodeType::Sequence: return TEXT("Sequence");
|
||||
default: return TEXT("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
FString MoveTypeToString(EHyperTwistAlgorithmMoveType MoveType)
|
||||
{
|
||||
switch (MoveType)
|
||||
{
|
||||
case EHyperTwistAlgorithmMoveType::Plain: return TEXT("Plain");
|
||||
case EHyperTwistAlgorithmMoveType::InnerSlice: return TEXT("InnerSlice");
|
||||
case EHyperTwistAlgorithmMoveType::RangedSlice: return TEXT("RangedSlice");
|
||||
case EHyperTwistAlgorithmMoveType::WideMove: return TEXT("WideMove");
|
||||
default: return TEXT("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
FString CommentTypeToString(const EHyperTwistAlgorithmCommentType CommentType)
|
||||
{
|
||||
switch (CommentType)
|
||||
{
|
||||
case EHyperTwistAlgorithmCommentType::Line: return TEXT("Line");
|
||||
case EHyperTwistAlgorithmCommentType::Block: return TEXT("Block");
|
||||
default: return TEXT("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void AppendNodeDebugRow(const FHyperTwistAlgorithmNode& Node, int32 Index, FString& OutTsv)
|
||||
{
|
||||
OutTsv.Append(FString::FromInt(Index));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(NodeTypeToString(Node.NodeType));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
|
||||
switch (Node.NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
OutTsv.Append(Node.BlockMove.Family);
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::FromInt(Node.BlockMove.Amount));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(MoveTypeToString(Node.BlockMove.GetCanonicalMoveType()));
|
||||
OutTsv.Append(Node.BlockMove.UsesWideSuffix() ? TEXT("+wide") : TEXT(""));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::FromInt(Node.BlockMove.GetCanonicalInnerLayer()));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::FromInt(Node.BlockMove.GetCanonicalOuterLayer()));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
OutTsv.Append(TEXT("(...)"));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::FromInt(Node.Group.Amount));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::FromInt(Node.Group.Inner.Num()));
|
||||
OutTsv.Append(TEXT(" nodes"));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
OutTsv.Append(TEXT("[A, B]"));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::Printf(
|
||||
TEXT("count=%d\t%d nodes in A\t%d nodes in B"),
|
||||
Node.Commutator.Amount,
|
||||
Node.Commutator.A.Num(),
|
||||
Node.Commutator.B.Num()));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
OutTsv.Append(TEXT("[A: B]"));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(FString::Printf(
|
||||
TEXT("count=%d\t%d nodes in A\t%d nodes in B"),
|
||||
Node.Conjugate.Amount,
|
||||
Node.Conjugate.A.Num(),
|
||||
Node.Conjugate.B.Num()));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
OutTsv.Append(CommentTypeToString(Node.Comment.CommentType));
|
||||
OutTsv.Append(TEXT("\t"));
|
||||
OutTsv.Append(Node.Comment.CommentText.Replace(TEXT("\t"), TEXT(" ")));
|
||||
break;
|
||||
|
||||
default:
|
||||
OutTsv.Append(TEXT("-"));
|
||||
break;
|
||||
}
|
||||
|
||||
OutTsv.Append(TEXT("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistAlgorithmLibrary::TryCanonicalizeAlgorithmText(
|
||||
const FString& AlgorithmText,
|
||||
FString& OutCanonicalText,
|
||||
FString& OutErrorMessage,
|
||||
int32& OutErrorPosition
|
||||
)
|
||||
{
|
||||
OutCanonicalText.Empty();
|
||||
OutErrorMessage.Empty();
|
||||
OutErrorPosition = -1;
|
||||
|
||||
const FHyperTwistAlgorithmParseResult ParseResult =
|
||||
UHyperTwistAlgorithmParser::ParseAlgorithm(AlgorithmText);
|
||||
if (!ParseResult.bSuccess)
|
||||
{
|
||||
OutErrorMessage = ParseResult.ErrorMessage;
|
||||
OutErrorPosition = ParseResult.ErrorPosition;
|
||||
return false;
|
||||
}
|
||||
|
||||
OutCanonicalText = UHyperTwistAlgorithmSerializer::SerializeAlgorithm(ParseResult.Sequence);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UHyperTwistAlgorithmLibrary::ValidateRoundTrip(
|
||||
const FString& AlgorithmText,
|
||||
FString& OutReserializedText,
|
||||
bool& bOutStructurallyEqual
|
||||
)
|
||||
{
|
||||
// Parse original
|
||||
FHyperTwistAlgorithmParseResult Result1 = UHyperTwistAlgorithmParser::ParseAlgorithm(AlgorithmText);
|
||||
if (!Result1.bSuccess)
|
||||
{
|
||||
bOutStructurallyEqual = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serialize
|
||||
OutReserializedText = UHyperTwistAlgorithmSerializer::SerializeAlgorithm(Result1.Sequence);
|
||||
|
||||
// Parse reserialized
|
||||
FHyperTwistAlgorithmParseResult Result2 = UHyperTwistAlgorithmParser::ParseAlgorithm(OutReserializedText);
|
||||
if (!Result2.bSuccess)
|
||||
{
|
||||
bOutStructurallyEqual = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare ASTs
|
||||
bOutStructurallyEqual = AreAlgorithmsStructurallyEqual(Result1.Sequence, Result2.Sequence);
|
||||
return true;
|
||||
}
|
||||
|
||||
FString UHyperTwistAlgorithmLibrary::BuildAlgorithmDebugTsv(const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmLibraryInternal;
|
||||
|
||||
FString Output = TEXT("index\tnode_type\tpayload\tamount\tdetails\tlayer_1\tlayer_2\n");
|
||||
|
||||
for (int32 Index = 0; Index < Sequence.Nodes.Num(); ++Index)
|
||||
{
|
||||
AppendNodeDebugRow(Sequence.Nodes[Index], Index, Output);
|
||||
}
|
||||
|
||||
return Output;
|
||||
}
|
||||
|
||||
bool UHyperTwistAlgorithmLibrary::AreAlgorithmsStructurallyEqual(
|
||||
const FHyperTwistAlgorithmSequence& A,
|
||||
const FHyperTwistAlgorithmSequence& B
|
||||
)
|
||||
{
|
||||
return A == B; // Uses the operator== defined in FHyperTwistAlgorithmSequence
|
||||
}
|
||||
|
||||
FString UHyperTwistAlgorithmLibrary::GetAlgorithmSubsystemProvenance()
|
||||
{
|
||||
return HyperTwistAlgorithmLibraryInternal::AlgorithmSubsystemProvenance;
|
||||
}
|
||||
|
|
@ -0,0 +1,835 @@
|
|||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane)
|
||||
// No source code inspection. Clean-room workflow only.
|
||||
// Date: 2026-05-15
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmParser.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
|
||||
namespace HyperTwistAlgorithmParserInternal
|
||||
{
|
||||
const TCHAR* StructuredJsonSchema = TEXT("ht-alg/v1");
|
||||
|
||||
bool IsPrimeChar(const TCHAR Character)
|
||||
{
|
||||
return Character == TEXT('\'')
|
||||
|| Character == TEXT('`')
|
||||
|| Character == 0x2019;
|
||||
}
|
||||
|
||||
bool IsHorizontalWhitespace(const TCHAR Character)
|
||||
{
|
||||
return Character == TEXT(' ')
|
||||
|| Character == TEXT('\t')
|
||||
|| Character == TEXT('\v')
|
||||
|| Character == TEXT('\f');
|
||||
}
|
||||
|
||||
bool IsIdentifierStart(const TCHAR Character)
|
||||
{
|
||||
return FChar::IsAlpha(Character) || Character == TEXT('_');
|
||||
}
|
||||
|
||||
bool IsIdentifierContinuation(const TCHAR Character)
|
||||
{
|
||||
return FChar::IsAlpha(Character) || Character == TEXT('_');
|
||||
}
|
||||
|
||||
struct FParserState
|
||||
{
|
||||
explicit FParserState(const FString& InText)
|
||||
: Text(InText)
|
||||
{
|
||||
}
|
||||
|
||||
const FString& Text;
|
||||
int32 Position = 0;
|
||||
FString ErrorMessage;
|
||||
|
||||
bool IsAtEnd() const
|
||||
{
|
||||
return Position >= Text.Len();
|
||||
}
|
||||
|
||||
TCHAR CurrentChar() const
|
||||
{
|
||||
return Position < Text.Len() ? Text[Position] : TEXT('\0');
|
||||
}
|
||||
|
||||
TCHAR PeekChar(const int32 Offset = 1) const
|
||||
{
|
||||
const int32 Index = Position + Offset;
|
||||
return Index < Text.Len() ? Text[Index] : TEXT('\0');
|
||||
}
|
||||
|
||||
bool IsNewlineStart() const
|
||||
{
|
||||
return CurrentChar() == TEXT('\n') || CurrentChar() == TEXT('\r');
|
||||
}
|
||||
|
||||
void Advance(const int32 Count = 1)
|
||||
{
|
||||
Position = FMath::Min(Position + Count, Text.Len());
|
||||
}
|
||||
|
||||
void SkipHorizontalWhitespace()
|
||||
{
|
||||
while (!IsAtEnd() && IsHorizontalWhitespace(CurrentChar()))
|
||||
{
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
|
||||
void ConsumeNewline()
|
||||
{
|
||||
if (CurrentChar() == TEXT('\r') && PeekChar() == TEXT('\n'))
|
||||
{
|
||||
Advance(2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsNewlineStart())
|
||||
{
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
|
||||
bool Consume(const TCHAR Expected)
|
||||
{
|
||||
if (CurrentChar() != Expected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetError(const FString& InErrorMessage)
|
||||
{
|
||||
if (ErrorMessage.IsEmpty())
|
||||
{
|
||||
ErrorMessage = InErrorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryReadUnsignedInteger(int32& OutValue)
|
||||
{
|
||||
OutValue = 0;
|
||||
bool bReadDigit = false;
|
||||
|
||||
while (!IsAtEnd() && FChar::IsDigit(CurrentChar()))
|
||||
{
|
||||
bReadDigit = true;
|
||||
OutValue = (OutValue * 10) + (CurrentChar() - TEXT('0'));
|
||||
Advance();
|
||||
}
|
||||
|
||||
return bReadDigit;
|
||||
}
|
||||
};
|
||||
|
||||
int32 ReadSignedAmountSuffix(FParserState& State)
|
||||
{
|
||||
int32 Value = 0;
|
||||
const bool bHasDigits = State.TryReadUnsignedInteger(Value);
|
||||
const bool bNegative = IsPrimeChar(State.CurrentChar());
|
||||
if (bNegative)
|
||||
{
|
||||
State.Advance();
|
||||
}
|
||||
|
||||
if (!bHasDigits && !bNegative)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!bHasDigits)
|
||||
{
|
||||
Value = 1;
|
||||
}
|
||||
|
||||
return bNegative ? -Value : Value;
|
||||
}
|
||||
|
||||
FHyperTwistAlgorithmNode MakeUnitNode(const EHyperTwistAlgorithmNodeType NodeType)
|
||||
{
|
||||
FHyperTwistAlgorithmNode Node;
|
||||
Node.NodeType = NodeType;
|
||||
return Node;
|
||||
}
|
||||
|
||||
bool TryReadComment(
|
||||
FParserState& State,
|
||||
FHyperTwistAlgorithmNode& OutNode)
|
||||
{
|
||||
if (State.CurrentChar() != TEXT('/')
|
||||
|| (State.PeekChar() != TEXT('/') && State.PeekChar() != TEXT('*')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Comment);
|
||||
|
||||
if (State.PeekChar() == TEXT('/'))
|
||||
{
|
||||
State.Advance(2);
|
||||
FString CommentText;
|
||||
while (!State.IsAtEnd() && !State.IsNewlineStart())
|
||||
{
|
||||
CommentText.AppendChar(State.CurrentChar());
|
||||
State.Advance();
|
||||
}
|
||||
|
||||
OutNode.Comment.CommentType = EHyperTwistAlgorithmCommentType::Line;
|
||||
OutNode.Comment.CommentText = CommentText.TrimStartAndEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
State.Advance(2);
|
||||
FString CommentText;
|
||||
while (!State.IsAtEnd())
|
||||
{
|
||||
if (State.CurrentChar() == TEXT('*') && State.PeekChar() == TEXT('/'))
|
||||
{
|
||||
State.Advance(2);
|
||||
OutNode.Comment.CommentType = EHyperTwistAlgorithmCommentType::Block;
|
||||
OutNode.Comment.CommentText = CommentText.TrimStartAndEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
CommentText.AppendChar(State.CurrentChar());
|
||||
State.Advance();
|
||||
}
|
||||
|
||||
State.SetError(TEXT("Unterminated block comment"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseNodeSequence(
|
||||
FParserState& State,
|
||||
TArray<FHyperTwistAlgorithmNode>& OutNodes,
|
||||
const TArray<TCHAR>& Terminators);
|
||||
|
||||
bool TryParseBlockMove(
|
||||
FParserState& State,
|
||||
FHyperTwistAlgorithmNode& OutNode)
|
||||
{
|
||||
const int32 StartPosition = State.Position;
|
||||
int32 PrefixStart = 0;
|
||||
int32 PrefixEnd = 0;
|
||||
const bool bHasPrefix = State.TryReadUnsignedInteger(PrefixStart);
|
||||
bool bHasRange = false;
|
||||
|
||||
if (bHasPrefix && State.CurrentChar() == TEXT('-'))
|
||||
{
|
||||
State.Advance();
|
||||
if (!State.TryReadUnsignedInteger(PrefixEnd))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected range end after '-' at position %d"),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
bHasRange = true;
|
||||
}
|
||||
|
||||
if (!IsIdentifierStart(State.CurrentChar()))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected move family at position %d"),
|
||||
StartPosition));
|
||||
return false;
|
||||
}
|
||||
|
||||
FString RawFamily;
|
||||
while (!State.IsAtEnd() && IsIdentifierContinuation(State.CurrentChar()))
|
||||
{
|
||||
RawFamily.AppendChar(State.CurrentChar());
|
||||
State.Advance();
|
||||
}
|
||||
|
||||
bool bIsWide = false;
|
||||
if (RawFamily.Len() > 1 && RawFamily.EndsWith(TEXT("w")))
|
||||
{
|
||||
bIsWide = true;
|
||||
RawFamily = RawFamily.LeftChop(1);
|
||||
}
|
||||
|
||||
const int32 Amount = ReadSignedAmountSuffix(State);
|
||||
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::BlockMove);
|
||||
OutNode.BlockMove.Family = RawFamily;
|
||||
OutNode.BlockMove.Amount = Amount;
|
||||
OutNode.BlockMove.bIsWide = bIsWide;
|
||||
OutNode.BlockMove.InnerLayer = 0;
|
||||
OutNode.BlockMove.OuterLayer = 0;
|
||||
|
||||
if (bHasRange)
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::RangedSlice;
|
||||
OutNode.BlockMove.InnerLayer = PrefixStart;
|
||||
OutNode.BlockMove.OuterLayer = PrefixEnd;
|
||||
}
|
||||
else if (bHasPrefix)
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::InnerSlice;
|
||||
OutNode.BlockMove.InnerLayer = PrefixStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::Plain;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseGroup(
|
||||
FParserState& State,
|
||||
FHyperTwistAlgorithmNode& OutNode)
|
||||
{
|
||||
const int32 StartPosition = State.Position;
|
||||
if (!State.Consume(TEXT('(')))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected '(' at position %d"),
|
||||
StartPosition));
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmNode> InnerNodes;
|
||||
if (!TryParseNodeSequence(State, InnerNodes, { TEXT(')') }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!State.Consume(TEXT(')')))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected ')' to close group at position %d"),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Group);
|
||||
OutNode.Group.Inner = MoveTemp(InnerNodes);
|
||||
OutNode.Group.Amount = ReadSignedAmountSuffix(State);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseBracketContainer(
|
||||
FParserState& State,
|
||||
FHyperTwistAlgorithmNode& OutNode)
|
||||
{
|
||||
const int32 StartPosition = State.Position;
|
||||
if (!State.Consume(TEXT('[')))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected '[' at position %d"),
|
||||
StartPosition));
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmNode> LeftNodes;
|
||||
if (!TryParseNodeSequence(State, LeftNodes, { TEXT(','), TEXT(':') }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bIsConjugate = State.Consume(TEXT(':'));
|
||||
if (!bIsConjugate && !State.Consume(TEXT(',')))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected ',' or ':' inside bracket container at position %d"),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmNode> RightNodes;
|
||||
if (!TryParseNodeSequence(State, RightNodes, { TEXT(']') }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!State.Consume(TEXT(']')))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Expected ']' to close bracket container at position %d"),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bIsConjugate)
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Conjugate);
|
||||
OutNode.Conjugate.A = MoveTemp(LeftNodes);
|
||||
OutNode.Conjugate.B = MoveTemp(RightNodes);
|
||||
OutNode.Conjugate.Amount = ReadSignedAmountSuffix(State);
|
||||
return true;
|
||||
}
|
||||
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Commutator);
|
||||
OutNode.Commutator.A = MoveTemp(LeftNodes);
|
||||
OutNode.Commutator.B = MoveTemp(RightNodes);
|
||||
OutNode.Commutator.Amount = ReadSignedAmountSuffix(State);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseNodeSequence(
|
||||
FParserState& State,
|
||||
TArray<FHyperTwistAlgorithmNode>& OutNodes,
|
||||
const TArray<TCHAR>& Terminators)
|
||||
{
|
||||
while (!State.IsAtEnd())
|
||||
{
|
||||
State.SkipHorizontalWhitespace();
|
||||
if (State.IsAtEnd())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (Terminators.Contains(State.CurrentChar()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (State.IsNewlineStart())
|
||||
{
|
||||
OutNodes.Add(MakeUnitNode(EHyperTwistAlgorithmNodeType::Newline));
|
||||
State.ConsumeNewline();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT(')') || State.CurrentChar() == TEXT(']'))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Unexpected closing delimiter '%c' at position %d"),
|
||||
State.CurrentChar(),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT(',') || State.CurrentChar() == TEXT(':'))
|
||||
{
|
||||
State.SetError(FString::Printf(
|
||||
TEXT("Unexpected separator '%c' at position %d"),
|
||||
State.CurrentChar(),
|
||||
State.Position));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT('.') )
|
||||
{
|
||||
OutNodes.Add(MakeUnitNode(EHyperTwistAlgorithmNodeType::Pause));
|
||||
State.Advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT('/') && (State.PeekChar() == TEXT('/') || State.PeekChar() == TEXT('*')))
|
||||
{
|
||||
FHyperTwistAlgorithmNode CommentNode;
|
||||
if (!TryReadComment(State, CommentNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNodes.Add(CommentNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT('('))
|
||||
{
|
||||
FHyperTwistAlgorithmNode GroupNode;
|
||||
if (!TryParseGroup(State, GroupNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNodes.Add(GroupNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (State.CurrentChar() == TEXT('['))
|
||||
{
|
||||
FHyperTwistAlgorithmNode ContainerNode;
|
||||
if (!TryParseBracketContainer(State, ContainerNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNodes.Add(ContainerNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistAlgorithmNode MoveNode;
|
||||
if (!TryParseBlockMove(State, MoveNode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNodes.Add(MoveNode);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryReadJsonObject(
|
||||
const TSharedPtr<FJsonValue>& Value,
|
||||
const FString& Context,
|
||||
TSharedPtr<FJsonObject>& OutObject,
|
||||
FString& OutError)
|
||||
{
|
||||
if (!Value.IsValid())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("%s is missing"), *Context);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Value->Type != EJson::Object)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("%s must be a JSON object"), *Context);
|
||||
return false;
|
||||
}
|
||||
|
||||
OutObject = Value->AsObject();
|
||||
if (!OutObject.IsValid())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("%s could not be read as an object"), *Context);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryGetRequiredStringField(
|
||||
const TSharedPtr<FJsonObject>& Object,
|
||||
const TCHAR* FieldName,
|
||||
FString& OutValue,
|
||||
FString& OutError)
|
||||
{
|
||||
if (!Object.IsValid() || !Object->TryGetStringField(FieldName, OutValue))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Missing string field '%s'"), FieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryGetRequiredIntegerField(
|
||||
const TSharedPtr<FJsonObject>& Object,
|
||||
const TCHAR* FieldName,
|
||||
int32& OutValue,
|
||||
FString& OutError)
|
||||
{
|
||||
double NumericValue = 0.0;
|
||||
if (!Object.IsValid() || !Object->TryGetNumberField(FieldName, NumericValue))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Missing numeric field '%s'"), FieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
const double Rounded = FMath::RoundToDouble(NumericValue);
|
||||
if (!FMath::IsNearlyEqual(NumericValue, Rounded))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Field '%s' must be an integer"), FieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
OutValue = static_cast<int32>(Rounded);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryGetRequiredNodeArrayField(
|
||||
const TSharedPtr<FJsonObject>& Object,
|
||||
const TCHAR* FieldName,
|
||||
const TArray<TSharedPtr<FJsonValue>>*& OutArray,
|
||||
FString& OutError)
|
||||
{
|
||||
if (!Object.IsValid() || !Object->TryGetArrayField(FieldName, OutArray) || OutArray == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Missing array field '%s'"), FieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseJsonNodeArray(
|
||||
const TArray<TSharedPtr<FJsonValue>>& NodeValues,
|
||||
TArray<FHyperTwistAlgorithmNode>& OutNodes,
|
||||
FString& OutError);
|
||||
|
||||
bool TryParseJsonNodeObject(
|
||||
const TSharedPtr<FJsonObject>& NodeObject,
|
||||
FHyperTwistAlgorithmNode& OutNode,
|
||||
FString& OutError)
|
||||
{
|
||||
FString NodeKind;
|
||||
if (!TryGetRequiredStringField(NodeObject, TEXT("kind"), NodeKind, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("move"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::BlockMove);
|
||||
if (!TryGetRequiredStringField(NodeObject, TEXT("family"), OutNode.BlockMove.Family, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!TryGetRequiredIntegerField(NodeObject, TEXT("amount"), OutNode.BlockMove.Amount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FString LayerMode;
|
||||
if (!TryGetRequiredStringField(NodeObject, TEXT("layer_mode"), LayerMode, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNode.BlockMove.bIsWide = false;
|
||||
NodeObject->TryGetBoolField(TEXT("wide"), OutNode.BlockMove.bIsWide);
|
||||
OutNode.BlockMove.InnerLayer = 0;
|
||||
OutNode.BlockMove.OuterLayer = 0;
|
||||
|
||||
if (LayerMode == TEXT("plain"))
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::Plain;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LayerMode == TEXT("index"))
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::InnerSlice;
|
||||
return TryGetRequiredIntegerField(
|
||||
NodeObject,
|
||||
TEXT("layer_index"),
|
||||
OutNode.BlockMove.InnerLayer,
|
||||
OutError);
|
||||
}
|
||||
|
||||
if (LayerMode == TEXT("range"))
|
||||
{
|
||||
OutNode.BlockMove.MoveType = EHyperTwistAlgorithmMoveType::RangedSlice;
|
||||
return TryGetRequiredIntegerField(
|
||||
NodeObject,
|
||||
TEXT("layer_index"),
|
||||
OutNode.BlockMove.InnerLayer,
|
||||
OutError)
|
||||
&& TryGetRequiredIntegerField(
|
||||
NodeObject,
|
||||
TEXT("layer_end"),
|
||||
OutNode.BlockMove.OuterLayer,
|
||||
OutError);
|
||||
}
|
||||
|
||||
OutError = FString::Printf(TEXT("Unsupported move layer_mode '%s'"), *LayerMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("group"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Group);
|
||||
if (!TryGetRequiredIntegerField(NodeObject, TEXT("amount"), OutNode.Group.Amount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* InnerValues = nullptr;
|
||||
if (!TryGetRequiredNodeArrayField(NodeObject, TEXT("items"), InnerValues, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseJsonNodeArray(*InnerValues, OutNode.Group.Inner, OutError);
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("commutator"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Commutator);
|
||||
if (!TryGetRequiredIntegerField(NodeObject, TEXT("amount"), OutNode.Commutator.Amount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* LeftValues = nullptr;
|
||||
const TArray<TSharedPtr<FJsonValue>>* RightValues = nullptr;
|
||||
if (!TryGetRequiredNodeArrayField(NodeObject, TEXT("left"), LeftValues, OutError)
|
||||
|| !TryGetRequiredNodeArrayField(NodeObject, TEXT("right"), RightValues, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseJsonNodeArray(*LeftValues, OutNode.Commutator.A, OutError)
|
||||
&& TryParseJsonNodeArray(*RightValues, OutNode.Commutator.B, OutError);
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("conjugate"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Conjugate);
|
||||
if (!TryGetRequiredIntegerField(NodeObject, TEXT("amount"), OutNode.Conjugate.Amount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* LeftValues = nullptr;
|
||||
const TArray<TSharedPtr<FJsonValue>>* RightValues = nullptr;
|
||||
if (!TryGetRequiredNodeArrayField(NodeObject, TEXT("left"), LeftValues, OutError)
|
||||
|| !TryGetRequiredNodeArrayField(NodeObject, TEXT("right"), RightValues, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseJsonNodeArray(*LeftValues, OutNode.Conjugate.A, OutError)
|
||||
&& TryParseJsonNodeArray(*RightValues, OutNode.Conjugate.B, OutError);
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("pause"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Pause);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("newline"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Newline);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NodeKind == TEXT("comment"))
|
||||
{
|
||||
OutNode = MakeUnitNode(EHyperTwistAlgorithmNodeType::Comment);
|
||||
|
||||
FString CommentKind;
|
||||
if (!TryGetRequiredStringField(NodeObject, TEXT("comment_kind"), CommentKind, OutError)
|
||||
|| !TryGetRequiredStringField(NodeObject, TEXT("text"), OutNode.Comment.CommentText, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CommentKind == TEXT("line"))
|
||||
{
|
||||
OutNode.Comment.CommentType = EHyperTwistAlgorithmCommentType::Line;
|
||||
return true;
|
||||
}
|
||||
if (CommentKind == TEXT("block"))
|
||||
{
|
||||
OutNode.Comment.CommentType = EHyperTwistAlgorithmCommentType::Block;
|
||||
return true;
|
||||
}
|
||||
|
||||
OutError = FString::Printf(TEXT("Unsupported comment_kind '%s'"), *CommentKind);
|
||||
return false;
|
||||
}
|
||||
|
||||
OutError = FString::Printf(TEXT("Unsupported node kind '%s'"), *NodeKind);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseJsonNodeArray(
|
||||
const TArray<TSharedPtr<FJsonValue>>& NodeValues,
|
||||
TArray<FHyperTwistAlgorithmNode>& OutNodes,
|
||||
FString& OutError)
|
||||
{
|
||||
OutNodes.Reset();
|
||||
OutNodes.Reserve(NodeValues.Num());
|
||||
|
||||
for (int32 Index = 0; Index < NodeValues.Num(); ++Index)
|
||||
{
|
||||
TSharedPtr<FJsonObject> NodeObject;
|
||||
if (!TryReadJsonObject(
|
||||
NodeValues[Index],
|
||||
FString::Printf(TEXT("root[%d]"), Index),
|
||||
NodeObject,
|
||||
OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistAlgorithmNode ParsedNode;
|
||||
if (!TryParseJsonNodeObject(NodeObject, ParsedNode, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("root[%d]: %s"), Index, *OutError);
|
||||
return false;
|
||||
}
|
||||
|
||||
OutNodes.Add(MoveTemp(ParsedNode));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FHyperTwistAlgorithmParseResult UHyperTwistAlgorithmParser::ParseAlgorithm(const FString& AlgorithmText)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmParserInternal;
|
||||
|
||||
FHyperTwistAlgorithmParseResult Result;
|
||||
FParserState State(AlgorithmText);
|
||||
TArray<FHyperTwistAlgorithmNode> ParsedNodes;
|
||||
|
||||
if (TryParseNodeSequence(State, ParsedNodes, {}))
|
||||
{
|
||||
Result.bSuccess = true;
|
||||
Result.Sequence.Nodes = MoveTemp(ParsedNodes);
|
||||
return Result;
|
||||
}
|
||||
|
||||
Result.bSuccess = false;
|
||||
Result.ErrorMessage = State.ErrorMessage;
|
||||
Result.ErrorPosition = State.Position;
|
||||
return Result;
|
||||
}
|
||||
|
||||
FHyperTwistAlgorithmSequence UHyperTwistAlgorithmParser::ParseAlgorithmOrEmpty(const FString& AlgorithmText)
|
||||
{
|
||||
const FHyperTwistAlgorithmParseResult Result = ParseAlgorithm(AlgorithmText);
|
||||
return Result.bSuccess ? Result.Sequence : FHyperTwistAlgorithmSequence();
|
||||
}
|
||||
|
||||
bool UHyperTwistAlgorithmParser::TryParseFromJson(
|
||||
const FString& JsonText,
|
||||
FHyperTwistAlgorithmSequence& OutSequence,
|
||||
FString& OutError
|
||||
)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmParserInternal;
|
||||
|
||||
OutSequence = FHyperTwistAlgorithmSequence();
|
||||
OutError.Empty();
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Structured JSON payload could not be parsed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString SchemaName;
|
||||
if (!TryGetRequiredStringField(RootObject, TEXT("schema"), SchemaName, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SchemaName != StructuredJsonSchema)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Unsupported structured JSON schema '%s'"), *SchemaName);
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* RootValues = nullptr;
|
||||
if (!TryGetRequiredNodeArrayField(RootObject, TEXT("root"), RootValues, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseJsonNodeArray(*RootValues, OutSequence.Nodes, OutError);
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
|
||||
namespace HyperTwistAlgorithmSerializerInternal
|
||||
{
|
||||
const TCHAR* StructuredJsonSchema = TEXT("ht-alg/v1");
|
||||
|
||||
void AppendSignedAmountSuffix(const int32 Amount, FString& OutText)
|
||||
{
|
||||
const int32 AbsoluteAmount = FMath::Abs(Amount);
|
||||
if (AbsoluteAmount != 1 || Amount == 0)
|
||||
{
|
||||
OutText.Append(FString::FromInt(AbsoluteAmount));
|
||||
}
|
||||
|
||||
if (Amount < 0)
|
||||
{
|
||||
OutText.Append(TEXT("'"));
|
||||
}
|
||||
}
|
||||
|
||||
void SerializeNodeList(const TArray<FHyperTwistAlgorithmNode>& Nodes, FString& OutText);
|
||||
|
||||
TSharedPtr<FJsonObject> BuildNodeObject(const FHyperTwistAlgorithmNode& Node);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> BuildNodeArray(const TArray<FHyperTwistAlgorithmNode>& Nodes)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
Values.Reserve(Nodes.Num());
|
||||
for (const FHyperTwistAlgorithmNode& Node : Nodes)
|
||||
{
|
||||
Values.Add(MakeShared<FJsonValueObject>(BuildNodeObject(Node)));
|
||||
}
|
||||
|
||||
return Values;
|
||||
}
|
||||
|
||||
void SerializeBlockMove(const FHyperTwistAlgorithmBlockMove& Move, FString& OutText)
|
||||
{
|
||||
const EHyperTwistAlgorithmMoveType CanonicalMoveType = Move.GetCanonicalMoveType();
|
||||
if (CanonicalMoveType == EHyperTwistAlgorithmMoveType::InnerSlice)
|
||||
{
|
||||
OutText.Append(FString::FromInt(Move.GetCanonicalInnerLayer()));
|
||||
}
|
||||
else if (CanonicalMoveType == EHyperTwistAlgorithmMoveType::RangedSlice)
|
||||
{
|
||||
OutText.Append(FString::FromInt(Move.GetCanonicalInnerLayer()));
|
||||
OutText.Append(TEXT("-"));
|
||||
OutText.Append(FString::FromInt(Move.GetCanonicalOuterLayer()));
|
||||
}
|
||||
|
||||
OutText.Append(Move.Family);
|
||||
if (Move.UsesWideSuffix())
|
||||
{
|
||||
OutText.Append(TEXT("w"));
|
||||
}
|
||||
|
||||
AppendSignedAmountSuffix(Move.Amount, OutText);
|
||||
}
|
||||
|
||||
void SerializeComment(const FHyperTwistAlgorithmComment& Comment, FString& OutText)
|
||||
{
|
||||
if (Comment.CommentType == EHyperTwistAlgorithmCommentType::Block)
|
||||
{
|
||||
if (Comment.CommentText.IsEmpty())
|
||||
{
|
||||
OutText.Append(TEXT("/**/"));
|
||||
return;
|
||||
}
|
||||
|
||||
OutText.Append(TEXT("/* "));
|
||||
OutText.Append(Comment.CommentText);
|
||||
OutText.Append(TEXT(" */"));
|
||||
return;
|
||||
}
|
||||
|
||||
OutText.Append(TEXT("//"));
|
||||
if (!Comment.CommentText.IsEmpty())
|
||||
{
|
||||
OutText.Append(TEXT(" "));
|
||||
OutText.Append(Comment.CommentText);
|
||||
}
|
||||
}
|
||||
|
||||
void SerializeNode(const FHyperTwistAlgorithmNode& Node, FString& OutText)
|
||||
{
|
||||
switch (Node.NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
SerializeBlockMove(Node.BlockMove, OutText);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
OutText.Append(TEXT("("));
|
||||
SerializeNodeList(Node.Group.Inner, OutText);
|
||||
OutText.Append(TEXT(")"));
|
||||
if (Node.Group.Amount != 1)
|
||||
{
|
||||
AppendSignedAmountSuffix(Node.Group.Amount, OutText);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
OutText.Append(TEXT("["));
|
||||
SerializeNodeList(Node.Commutator.A, OutText);
|
||||
OutText.Append(TEXT(", "));
|
||||
SerializeNodeList(Node.Commutator.B, OutText);
|
||||
OutText.Append(TEXT("]"));
|
||||
if (Node.Commutator.Amount != 1)
|
||||
{
|
||||
AppendSignedAmountSuffix(Node.Commutator.Amount, OutText);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
OutText.Append(TEXT("["));
|
||||
SerializeNodeList(Node.Conjugate.A, OutText);
|
||||
OutText.Append(TEXT(": "));
|
||||
SerializeNodeList(Node.Conjugate.B, OutText);
|
||||
OutText.Append(TEXT("]"));
|
||||
if (Node.Conjugate.Amount != 1)
|
||||
{
|
||||
AppendSignedAmountSuffix(Node.Conjugate.Amount, OutText);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Pause:
|
||||
OutText.Append(TEXT("."));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Newline:
|
||||
OutText.Append(TEXT("\n"));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
SerializeComment(Node.Comment, OutText);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Sequence:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool NeedsSeparator(
|
||||
const FHyperTwistAlgorithmNode& PreviousNode,
|
||||
const FHyperTwistAlgorithmNode& CurrentNode)
|
||||
{
|
||||
if (PreviousNode.NodeType == EHyperTwistAlgorithmNodeType::Newline
|
||||
|| CurrentNode.NodeType == EHyperTwistAlgorithmNodeType::Newline)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PreviousNode.NodeType == EHyperTwistAlgorithmNodeType::Pause
|
||||
&& CurrentNode.NodeType == EHyperTwistAlgorithmNodeType::Pause)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SerializeNodeList(const TArray<FHyperTwistAlgorithmNode>& Nodes, FString& OutText)
|
||||
{
|
||||
for (int32 Index = 0; Index < Nodes.Num(); ++Index)
|
||||
{
|
||||
if (Index > 0 && NeedsSeparator(Nodes[Index - 1], Nodes[Index]))
|
||||
{
|
||||
OutText.Append(TEXT(" "));
|
||||
}
|
||||
|
||||
SerializeNode(Nodes[Index], OutText);
|
||||
}
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> BuildNodeObject(const FHyperTwistAlgorithmNode& Node)
|
||||
{
|
||||
TSharedPtr<FJsonObject> NodeObject = MakeShared<FJsonObject>();
|
||||
|
||||
switch (Node.NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
{
|
||||
const EHyperTwistAlgorithmMoveType CanonicalMoveType = Node.BlockMove.GetCanonicalMoveType();
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("move"));
|
||||
NodeObject->SetStringField(TEXT("family"), Node.BlockMove.Family);
|
||||
NodeObject->SetNumberField(TEXT("amount"), Node.BlockMove.Amount);
|
||||
NodeObject->SetBoolField(TEXT("wide"), Node.BlockMove.UsesWideSuffix());
|
||||
if (CanonicalMoveType == EHyperTwistAlgorithmMoveType::InnerSlice)
|
||||
{
|
||||
NodeObject->SetStringField(TEXT("layer_mode"), TEXT("index"));
|
||||
NodeObject->SetNumberField(TEXT("layer_index"), Node.BlockMove.GetCanonicalInnerLayer());
|
||||
}
|
||||
else if (CanonicalMoveType == EHyperTwistAlgorithmMoveType::RangedSlice)
|
||||
{
|
||||
NodeObject->SetStringField(TEXT("layer_mode"), TEXT("range"));
|
||||
NodeObject->SetNumberField(TEXT("layer_index"), Node.BlockMove.GetCanonicalInnerLayer());
|
||||
NodeObject->SetNumberField(TEXT("layer_end"), Node.BlockMove.GetCanonicalOuterLayer());
|
||||
}
|
||||
else
|
||||
{
|
||||
NodeObject->SetStringField(TEXT("layer_mode"), TEXT("plain"));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("group"));
|
||||
NodeObject->SetNumberField(TEXT("amount"), Node.Group.Amount);
|
||||
NodeObject->SetArrayField(TEXT("items"), BuildNodeArray(Node.Group.Inner));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("commutator"));
|
||||
NodeObject->SetNumberField(TEXT("amount"), Node.Commutator.Amount);
|
||||
NodeObject->SetArrayField(TEXT("left"), BuildNodeArray(Node.Commutator.A));
|
||||
NodeObject->SetArrayField(TEXT("right"), BuildNodeArray(Node.Commutator.B));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("conjugate"));
|
||||
NodeObject->SetNumberField(TEXT("amount"), Node.Conjugate.Amount);
|
||||
NodeObject->SetArrayField(TEXT("left"), BuildNodeArray(Node.Conjugate.A));
|
||||
NodeObject->SetArrayField(TEXT("right"), BuildNodeArray(Node.Conjugate.B));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Pause:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("pause"));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Newline:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("newline"));
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("comment"));
|
||||
NodeObject->SetStringField(
|
||||
TEXT("comment_kind"),
|
||||
Node.Comment.CommentType == EHyperTwistAlgorithmCommentType::Block ? TEXT("block") : TEXT("line"));
|
||||
NodeObject->SetStringField(TEXT("text"), Node.Comment.CommentText);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Sequence:
|
||||
default:
|
||||
NodeObject->SetStringField(TEXT("kind"), TEXT("unsupported"));
|
||||
break;
|
||||
}
|
||||
|
||||
return NodeObject;
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistAlgorithmSerializer::SerializeAlgorithm(const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
FString SerializedText;
|
||||
HyperTwistAlgorithmSerializerInternal::SerializeNodeList(Sequence.Nodes, SerializedText);
|
||||
return SerializedText;
|
||||
}
|
||||
|
||||
bool UHyperTwistAlgorithmSerializer::TrySerializeToJson(
|
||||
const FHyperTwistAlgorithmSequence& Sequence,
|
||||
FString& OutJson
|
||||
)
|
||||
{
|
||||
OutJson.Empty();
|
||||
TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
|
||||
RootObject->SetStringField(TEXT("schema"), HyperTwistAlgorithmSerializerInternal::StructuredJsonSchema);
|
||||
RootObject->SetArrayField(
|
||||
TEXT("root"),
|
||||
HyperTwistAlgorithmSerializerInternal::BuildNodeArray(Sequence.Nodes));
|
||||
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
||||
return FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer);
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane)
|
||||
// No source code inspection. Clean-room workflow only.
|
||||
// Date: 2026-05-15
|
||||
// Bound 2 — Traversal and transformation helpers
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal implementation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace HyperTwistAlgorithmTraversalInternal
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Block-move inversion
|
||||
// Negates Amount: 1→-1, -1→1, 2→-2, -2→2, 3→-3, etc.
|
||||
// All other fields (Family, MoveType, InnerLayer, OuterLayer) are unchanged.
|
||||
// -----------------------------------------------------------------------
|
||||
static FHyperTwistAlgorithmBlockMove InvertBlockMove(
|
||||
const FHyperTwistAlgorithmBlockMove& Move)
|
||||
{
|
||||
FHyperTwistAlgorithmBlockMove Result = Move;
|
||||
Result.Amount = -Move.Amount;
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Forward-declare so InvertNode can call InvertNodeList.
|
||||
static TArray<FHyperTwistAlgorithmNode> InvertNodeList(
|
||||
const TArray<FHyperTwistAlgorithmNode>& Nodes);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Node inversion
|
||||
//
|
||||
// BlockMove : negate Amount
|
||||
// Group : InvertNodeList(Inner), Amount unchanged
|
||||
// Commutator : [A, B]' = [B, A] — swap operands
|
||||
// Conjugate : [A: B]' = [A: B'] — invert B; A unchanged
|
||||
// Pause / Newline / Comment : pass through
|
||||
// -----------------------------------------------------------------------
|
||||
static FHyperTwistAlgorithmNode InvertNode(const FHyperTwistAlgorithmNode& Node)
|
||||
{
|
||||
FHyperTwistAlgorithmNode Result = Node;
|
||||
|
||||
switch (Node.NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
Result.BlockMove = InvertBlockMove(Node.BlockMove);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
Result.Group.Inner = InvertNodeList(Node.Group.Inner);
|
||||
// Amount stays the same — the inverse repeats the inverted content
|
||||
// the same number of times, not in a different direction.
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
// [A, B]' = [B, A]
|
||||
Result.Commutator.A = Node.Commutator.B;
|
||||
Result.Commutator.B = Node.Commutator.A;
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
// [A: B]' = [A: B'] — setup A is unchanged; only B is inverted
|
||||
Result.Conjugate.A = Node.Conjugate.A;
|
||||
Result.Conjugate.B = InvertNodeList(Node.Conjugate.B);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Pause:
|
||||
case EHyperTwistAlgorithmNodeType::Newline:
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
case EHyperTwistAlgorithmNodeType::Sequence:
|
||||
default:
|
||||
// These node types pass through unchanged.
|
||||
break;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// InvertNodeList
|
||||
// Reverses the node ordering and inverts each node individually.
|
||||
// This implements the standard group inversion rule:
|
||||
// (A B C)' = C' B' A'
|
||||
// -----------------------------------------------------------------------
|
||||
static TArray<FHyperTwistAlgorithmNode> InvertNodeList(
|
||||
const TArray<FHyperTwistAlgorithmNode>& Nodes)
|
||||
{
|
||||
TArray<FHyperTwistAlgorithmNode> Result;
|
||||
Result.Reserve(Nodes.Num());
|
||||
|
||||
for (int32 Index = Nodes.Num() - 1; Index >= 0; --Index)
|
||||
{
|
||||
Result.Add(InvertNode(Nodes[Index]));
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Flat-move inversion helper used during expansion.
|
||||
// Appends the inverse of each move (in reverse order) to OutMoves.
|
||||
// Does not return a new array — appends directly for efficiency.
|
||||
// -----------------------------------------------------------------------
|
||||
static void AppendInvertedBlockMoves(
|
||||
const TArray<FHyperTwistAlgorithmBlockMove>& Moves,
|
||||
TArray<FHyperTwistAlgorithmBlockMove>& OutMoves)
|
||||
{
|
||||
for (int32 Index = Moves.Num() - 1; Index >= 0; --Index)
|
||||
{
|
||||
OutMoves.Add(InvertBlockMove(Moves[Index]));
|
||||
}
|
||||
}
|
||||
|
||||
static void AppendRepeatedMoves(
|
||||
const TArray<FHyperTwistAlgorithmBlockMove>& Moves,
|
||||
const int32 Amount,
|
||||
TArray<FHyperTwistAlgorithmBlockMove>& OutMoves)
|
||||
{
|
||||
const int32 RepeatCount = FMath::Abs(Amount);
|
||||
const bool bShouldInvert = Amount < 0;
|
||||
for (int32 RepeatIndex = 0; RepeatIndex < RepeatCount; ++RepeatIndex)
|
||||
{
|
||||
if (bShouldInvert)
|
||||
{
|
||||
AppendInvertedBlockMoves(Moves, OutMoves);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutMoves.Append(Moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declaration for ExpandNodeListToMoves.
|
||||
static void ExpandNodeListToMoves(
|
||||
const TArray<FHyperTwistAlgorithmNode>& Nodes,
|
||||
TArray<FHyperTwistAlgorithmBlockMove>& OutMoves);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ExpandNodeToMoves
|
||||
//
|
||||
// Recursively expands a single node into a flat BlockMove list.
|
||||
//
|
||||
// BlockMove → itself
|
||||
// Group n → inner expanded then repeated n times
|
||||
// (negative n → inverted inner repeated |n| times)
|
||||
// Commutator → A + B + A⁻¹ + B⁻¹
|
||||
// Conjugate → A + B + A⁻¹
|
||||
// Other → skipped (Pause, Newline, Comment not emitted)
|
||||
// -----------------------------------------------------------------------
|
||||
static void ExpandNodeToMoves(
|
||||
const FHyperTwistAlgorithmNode& Node,
|
||||
TArray<FHyperTwistAlgorithmBlockMove>& OutMoves)
|
||||
{
|
||||
switch (Node.NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
OutMoves.Add(Node.BlockMove);
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
{
|
||||
TArray<FHyperTwistAlgorithmBlockMove> InnerMoves;
|
||||
ExpandNodeListToMoves(Node.Group.Inner, InnerMoves);
|
||||
AppendRepeatedMoves(InnerMoves, Node.Group.Amount, OutMoves);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
{
|
||||
// [A, B] = A B A⁻¹ B⁻¹
|
||||
TArray<FHyperTwistAlgorithmBlockMove> AMoves;
|
||||
TArray<FHyperTwistAlgorithmBlockMove> BMoves;
|
||||
ExpandNodeListToMoves(Node.Commutator.A, AMoves);
|
||||
ExpandNodeListToMoves(Node.Commutator.B, BMoves);
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> ExpandedMoves;
|
||||
ExpandedMoves.Append(AMoves);
|
||||
ExpandedMoves.Append(BMoves);
|
||||
AppendInvertedBlockMoves(AMoves, ExpandedMoves);
|
||||
AppendInvertedBlockMoves(BMoves, ExpandedMoves);
|
||||
AppendRepeatedMoves(ExpandedMoves, Node.Commutator.Amount, OutMoves);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
{
|
||||
// [A: B] = A B A⁻¹
|
||||
TArray<FHyperTwistAlgorithmBlockMove> AMoves;
|
||||
TArray<FHyperTwistAlgorithmBlockMove> BMoves;
|
||||
ExpandNodeListToMoves(Node.Conjugate.A, AMoves);
|
||||
ExpandNodeListToMoves(Node.Conjugate.B, BMoves);
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> ExpandedMoves;
|
||||
ExpandedMoves.Append(AMoves);
|
||||
ExpandedMoves.Append(BMoves);
|
||||
AppendInvertedBlockMoves(AMoves, ExpandedMoves);
|
||||
AppendRepeatedMoves(ExpandedMoves, Node.Conjugate.Amount, OutMoves);
|
||||
}
|
||||
break;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Pause:
|
||||
case EHyperTwistAlgorithmNodeType::Newline:
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
case EHyperTwistAlgorithmNodeType::Sequence:
|
||||
default:
|
||||
// Structural/annotation nodes are not emitted in the flat output.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ExpandNodeListToMoves — expand an array of nodes in order
|
||||
// -----------------------------------------------------------------------
|
||||
static void ExpandNodeListToMoves(
|
||||
const TArray<FHyperTwistAlgorithmNode>& Nodes,
|
||||
TArray<FHyperTwistAlgorithmBlockMove>& OutMoves)
|
||||
{
|
||||
for (const FHyperTwistAlgorithmNode& Node : Nodes)
|
||||
{
|
||||
ExpandNodeToMoves(Node, OutMoves);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// NormalizeAmount
|
||||
//
|
||||
// Reduce an arbitrary integer turn amount to the canonical range.
|
||||
// Modulo 4 (since four quarter turns = identity for standard face turns),
|
||||
// then bias toward the shortest representation in [-2, 2]:
|
||||
// 0 → 0 (cancelled)
|
||||
// 1 or -3 → 1
|
||||
// -1 or 3 → -1
|
||||
// 2 or -2 → 2
|
||||
// -----------------------------------------------------------------------
|
||||
static int32 NormalizeAmount(int32 Amount)
|
||||
{
|
||||
// Reduce modulo 4 to range (-3 .. 3).
|
||||
Amount = Amount % 4;
|
||||
|
||||
// Bias to [-2, 2] for canonical shortest-path representation.
|
||||
if (Amount > 2)
|
||||
{
|
||||
Amount -= 4; // 3 → -1
|
||||
}
|
||||
else if (Amount < -2)
|
||||
{
|
||||
Amount += 4; // -3 → 1
|
||||
}
|
||||
|
||||
return Amount;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MovesAreCoalesceable
|
||||
//
|
||||
// Two moves coalesce if and only if they address the same physical slice:
|
||||
// same Family, same MoveType, same InnerLayer, same OuterLayer.
|
||||
// -----------------------------------------------------------------------
|
||||
static bool MovesAreCoalesceable(
|
||||
const FHyperTwistAlgorithmBlockMove& A,
|
||||
const FHyperTwistAlgorithmBlockMove& B)
|
||||
{
|
||||
return A.Family == B.Family
|
||||
&& A.UsesWideSuffix() == B.UsesWideSuffix()
|
||||
&& A.GetCanonicalMoveType() == B.GetCanonicalMoveType()
|
||||
&& A.GetCanonicalInnerLayer() == B.GetCanonicalInnerLayer()
|
||||
&& A.GetCanonicalOuterLayer() == B.GetCanonicalOuterLayer();
|
||||
}
|
||||
|
||||
} // namespace HyperTwistAlgorithmTraversalInternal
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UHyperTwistAlgorithmTraversal — public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
FHyperTwistAlgorithmSequence UHyperTwistAlgorithmTraversal::InvertSequence(
|
||||
const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmTraversalInternal;
|
||||
|
||||
FHyperTwistAlgorithmSequence Result;
|
||||
Result.Nodes = InvertNodeList(Sequence.Nodes);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> UHyperTwistAlgorithmTraversal::ExpandToFlatMoves(
|
||||
const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmTraversalInternal;
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> Result;
|
||||
ExpandNodeListToMoves(Sequence.Nodes, Result);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> UHyperTwistAlgorithmTraversal::CoalesceMoves(
|
||||
const TArray<FHyperTwistAlgorithmBlockMove>& Moves)
|
||||
{
|
||||
using namespace HyperTwistAlgorithmTraversalInternal;
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> Result;
|
||||
Result.Reserve(Moves.Num());
|
||||
|
||||
for (const FHyperTwistAlgorithmBlockMove& Move : Moves)
|
||||
{
|
||||
const int32 NormalizedAmount = NormalizeAmount(Move.Amount);
|
||||
if (NormalizedAmount == 0)
|
||||
{
|
||||
// A move with a zero net amount is a no-op; skip it entirely.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Result.Num() > 0 && MovesAreCoalesceable(Result.Last(), Move))
|
||||
{
|
||||
// Combine with the previous move.
|
||||
const int32 Combined = NormalizeAmount(Result.Last().Amount + Move.Amount);
|
||||
if (Combined == 0)
|
||||
{
|
||||
// They cancel — remove the previous move.
|
||||
Result.RemoveAt(Result.Num() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Result.Last().Amount = Combined;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Start a new coalescing window with this move (normalized).
|
||||
FHyperTwistAlgorithmBlockMove NormalizedMove = Move;
|
||||
NormalizedMove.Amount = NormalizedAmount;
|
||||
Result.Add(NormalizedMove);
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistAlgorithmBlockMove> UHyperTwistAlgorithmTraversal::ExpandAndSimplify(
|
||||
const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
return CoalesceMoves(ExpandToFlatMoves(Sequence));
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
// Model: Claude (Model B, Bound 1 contribution)
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
|
||||
bool FHyperTwistAlgorithmNode::operator==(const FHyperTwistAlgorithmNode& Other) const
|
||||
{
|
||||
if (NodeType != Other.NodeType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (NodeType)
|
||||
{
|
||||
case EHyperTwistAlgorithmNodeType::BlockMove:
|
||||
return BlockMove == Other.BlockMove;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Group:
|
||||
{
|
||||
if (Group.Amount != Other.Group.Amount || Group.Inner.Num() != Other.Group.Inner.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < Group.Inner.Num(); ++Index)
|
||||
{
|
||||
if (!(Group.Inner[Index] == Other.Group.Inner[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Commutator:
|
||||
{
|
||||
if (Commutator.Amount != Other.Commutator.Amount
|
||||
|| Commutator.A.Num() != Other.Commutator.A.Num()
|
||||
|| Commutator.B.Num() != Other.Commutator.B.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < Commutator.A.Num(); ++Index)
|
||||
{
|
||||
if (!(Commutator.A[Index] == Other.Commutator.A[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int32 Index = 0; Index < Commutator.B.Num(); ++Index)
|
||||
{
|
||||
if (!(Commutator.B[Index] == Other.Commutator.B[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Conjugate:
|
||||
{
|
||||
if (Conjugate.Amount != Other.Conjugate.Amount
|
||||
|| Conjugate.A.Num() != Other.Conjugate.A.Num()
|
||||
|| Conjugate.B.Num() != Other.Conjugate.B.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < Conjugate.A.Num(); ++Index)
|
||||
{
|
||||
if (!(Conjugate.A[Index] == Other.Conjugate.A[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int32 Index = 0; Index < Conjugate.B.Num(); ++Index)
|
||||
{
|
||||
if (!(Conjugate.B[Index] == Other.Conjugate.B[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Comment:
|
||||
return Comment.CommentType == Other.Comment.CommentType
|
||||
&& Comment.CommentText == Other.Comment.CommentText;
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Pause:
|
||||
case EHyperTwistAlgorithmNodeType::Newline:
|
||||
return true; // These have no payload
|
||||
|
||||
case EHyperTwistAlgorithmNodeType::Sequence:
|
||||
default:
|
||||
return false; // Sequence nodes should not be nested
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
|
||||
namespace HyperTwistCoreLibraryInternal
|
||||
|
|
@ -109,6 +110,21 @@ FHyperTwistNotationNormalizationResult UHyperTwistCoreLibrary::NormalizeNotation
|
|||
return Result;
|
||||
}
|
||||
|
||||
bool UHyperTwistCoreLibrary::TryCanonicalizeAlgorithmNotation(
|
||||
const FString& RawNotation,
|
||||
FString& OutCanonicalNotation,
|
||||
FString& OutErrorMessage,
|
||||
int32& OutErrorPosition
|
||||
)
|
||||
{
|
||||
return UHyperTwistAlgorithmLibrary::TryCanonicalizeAlgorithmText(
|
||||
RawNotation,
|
||||
OutCanonicalNotation,
|
||||
OutErrorMessage,
|
||||
OutErrorPosition
|
||||
);
|
||||
}
|
||||
|
||||
bool UHyperTwistCoreLibrary::IsSolved(const FHyperTwistPuzzleState& State)
|
||||
{
|
||||
if (!State.IsStructurallyValid())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
|
||||
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmParser.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
|
||||
|
||||
|
|
@ -10,6 +13,24 @@ namespace HyperTwistTrainingRuntimeLibraryInternal
|
|||
{
|
||||
if (WorldContextObject == nullptr)
|
||||
{
|
||||
if (GEngine != nullptr)
|
||||
{
|
||||
for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
|
||||
{
|
||||
UWorld* CandidateWorld = WorldContext.World();
|
||||
if (CandidateWorld == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UGameInstance* CandidateGameInstance = CandidateWorld->GetGameInstance();
|
||||
if (CandidateGameInstance != nullptr)
|
||||
{
|
||||
return CandidateGameInstance->GetSubsystem<UHyperTwistTrainingSubsystem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -901,6 +922,41 @@ FString UHyperTwistTrainingRuntimeLibrary::BuildBundledClassicCubingPackageCheck
|
|||
);
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRuntimeLibrary::ParseAlgJs(const FString& AlgorithmString, FString& OutError)
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(nullptr))
|
||||
{
|
||||
const FHyperTwistAlgorithmParseResult ParseResult =
|
||||
UHyperTwistAlgorithmParser::ParseAlgorithm(AlgorithmString);
|
||||
if (ParseResult.bSuccess)
|
||||
{
|
||||
TrainingSubsystem->SetActiveAlgJsSequence(ParseResult.Sequence);
|
||||
OutError.Empty();
|
||||
return true;
|
||||
}
|
||||
|
||||
OutError = ParseResult.ErrorMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutError = TEXT("Training subsystem not found.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString UHyperTwistTrainingRuntimeLibrary::SerializeAlgJs()
|
||||
{
|
||||
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(nullptr))
|
||||
{
|
||||
FHyperTwistAlgorithmSequence Sequence;
|
||||
if (TrainingSubsystem->TryGetActiveAlgJsSequence(Sequence))
|
||||
{
|
||||
return UHyperTwistAlgorithmSerializer::SerializeAlgorithm(Sequence);
|
||||
}
|
||||
}
|
||||
return TEXT("");
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRuntimeLibrary::TryGetBundledLegacy4DHistoryContract(
|
||||
const FString& ContractId,
|
||||
FHyperTwistTrainingLegacy4DHistoryContract& OutContract
|
||||
|
|
|
|||
|
|
@ -1953,37 +1953,55 @@ namespace HyperTwistTrainingSubsystemInternal
|
|||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveRun() const
|
||||
{
|
||||
return bHasActiveRun && ActiveRunState.IsStructurallyValid();
|
||||
}
|
||||
void UHyperTwistTrainingSubsystem::SetActiveAlgJsSequence(const FHyperTwistAlgorithmSequence& Sequence)
|
||||
{
|
||||
ActiveAlgJsSequence = Sequence;
|
||||
bHasActiveAlgJsSequence = true;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveMethodDrillRun() const
|
||||
{
|
||||
return bHasActiveMethodDrillRun && ActiveMethodDrillRunState.IsStructurallyValid();
|
||||
}
|
||||
bool UHyperTwistTrainingSubsystem::TryGetActiveAlgJsSequence(FHyperTwistAlgorithmSequence& OutSequence) const
|
||||
{
|
||||
if (!bHasActiveAlgJsSequence)
|
||||
{
|
||||
OutSequence = FHyperTwistAlgorithmSequence();
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::GetActiveRunState() const
|
||||
{
|
||||
return ActiveRunState;
|
||||
}
|
||||
OutSequence = ActiveAlgJsSequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveLiveTimer() const
|
||||
{
|
||||
return HasActiveRun()
|
||||
&& ActiveRunState.Session.IsStructurallyValid()
|
||||
&& ActiveLiveTimerState.IsActive()
|
||||
&& ActiveLiveTimerState.TrainingSessionId == ActiveRunState.Session.TrainingSessionId
|
||||
&& ActiveLiveTimerState.CaseId == ActiveRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
}
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveRun() const
|
||||
{
|
||||
return bHasActiveRun && ActiveRunState.IsStructurallyValid();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingLiveTimerState UHyperTwistTrainingSubsystem::GetActiveLiveTimerState()
|
||||
{
|
||||
RefreshActiveLiveTimerState();
|
||||
return ActiveLiveTimerState;
|
||||
}
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveMethodDrillRun() const
|
||||
{
|
||||
return bHasActiveMethodDrillRun && ActiveMethodDrillRunState.IsStructurallyValid();
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::TryGetActiveImportedRuntimeSurface(
|
||||
FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::GetActiveRunState() const
|
||||
{
|
||||
return ActiveRunState;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::HasActiveLiveTimer() const
|
||||
{
|
||||
return HasActiveRun()
|
||||
&& ActiveRunState.Session.IsStructurallyValid()
|
||||
&& ActiveLiveTimerState.IsActive()
|
||||
&& ActiveLiveTimerState.TrainingSessionId == ActiveRunState.Session.TrainingSessionId
|
||||
&& ActiveLiveTimerState.CaseId == ActiveRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingLiveTimerState UHyperTwistTrainingSubsystem::GetActiveLiveTimerState()
|
||||
{
|
||||
RefreshActiveLiveTimerState();
|
||||
return ActiveLiveTimerState;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingSubsystem::TryGetActiveImportedRuntimeSurface(
|
||||
FHyperTwistTrainingImportedRuntimeSurface& OutRuntimeSurface
|
||||
) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
// Model: Claude (Model B, Bound 1 contribution)
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
#include "HyperTwistAlgorithmLibrary.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistAlgorithmLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static bool TryCanonicalizeAlgorithmText(
|
||||
const FString& AlgorithmText,
|
||||
FString& OutCanonicalText,
|
||||
FString& OutErrorMessage,
|
||||
int32& OutErrorPosition
|
||||
);
|
||||
|
||||
// Round-trip validation: Parse → Serialize → Parse → Compare
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static bool ValidateRoundTrip(
|
||||
const FString& AlgorithmText,
|
||||
FString& OutReserializedText,
|
||||
bool& bOutStructurallyEqual
|
||||
);
|
||||
|
||||
// Diagnostic TSV export
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm")
|
||||
static FString BuildAlgorithmDebugTsv(const FHyperTwistAlgorithmSequence& Sequence);
|
||||
|
||||
// Structural equality check
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm")
|
||||
static bool AreAlgorithmsStructurallyEqual(
|
||||
const FHyperTwistAlgorithmSequence& A,
|
||||
const FHyperTwistAlgorithmSequence& B
|
||||
);
|
||||
|
||||
// Get subsystem provenance information
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm")
|
||||
static FString GetAlgorithmSubsystemProvenance();
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
// Model: Claude (Model B, Bound 1 contribution)
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
#include "HyperTwistAlgorithmParser.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistAlgorithmParser : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Parse algorithm text into AST
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static FHyperTwistAlgorithmParseResult ParseAlgorithm(const FString& AlgorithmText);
|
||||
|
||||
// Parse algorithm text, returning empty sequence on error
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static FHyperTwistAlgorithmSequence ParseAlgorithmOrEmpty(const FString& AlgorithmText);
|
||||
|
||||
// Parse from JSON representation
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static bool TryParseFromJson(
|
||||
const FString& JsonText,
|
||||
FHyperTwistAlgorithmSequence& OutSequence,
|
||||
FString& OutError
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Date: 2026-05-15
|
||||
// Model: Claude (Model B, Bound 1 contribution)
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
#include "HyperTwistAlgorithmSerializer.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistAlgorithmSerializer : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Serialize AST to canonical text representation
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm")
|
||||
static FString SerializeAlgorithm(const FHyperTwistAlgorithmSequence& Sequence);
|
||||
|
||||
// Serialize AST to JSON
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Algorithm")
|
||||
static bool TrySerializeToJson(
|
||||
const FHyperTwistAlgorithmSequence& Sequence,
|
||||
FString& OutJson
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
#pragma once
|
||||
|
||||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane)
|
||||
// No source code inspection. Clean-room workflow only.
|
||||
// Date: 2026-05-15
|
||||
// Bound 2 — Traversal and transformation helpers
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
#include "HyperTwistAlgorithmTraversal.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UHyperTwistAlgorithmTraversal
|
||||
//
|
||||
// Stateless traversal and transformation operations on algorithm ASTs.
|
||||
// All functions are pure (no side effects, no mutation of inputs).
|
||||
//
|
||||
// Bound 2 scope:
|
||||
// - InvertSequence : structural inverse of an algorithm tree
|
||||
// - ExpandToFlatMoves : flatten all structure to a BlockMove list
|
||||
// - CoalesceMoves : combine adjacent same-move occurrences, cancel zeroes
|
||||
// - ExpandAndSimplify : convenience composition of expand then coalesce
|
||||
//
|
||||
// Out of scope for this bound (deferred):
|
||||
// - Validation (Bound 3)
|
||||
// - JSON / URL / keyboard interchange (Bound 4)
|
||||
// - Integration with UHyperTwistMoveController or AWTscrambler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistAlgorithmTraversal : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// -----------------------------------------------------------------------
|
||||
// InvertSequence
|
||||
//
|
||||
// Produce the structural inverse of an algorithm sequence.
|
||||
//
|
||||
// Semantics per node type:
|
||||
// BlockMove : negate Amount (1→-1, -1→1, 2→-2, 3→-3, …)
|
||||
// Group : reverse Inner node list and invert each; Amount unchanged
|
||||
// Commutator : [A, B]' = [B, A] — swap A and B operands
|
||||
// Conjugate : [A: B]' = [A: B'] — invert B only; A (setup) is unchanged
|
||||
// Pause / Newline / Comment : pass through unchanged
|
||||
//
|
||||
// The top-level node list of the sequence is reversed and each element
|
||||
// is individually inverted.
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Traversal")
|
||||
static FHyperTwistAlgorithmSequence InvertSequence(
|
||||
const FHyperTwistAlgorithmSequence& Sequence
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ExpandToFlatMoves
|
||||
//
|
||||
// Recursively expand an algorithm sequence into an ordered flat list of
|
||||
// FHyperTwistAlgorithmBlockMove values. All structural nodes (Group,
|
||||
// Commutator, Conjugate) are resolved; Pause, Newline, and Comment nodes
|
||||
// are omitted from the flat output.
|
||||
//
|
||||
// Expansion rules:
|
||||
// BlockMove : emitted as-is
|
||||
// Group n : inner expanded, then that expansion repeated n times
|
||||
// (negative n: expand inverted inner |n| times)
|
||||
// Commutator : [A, B] → A_flat + B_flat + A_flat_inverted + B_flat_inverted
|
||||
// Conjugate : [A: B] → A_flat + B_flat + A_flat_inverted
|
||||
// Pause / Newline / Comment : skipped
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Traversal")
|
||||
static TArray<FHyperTwistAlgorithmBlockMove> ExpandToFlatMoves(
|
||||
const FHyperTwistAlgorithmSequence& Sequence
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// CoalesceMoves
|
||||
//
|
||||
// Accept a flat move list and return a simplified list where:
|
||||
// - Adjacent moves with the same Family, MoveType, InnerLayer, and
|
||||
// OuterLayer are combined by summing their Amount values.
|
||||
// - Amounts are normalized modulo 4 to the canonical range [-2, -1, 1, 2].
|
||||
// - Moves with a resulting amount of 0 are removed (cancelled).
|
||||
//
|
||||
// The function makes a single left-to-right pass; it does not repeat
|
||||
// until a fixed point. Callers who need fully-simplified output on complex
|
||||
// algorithms may call it iteratively if needed (in practice one pass
|
||||
// suffices for the common cases).
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Traversal")
|
||||
static TArray<FHyperTwistAlgorithmBlockMove> CoalesceMoves(
|
||||
const TArray<FHyperTwistAlgorithmBlockMove>& Moves
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ExpandAndSimplify
|
||||
//
|
||||
// Convenience: ExpandToFlatMoves followed by CoalesceMoves.
|
||||
// Equivalent to:
|
||||
// CoalesceMoves(ExpandToFlatMoves(Sequence))
|
||||
// -----------------------------------------------------------------------
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Traversal")
|
||||
static TArray<FHyperTwistAlgorithmBlockMove> ExpandAndSimplify(
|
||||
const FHyperTwistAlgorithmSequence& Sequence
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
#pragma once
|
||||
|
||||
// Clean-room implementation for HyperTwist algorithm-language subsystem
|
||||
// Implemented from governance docs only (Phase 0R-D, Phase 1R, Phase 5R-A)
|
||||
// Source: Behavioral contracts from cubing/alg.js Model A handoff (GPL-3.0-or-later lane)
|
||||
// No source code inspection. Clean-room workflow only.
|
||||
// Date: 2026-05-15
|
||||
// Bound 1 (initial types + structural equality) — structural UHT fix applied in Bound 2 pass:
|
||||
// Removed UPROPERTY from recursive TArray fields (Group.Inner, Commutator/Conjugate A/B)
|
||||
// because UHT cannot resolve circular USTRUCT references. Those fields remain accessible
|
||||
// in C++ but are not Blueprint-exposed. All non-recursive fields retain UPROPERTY.
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "HyperTwistAlgorithmTypes.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistAlgorithmNodeType : uint8
|
||||
{
|
||||
Sequence UMETA(DisplayName = "Sequence"),
|
||||
Group UMETA(DisplayName = "Group"),
|
||||
BlockMove UMETA(DisplayName = "BlockMove"),
|
||||
Commutator UMETA(DisplayName = "Commutator"),
|
||||
Conjugate UMETA(DisplayName = "Conjugate"),
|
||||
Pause UMETA(DisplayName = "Pause"),
|
||||
Newline UMETA(DisplayName = "Newline"),
|
||||
Comment UMETA(DisplayName = "Comment")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistAlgorithmMoveType : uint8
|
||||
{
|
||||
Plain UMETA(DisplayName = "Plain"), // no explicit layer prefix
|
||||
InnerSlice UMETA(DisplayName = "InnerSlice"), // N + family, e.g. 2R or 3Rw
|
||||
RangedSlice UMETA(DisplayName = "RangedSlice"), // A-B + family, e.g. 2-3Uw
|
||||
WideMove UMETA(DisplayName = "WideMove") // legacy wide encoding retained for compatibility
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistAlgorithmCommentType : uint8
|
||||
{
|
||||
Line UMETA(DisplayName = "Line"),
|
||||
Block UMETA(DisplayName = "Block")
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BlockMove — a single atomic move (Plain, InnerSlice, RangedSlice, or WideMove)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmBlockMove
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// Face / axis family: "R", "U", "D", "L", "F", "B", "M", "E", "S", "x", "y", "z", etc.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
FString Family;
|
||||
|
||||
// Turn amount: 1 = quarter CW, -1 = quarter CCW (prime), 2 = half, -2 = half prime, 3 = three-quarter CW
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 Amount = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
EHyperTwistAlgorithmMoveType MoveType = EHyperTwistAlgorithmMoveType::Plain;
|
||||
|
||||
// Wide-turn marker carried separately from the layer-prefix mode so that
|
||||
// notations such as 3Rw and 2-3Uw can be represented without losing the
|
||||
// prefix information. The older WideMove enum value remains readable for
|
||||
// compatibility with earlier local work-in-progress states.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
bool bIsWide = false;
|
||||
|
||||
// InnerSlice: the affected layer index (1-based). e.g. "2R" → InnerLayer=2.
|
||||
// RangedSlice: the start of the range. e.g. "3-5Uw" → InnerLayer=3, OuterLayer=5.
|
||||
// WideMove: OuterLayer = number of layers (default 2 for plain "Rw").
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 InnerLayer = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 OuterLayer = 0;
|
||||
|
||||
bool UsesWideSuffix() const
|
||||
{
|
||||
return bIsWide || MoveType == EHyperTwistAlgorithmMoveType::WideMove;
|
||||
}
|
||||
|
||||
EHyperTwistAlgorithmMoveType GetCanonicalMoveType() const
|
||||
{
|
||||
if (MoveType == EHyperTwistAlgorithmMoveType::WideMove)
|
||||
{
|
||||
return OuterLayer > 2
|
||||
? EHyperTwistAlgorithmMoveType::InnerSlice
|
||||
: EHyperTwistAlgorithmMoveType::Plain;
|
||||
}
|
||||
|
||||
return MoveType;
|
||||
}
|
||||
|
||||
int32 GetCanonicalInnerLayer() const
|
||||
{
|
||||
if (MoveType == EHyperTwistAlgorithmMoveType::WideMove && OuterLayer > 2)
|
||||
{
|
||||
return OuterLayer;
|
||||
}
|
||||
|
||||
return InnerLayer;
|
||||
}
|
||||
|
||||
int32 GetCanonicalOuterLayer() const
|
||||
{
|
||||
if (MoveType == EHyperTwistAlgorithmMoveType::WideMove)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return OuterLayer;
|
||||
}
|
||||
|
||||
bool operator==(const FHyperTwistAlgorithmBlockMove& Other) const
|
||||
{
|
||||
return Family == Other.Family
|
||||
&& Amount == Other.Amount
|
||||
&& UsesWideSuffix() == Other.UsesWideSuffix()
|
||||
&& GetCanonicalMoveType() == Other.GetCanonicalMoveType()
|
||||
&& GetCanonicalInnerLayer() == Other.GetCanonicalInnerLayer()
|
||||
&& GetCanonicalOuterLayer() == Other.GetCanonicalOuterLayer();
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHyperTwistAlgorithmNode — forward declaration required before composite types
|
||||
// (Because Group, Commutator, Conjugate all reference it.)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct FHyperTwistAlgorithmNode;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Composite sub-types
|
||||
// NOTE on UPROPERTY: The Inner/A/B arrays hold FHyperTwistAlgorithmNode, which
|
||||
// would create a circular USTRUCT reference that UHT cannot resolve. Those fields
|
||||
// are therefore NOT tagged as UPROPERTY. They are fully accessible in C++ and are
|
||||
// correctly serialized by the first-party algorithm library; they simply cannot be
|
||||
// directly exposed to Blueprints as nested properties.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmGroup
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// NOT a UPROPERTY — recursive USTRUCT array; UHT cannot resolve circular reference.
|
||||
TArray<FHyperTwistAlgorithmNode> Inner;
|
||||
|
||||
// Repeat count. Positive = repeat that many times. The expander handles the repetition.
|
||||
// Negative values represent the inverse direction of the repeated content.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 Amount = 1;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmCommutator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// NOT a UPROPERTY — recursive USTRUCT array.
|
||||
// [A, B] expands to: A B A⁻¹ B⁻¹
|
||||
TArray<FHyperTwistAlgorithmNode> A;
|
||||
TArray<FHyperTwistAlgorithmNode> B;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 Amount = 1;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmConjugate
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// NOT a UPROPERTY — recursive USTRUCT array.
|
||||
// [A: B] expands to: A B A⁻¹
|
||||
TArray<FHyperTwistAlgorithmNode> A;
|
||||
TArray<FHyperTwistAlgorithmNode> B;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 Amount = 1;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmComment
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
EHyperTwistAlgorithmCommentType CommentType = EHyperTwistAlgorithmCommentType::Line;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
FString CommentText;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHyperTwistAlgorithmNode — discriminated union over all node types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
EHyperTwistAlgorithmNodeType NodeType = EHyperTwistAlgorithmNodeType::BlockMove;
|
||||
|
||||
// Active field depends on NodeType. Only one is meaningful per node.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
FHyperTwistAlgorithmBlockMove BlockMove;
|
||||
|
||||
// Group, Commutator, Conjugate hold recursive arrays — see sub-type comments above.
|
||||
FHyperTwistAlgorithmGroup Group;
|
||||
FHyperTwistAlgorithmCommutator Commutator;
|
||||
FHyperTwistAlgorithmConjugate Conjugate;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
FHyperTwistAlgorithmComment Comment;
|
||||
|
||||
bool operator==(const FHyperTwistAlgorithmNode& Other) const;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHyperTwistAlgorithmSequence — top-level container for a parsed algorithm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmSequence
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// NOT a UPROPERTY — recursive USTRUCT array (nodes may contain sub-sequences).
|
||||
TArray<FHyperTwistAlgorithmNode> Nodes;
|
||||
|
||||
bool operator==(const FHyperTwistAlgorithmSequence& Other) const
|
||||
{
|
||||
if (Nodes.Num() != Other.Nodes.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32 Index = 0; Index < Nodes.Num(); ++Index)
|
||||
{
|
||||
if (!(Nodes[Index] == Other.Nodes[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHyperTwistAlgorithmParseResult — output of UHyperTwistAlgorithmParser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistAlgorithmParseResult
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
bool bSuccess = false;
|
||||
|
||||
// Valid when bSuccess == true.
|
||||
FHyperTwistAlgorithmSequence Sequence;
|
||||
|
||||
// Valid when bSuccess == false.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
FString ErrorMessage;
|
||||
|
||||
// Character position where parsing failed. -1 if not applicable.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Algorithm")
|
||||
int32 ErrorPosition = -1;
|
||||
};
|
||||
|
|
@ -56,6 +56,14 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
|
||||
static FHyperTwistNotationNormalizationResult NormalizeNotation(const FString& RawNotation);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Core")
|
||||
static bool TryCanonicalizeAlgorithmNotation(
|
||||
const FString& RawNotation,
|
||||
FString& OutCanonicalNotation,
|
||||
FString& OutErrorMessage,
|
||||
int32& OutErrorPosition
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
|
||||
static bool IsSolved(const FHyperTwistPuzzleState& State);
|
||||
|
||||
|
|
|
|||
|
|
@ -352,6 +352,12 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|ClassicCubing")
|
||||
static FString BuildBundledClassicCubingPackageChecklistTsv();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Alg.js")
|
||||
static bool ParseAlgJs(const FString& AlgorithmString, FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Alg.js")
|
||||
static FString SerializeAlgJs();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Legacy4D")
|
||||
static bool TryGetBundledLegacy4DHistoryContract(
|
||||
const FString& ContractId,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h"
|
||||
#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h"
|
||||
#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h"
|
||||
|
|
@ -595,6 +596,9 @@ public:
|
|||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Recognition")
|
||||
bool CloseActiveRecognitionSession(FString& OutError);
|
||||
|
||||
void SetActiveAlgJsSequence(const FHyperTwistAlgorithmSequence& Sequence);
|
||||
bool TryGetActiveAlgJsSequence(FHyperTwistAlgorithmSequence& OutSequence) const;
|
||||
|
||||
private:
|
||||
void RefreshSummary();
|
||||
void RefreshMethodDrillSummary();
|
||||
|
|
@ -790,4 +794,7 @@ private:
|
|||
double ActiveLiveTimerPausedAtSeconds = 0.0;
|
||||
int32 ActiveLiveTimerStoredInspectionElapsedMs = 0;
|
||||
int32 ActiveLiveTimerStoredSolveElapsedMs = 0;
|
||||
|
||||
bool bHasActiveAlgJsSequence = false;
|
||||
FHyperTwistAlgorithmSequence ActiveAlgJsSequence;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue