diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp new file mode 100644 index 0000000..a012282 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmAutomationTests.cpp @@ -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 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 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 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 diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.cpp new file mode 100644 index 0000000..ab66d78 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.cpp @@ -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; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmParser.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmParser.cpp new file mode 100644 index 0000000..77f7b3e --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmParser.cpp @@ -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& OutNodes, + const TArray& 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 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 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 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& OutNodes, + const TArray& 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& Value, + const FString& Context, + TSharedPtr& 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& 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& 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(Rounded); + return true; + } + + bool TryGetRequiredNodeArrayField( + const TSharedPtr& Object, + const TCHAR* FieldName, + const TArray>*& 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>& NodeValues, + TArray& OutNodes, + FString& OutError); + + bool TryParseJsonNodeObject( + const TSharedPtr& 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>* 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>* LeftValues = nullptr; + const TArray>* 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>* LeftValues = nullptr; + const TArray>* 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>& NodeValues, + TArray& OutNodes, + FString& OutError) + { + OutNodes.Reset(); + OutNodes.Reserve(NodeValues.Num()); + + for (int32 Index = 0; Index < NodeValues.Num(); ++Index) + { + TSharedPtr 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 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 RootObject; + const TSharedRef> 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>* RootValues = nullptr; + if (!TryGetRequiredNodeArrayField(RootObject, TEXT("root"), RootValues, OutError)) + { + return false; + } + + return TryParseJsonNodeArray(*RootValues, OutSequence.Nodes, OutError); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.cpp new file mode 100644 index 0000000..65c83a2 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.cpp @@ -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& Nodes, FString& OutText); + + TSharedPtr BuildNodeObject(const FHyperTwistAlgorithmNode& Node); + + TArray> BuildNodeArray(const TArray& Nodes) + { + TArray> Values; + Values.Reserve(Nodes.Num()); + for (const FHyperTwistAlgorithmNode& Node : Nodes) + { + Values.Add(MakeShared(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& 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 BuildNodeObject(const FHyperTwistAlgorithmNode& Node) + { + TSharedPtr NodeObject = MakeShared(); + + 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 RootObject = MakeShared(); + RootObject->SetStringField(TEXT("schema"), HyperTwistAlgorithmSerializerInternal::StructuredJsonSchema); + RootObject->SetArrayField( + TEXT("root"), + HyperTwistAlgorithmSerializerInternal::BuildNodeArray(Sequence.Nodes)); + + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutJson); + return FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.cpp new file mode 100644 index 0000000..beab94c --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.cpp @@ -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 InvertNodeList( + const TArray& 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 InvertNodeList( + const TArray& Nodes) + { + TArray 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& Moves, + TArray& OutMoves) + { + for (int32 Index = Moves.Num() - 1; Index >= 0; --Index) + { + OutMoves.Add(InvertBlockMove(Moves[Index])); + } + } + + static void AppendRepeatedMoves( + const TArray& Moves, + const int32 Amount, + TArray& 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& Nodes, + TArray& 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& OutMoves) + { + switch (Node.NodeType) + { + case EHyperTwistAlgorithmNodeType::BlockMove: + OutMoves.Add(Node.BlockMove); + break; + + case EHyperTwistAlgorithmNodeType::Group: + { + TArray InnerMoves; + ExpandNodeListToMoves(Node.Group.Inner, InnerMoves); + AppendRepeatedMoves(InnerMoves, Node.Group.Amount, OutMoves); + } + break; + + case EHyperTwistAlgorithmNodeType::Commutator: + { + // [A, B] = A B A⁻¹ B⁻¹ + TArray AMoves; + TArray BMoves; + ExpandNodeListToMoves(Node.Commutator.A, AMoves); + ExpandNodeListToMoves(Node.Commutator.B, BMoves); + + TArray 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 AMoves; + TArray BMoves; + ExpandNodeListToMoves(Node.Conjugate.A, AMoves); + ExpandNodeListToMoves(Node.Conjugate.B, BMoves); + + TArray 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& Nodes, + TArray& 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 UHyperTwistAlgorithmTraversal::ExpandToFlatMoves( + const FHyperTwistAlgorithmSequence& Sequence) +{ + using namespace HyperTwistAlgorithmTraversalInternal; + + TArray Result; + ExpandNodeListToMoves(Sequence.Nodes, Result); + return Result; +} + +TArray UHyperTwistAlgorithmTraversal::CoalesceMoves( + const TArray& Moves) +{ + using namespace HyperTwistAlgorithmTraversalInternal; + + TArray 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 UHyperTwistAlgorithmTraversal::ExpandAndSimplify( + const FHyperTwistAlgorithmSequence& Sequence) +{ + return CoalesceMoves(ExpandToFlatMoves(Sequence)); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.cpp new file mode 100644 index 0000000..f2965ca --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.cpp @@ -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 + } +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp index 1fe2b95..926a5ba 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp @@ -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()) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 8cab90e..f4f3cb0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -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(); + } + } + } + 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 diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 63a5013..b88aaec 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -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 { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h new file mode 100644 index 0000000..84a1bfb --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmLibrary.h @@ -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(); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmParser.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmParser.h new file mode 100644 index 0000000..584b4c1 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmParser.h @@ -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 + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h new file mode 100644 index 0000000..8bff2e1 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmSerializer.h @@ -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 + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.h new file mode 100644 index 0000000..78d3ee4 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTraversal.h @@ -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 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 CoalesceMoves( + const TArray& Moves + ); + + // ----------------------------------------------------------------------- + // ExpandAndSimplify + // + // Convenience: ExpandToFlatMoves followed by CoalesceMoves. + // Equivalent to: + // CoalesceMoves(ExpandToFlatMoves(Sequence)) + // ----------------------------------------------------------------------- + UFUNCTION(BlueprintPure, Category = "HyperTwist|Algorithm|Traversal") + static TArray ExpandAndSimplify( + const FHyperTwistAlgorithmSequence& Sequence + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h new file mode 100644 index 0000000..3e9aca3 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistAlgorithm/HyperTwistAlgorithmTypes.h @@ -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 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 A; + TArray 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 A; + TArray 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 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; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h index 06e68ba..0575a6f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h @@ -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); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 0efff9f..048ea4b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -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, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 760d192..eebe252 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -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; };