Add first-party cubing alg Bound 1 surface

This commit is contained in:
axiomlogicnexus 2026-05-15 06:28:29 +02:00
parent d34e923d8a
commit fe75513126
17 changed files with 2584 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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