Add HyperTwist Melinda bound 3 core algebra

This commit is contained in:
axiomlogicnexus 2026-05-16 19:39:46 +02:00
parent ba59e59a00
commit 096c1f52ea
4 changed files with 952 additions and 2 deletions

View file

@ -169,6 +169,27 @@ namespace HyperTwistCoreLibraryInternal
return FindMelindaOrientationIndex(ComposedPermutation);
}
int32 InvertMelindaOrientationIndex(const int32 OrientationIndex)
{
if (!IsValidMelindaOrientationIndex(OrientationIndex))
{
return INDEX_NONE;
}
const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
const TArray<int32>& OrientationPermutation = KnownPermutations[OrientationIndex];
TArray<int32> InversePermutation;
InversePermutation.SetNum(4);
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
{
InversePermutation[OrientationPermutation[AxisIndex]] = AxisIndex;
}
return FindMelindaOrientationIndex(InversePermutation);
}
int32 CountPermutationInversions(const TArray<int32>& Values)
{
int32 Inversions = 0;
@ -211,6 +232,34 @@ namespace HyperTwistCoreLibraryInternal
&& HasExpectedMelindaSizeVector(Definition.SizeVector);
}
bool DoMelindaDefinitionsMatch(
const FHyperTwistPuzzleDefinitionRef& LeftDefinition,
const FHyperTwistPuzzleDefinitionRef& RightDefinition
)
{
return LeftDefinition.PuzzleId == RightDefinition.PuzzleId
&& LeftDefinition.PuzzleFamily == RightDefinition.PuzzleFamily
&& LeftDefinition.Dimension == RightDefinition.Dimension
&& LeftDefinition.DefinitionVersion == RightDefinition.DefinitionVersion
&& LeftDefinition.NotationProfile == RightDefinition.NotationProfile
&& LeftDefinition.SizeVector == RightDefinition.SizeVector
&& LeftDefinition.Variant == RightDefinition.Variant;
}
bool IsSupportedMelindaState(const FHyperTwistPuzzleState& State)
{
return State.IsStructurallyValid()
&& State.StateEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-state-v1")
&& IsSupportedMelindaDefinition(State.Definition);
}
bool IsSupportedMelindaTransformation(const FHyperTwistTransformation& Transformation)
{
return Transformation.IsStructurallyValid()
&& Transformation.TransformEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-transform-v1")
&& IsSupportedMelindaDefinition(Transformation.Definition);
}
int32 GetMelindaHandednessBit(const int32 SignatureId)
{
int32 WorkingValue = SignatureId & 0xF;
@ -311,6 +360,19 @@ namespace HyperTwistCoreLibraryInternal
&& IsZeroOrientation(MelindaState.PieceOrientation);
}
FHyperTwistMelinda2x2x2x2StateEncoding MakeSolvedMelindaStateEncoding()
{
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
MelindaState.PositionToPiece.Reserve(16);
for (int32 PieceId = 0; PieceId < 16; ++PieceId)
{
MelindaState.PositionToPiece.Add(PieceId);
}
MelindaState.PieceOrientation.Init(0, 16);
return MelindaState;
}
FHyperTwistPuzzleState MakeMelindaPuzzleState(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState
@ -327,6 +389,102 @@ namespace HyperTwistCoreLibraryInternal
return State;
}
FHyperTwistTransformation MakeMelindaTransformation(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FHyperTwistMelinda2x2x2x2TransformEncoding& MelindaTransform,
const EHyperTwistTransformKind TransformKind,
const FString& Notation,
const FString& OriginalNotation
)
{
FHyperTwistTransformation Transformation;
Transformation.Definition = Definition;
Transformation.TransformKind = TransformKind;
Transformation.Notation = Notation;
Transformation.TransformEncoding.EncodingProfile = MelindaTransform.EncodingProfile;
Transformation.TransformEncoding.PayloadJson = SerializePayload(MelindaTransform);
Transformation.bInvertible = true;
Transformation.OriginalNotation = OriginalNotation.IsEmpty() ? Notation : OriginalNotation;
return Transformation;
}
FHyperTwistTransformation MakeIdentityMelindaTransformation(const FHyperTwistPuzzleDefinitionRef& Definition)
{
FHyperTwistMelinda2x2x2x2TransformEncoding IdentityTransform;
IdentityTransform.MoveId = TEXT("identity");
IdentityTransform.PositionPullMap.Reserve(16);
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
{
IdentityTransform.PositionPullMap.Add(PositionIndex);
}
IdentityTransform.OrientationDeltaPerNewPosition.Init(0, 16);
return MakeMelindaTransformation(
Definition,
IdentityTransform,
EHyperTwistTransformKind::SingleMove,
TEXT("identity"),
TEXT("identity")
);
}
bool TryDeserializeMelindaState(
const FHyperTwistPuzzleState& State,
FHyperTwistMelinda2x2x2x2StateEncoding& OutMelindaState,
TArray<FString>& OutWarnings
)
{
if (!IsSupportedMelindaState(State))
{
OutWarnings.Add(TEXT("unsupported-melinda-state"));
return false;
}
if (!DeserializePayload(State.StateEncoding, OutMelindaState) || !OutMelindaState.IsStructurallyValid())
{
OutWarnings.Add(TEXT("invalid-melinda-state-payload"));
return false;
}
return true;
}
bool TryDeserializeMelindaTransform(
const FHyperTwistTransformation& Transformation,
FHyperTwistMelinda2x2x2x2TransformEncoding& OutMelindaTransform,
TArray<FString>& OutWarnings
)
{
if (!IsSupportedMelindaTransformation(Transformation))
{
OutWarnings.Add(TEXT("unsupported-melinda-transformation"));
return false;
}
if (!DeserializePayload(Transformation.TransformEncoding, OutMelindaTransform)
|| !OutMelindaTransform.IsStructurallyValid())
{
OutWarnings.Add(TEXT("invalid-melinda-transform-payload"));
return false;
}
return true;
}
FString MakeMelindaInverseLabel(const FString& BaseLabel)
{
return FString::Printf(TEXT("inverse(%s)"), BaseLabel.IsEmpty() ? TEXT("transform") : *BaseLabel);
}
FString MakeMelindaCompositionLabel(const FString& FirstLabel, const FString& SecondLabel)
{
return FString::Printf(
TEXT("compose(%s,%s)"),
FirstLabel.IsEmpty() ? TEXT("first") : *FirstLabel,
SecondLabel.IsEmpty() ? TEXT("second") : *SecondLabel
);
}
bool TryApplyMelindaTransform(
const FHyperTwistPuzzleState& State,
const FHyperTwistTransformation& Transformation,
@ -339,8 +497,7 @@ namespace HyperTwistCoreLibraryInternal
return false;
}
if (State.Definition.PuzzleId != TEXT("hypercube/2x2x2x2")
|| Transformation.Definition.PuzzleId != State.Definition.PuzzleId)
if (!DoMelindaDefinitionsMatch(State.Definition, Transformation.Definition))
{
Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
return true;
@ -722,6 +879,291 @@ FHyperTwistRandomStateGenerationResult UHyperTwistCoreLibrary::GenerateRandomPuz
return Result;
}
FHyperTwistTransformationInversionResult UHyperTwistCoreLibrary::InvertTransformation(
const FHyperTwistTransformation& Transformation
)
{
FHyperTwistTransformationInversionResult Result;
if (!Transformation.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-transformation"));
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding MelindaTransform;
if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
Transformation,
MelindaTransform,
Result.Warnings
))
{
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding InverseTransform = MelindaTransform;
InverseTransform.MoveId = HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(MelindaTransform.MoveId);
InverseTransform.PositionPullMap.Init(0, 16);
InverseTransform.OrientationDeltaPerNewPosition.Init(0, 16);
for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
{
const int32 OldPosition = MelindaTransform.PositionPullMap[NewPosition];
const int32 InverseOrientation =
HyperTwistCoreLibraryInternal::InvertMelindaOrientationIndex(
MelindaTransform.OrientationDeltaPerNewPosition[NewPosition]
);
if (InverseOrientation == INDEX_NONE)
{
Result.Warnings.Add(TEXT("melinda-orientation-inverse-failed"));
return Result;
}
InverseTransform.PositionPullMap[OldPosition] = NewPosition;
InverseTransform.OrientationDeltaPerNewPosition[OldPosition] = InverseOrientation;
}
Result.Transformation = HyperTwistCoreLibraryInternal::MakeMelindaTransformation(
Transformation.Definition,
InverseTransform,
Transformation.TransformKind,
HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(
MelindaTransform.MoveId.IsEmpty() ? Transformation.Notation : MelindaTransform.MoveId
),
HyperTwistCoreLibraryInternal::MakeMelindaInverseLabel(Transformation.Notation)
);
Result.bInverted = true;
Result.bExactInverse = true;
return Result;
}
FHyperTwistTransformationCompositionResult UHyperTwistCoreLibrary::ComposeTransformations(
const FHyperTwistTransformation& FirstTransformation,
const FHyperTwistTransformation& SecondTransformation
)
{
FHyperTwistTransformationCompositionResult Result;
if (!FirstTransformation.IsStructurallyValid() || !SecondTransformation.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-transformation"));
return Result;
}
if (!HyperTwistCoreLibraryInternal::DoMelindaDefinitionsMatch(
FirstTransformation.Definition,
SecondTransformation.Definition
))
{
Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding FirstMelindaTransform;
if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
FirstTransformation,
FirstMelindaTransform,
Result.Warnings
))
{
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding SecondMelindaTransform;
if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaTransform(
SecondTransformation,
SecondMelindaTransform,
Result.Warnings
))
{
return Result;
}
if (FirstMelindaTransform.FrameProfile != SecondMelindaTransform.FrameProfile
|| FirstMelindaTransform.TopologyVersion != SecondMelindaTransform.TopologyVersion)
{
Result.Warnings.Add(TEXT("melinda-frame-or-topology-mismatch"));
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding ComposedTransform;
ComposedTransform.MoveId = HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
FirstMelindaTransform.MoveId,
SecondMelindaTransform.MoveId
);
ComposedTransform.FrameProfile = FirstMelindaTransform.FrameProfile;
ComposedTransform.TopologyVersion = FirstMelindaTransform.TopologyVersion;
ComposedTransform.PositionPullMap.Init(0, 16);
ComposedTransform.OrientationDeltaPerNewPosition.Init(0, 16);
for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
{
const int32 IntermediatePosition = SecondMelindaTransform.PositionPullMap[NewPosition];
ComposedTransform.PositionPullMap[NewPosition] = FirstMelindaTransform.PositionPullMap[IntermediatePosition];
const int32 ComposedOrientation = HyperTwistCoreLibraryInternal::ComposeMelindaOrientationIndices(
SecondMelindaTransform.OrientationDeltaPerNewPosition[NewPosition],
FirstMelindaTransform.OrientationDeltaPerNewPosition[IntermediatePosition]
);
if (ComposedOrientation == INDEX_NONE)
{
Result.Warnings.Add(TEXT("melinda-orientation-composition-failed"));
return Result;
}
ComposedTransform.OrientationDeltaPerNewPosition[NewPosition] = ComposedOrientation;
}
Result.Transformation = HyperTwistCoreLibraryInternal::MakeMelindaTransformation(
FirstTransformation.Definition,
ComposedTransform,
EHyperTwistTransformKind::Composite,
HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
FirstMelindaTransform.MoveId.IsEmpty() ? FirstTransformation.Notation : FirstMelindaTransform.MoveId,
SecondMelindaTransform.MoveId.IsEmpty() ? SecondTransformation.Notation : SecondMelindaTransform.MoveId
),
HyperTwistCoreLibraryInternal::MakeMelindaCompositionLabel(
FirstTransformation.Notation,
SecondTransformation.Notation
)
);
Result.bComposed = true;
Result.bExactComposition = true;
return Result;
}
FHyperTwistMelindaScramblePacketBuildResult UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromGeneratedState(
const FHyperTwistRandomStateGenerationResult& GeneratedStateResult
)
{
FHyperTwistMelindaScramblePacketBuildResult Result;
Result.Warnings.Append(GeneratedStateResult.Warnings);
if (GeneratedStateResult.State.IsStructurallyValid())
{
const bool bHasUsableValidation =
GeneratedStateResult.Validation.bStateSupported
|| GeneratedStateResult.Validation.bStructureValid
|| GeneratedStateResult.Validation.Errors.Num() > 0
|| GeneratedStateResult.Validation.Warnings.Num() > 0;
Result.TargetStateValidation = bHasUsableValidation
? GeneratedStateResult.Validation
: ValidatePuzzleState(GeneratedStateResult.State);
}
if (!GeneratedStateResult.bGenerated)
{
Result.Warnings.Add(TEXT("generated-state-result-missing-solvable-state"));
return Result;
}
FHyperTwistMelinda2x2x2x2StateEncoding GeneratedMelindaState;
if (!HyperTwistCoreLibraryInternal::TryDeserializeMelindaState(
GeneratedStateResult.State,
GeneratedMelindaState,
Result.Warnings
))
{
return Result;
}
if (!Result.TargetStateValidation.bIsSolvable)
{
Result.Warnings.Add(TEXT("generated-state-result-failed-validation"));
return Result;
}
Result.Packet.Definition = GeneratedStateResult.State.Definition;
Result.Packet.PacketSource = EHyperTwistMelindaScramblePacketSource::GeneratedState;
Result.Packet.TargetState = GeneratedStateResult.State;
Result.Packet.RandomSeed = GeneratedStateResult.SeedUsed;
Result.Packet.bCarriesTargetState = true;
Result.Packet.bCarriesExactTransformSequence = false;
Result.bBuilt = Result.Packet.IsStructurallyValid();
if (!Result.bBuilt)
{
Result.Warnings.Add(TEXT("invalid-melinda-scramble-packet"));
}
return Result;
}
FHyperTwistMelindaScramblePacketBuildResult UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromTransformSequence(
const FHyperTwistPuzzleDefinitionRef& Definition,
const TArray<FHyperTwistTransformation>& TransformSequence
)
{
FHyperTwistMelindaScramblePacketBuildResult Result;
if (!HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(Definition))
{
Result.Warnings.Add(TEXT("unsupported-scramble-packet-definition"));
return Result;
}
FHyperTwistPuzzleState CurrentState = HyperTwistCoreLibraryInternal::MakeMelindaPuzzleState(
Definition,
HyperTwistCoreLibraryInternal::MakeSolvedMelindaStateEncoding()
);
FHyperTwistTransformation NetTransform =
HyperTwistCoreLibraryInternal::MakeIdentityMelindaTransformation(Definition);
for (const FHyperTwistTransformation& Transformation : TransformSequence)
{
if (!HyperTwistCoreLibraryInternal::DoMelindaDefinitionsMatch(Definition, Transformation.Definition))
{
Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
return Result;
}
const FHyperTwistApplyTransformationResult ApplyResult = ApplyTransformation(CurrentState, Transformation);
Result.Warnings.Append(ApplyResult.Warnings);
if (!ApplyResult.bApplied || !ApplyResult.bExactStateUpdate)
{
Result.Warnings.Add(TEXT("melinda-transform-sequence-apply-failed"));
return Result;
}
const FHyperTwistTransformationCompositionResult CompositionResult =
ComposeTransformations(NetTransform, Transformation);
Result.Warnings.Append(CompositionResult.Warnings);
if (!CompositionResult.bComposed || !CompositionResult.bExactComposition)
{
Result.Warnings.Add(TEXT("melinda-transform-sequence-compose-failed"));
return Result;
}
CurrentState = ApplyResult.State;
NetTransform = CompositionResult.Transformation;
}
Result.TargetStateValidation = ValidatePuzzleState(CurrentState);
if (!Result.TargetStateValidation.bIsSolvable)
{
Result.Warnings.Add(TEXT("melinda-transform-sequence-target-state-invalid"));
return Result;
}
Result.Packet.Definition = Definition;
Result.Packet.PacketSource = EHyperTwistMelindaScramblePacketSource::TransformSequence;
Result.Packet.TargetState = CurrentState;
Result.Packet.TransformSequence = TransformSequence;
Result.Packet.NetTransform = NetTransform;
Result.Packet.bCarriesTargetState = true;
Result.Packet.bCarriesExactTransformSequence = true;
Result.bBuilt = Result.Packet.IsStructurallyValid();
if (!Result.bBuilt)
{
Result.Warnings.Add(TEXT("invalid-melinda-scramble-packet"));
}
return Result;
}
FHyperTwistApplyTransformationResult UHyperTwistCoreLibrary::ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation)
{
FHyperTwistApplyTransformationResult Result;

View file

@ -107,6 +107,60 @@ struct FHyperTwistRandomStateGenerationResult
bool bGenerated = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistTransformationInversionResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTransformation Transformation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bInverted = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bExactInverse = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistTransformationCompositionResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTransformation Transformation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bComposed = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bExactComposition = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaScramblePacketBuildResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistMelinda2x2x2x2ScramblePacket Packet;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistPuzzleStateValidationResult TargetStateValidation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bBuilt = false;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistCoreLibrary : public UBlueprintFunctionLibrary
{
@ -136,6 +190,26 @@ public:
int32 RandomSeed
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static FHyperTwistTransformationInversionResult InvertTransformation(const FHyperTwistTransformation& Transformation);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static FHyperTwistTransformationCompositionResult ComposeTransformations(
const FHyperTwistTransformation& FirstTransformation,
const FHyperTwistTransformation& SecondTransformation
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Scramble")
static FHyperTwistMelindaScramblePacketBuildResult BuildMelindaScramblePacketFromGeneratedState(
const FHyperTwistRandomStateGenerationResult& GeneratedStateResult
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Scramble")
static FHyperTwistMelindaScramblePacketBuildResult BuildMelindaScramblePacketFromTransformSequence(
const FHyperTwistPuzzleDefinitionRef& Definition,
const TArray<FHyperTwistTransformation>& TransformSequence
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static FHyperTwistApplyTransformationResult ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation);
};

View file

@ -41,6 +41,13 @@ enum class EHyperTwistTransformKind : uint8
Composite UMETA(DisplayName = "Composite")
};
UENUM(BlueprintType)
enum class EHyperTwistMelindaScramblePacketSource : uint8
{
GeneratedState UMETA(DisplayName = "Generated State"),
TransformSequence UMETA(DisplayName = "Transform Sequence")
};
USTRUCT(BlueprintType)
struct FHyperTwistPuzzleDefinitionRef
{
@ -429,3 +436,88 @@ struct FHyperTwistMelinda2x2x2x2TransformEncoding
return true;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistMelinda2x2x2x2ScramblePacket
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PacketProfile = TEXT("melinda-2x2x2x2-scramble-packet-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistPuzzleDefinitionRef Definition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistMelindaScramblePacketSource PacketSource = EHyperTwistMelindaScramblePacketSource::GeneratedState;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistPuzzleState TargetState;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTransformation> TransformSequence;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTransformation NetTransform;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 RandomSeed = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCarriesTargetState = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCarriesExactTransformSequence = false;
bool IsStructurallyValid() const
{
if (PacketProfile.IsEmpty() || !Definition.IsStructurallyValid())
{
return false;
}
if (!bCarriesTargetState && !bCarriesExactTransformSequence)
{
return false;
}
if (bCarriesTargetState)
{
if (!TargetState.IsStructurallyValid()
|| TargetState.Definition.PuzzleId != Definition.PuzzleId
|| TargetState.Definition.PuzzleFamily != Definition.PuzzleFamily
|| TargetState.Definition.Dimension != Definition.Dimension)
{
return false;
}
}
if (bCarriesExactTransformSequence)
{
if (!NetTransform.IsStructurallyValid()
|| NetTransform.Definition.PuzzleId != Definition.PuzzleId
|| NetTransform.Definition.PuzzleFamily != Definition.PuzzleFamily
|| NetTransform.Definition.Dimension != Definition.Dimension)
{
return false;
}
for (const FHyperTwistTransformation& Transformation : TransformSequence)
{
if (!Transformation.IsStructurallyValid()
|| Transformation.Definition.PuzzleId != Definition.PuzzleId
|| Transformation.Definition.PuzzleFamily != Definition.PuzzleFamily
|| Transformation.Definition.Dimension != Definition.Dimension)
{
return false;
}
}
}
else if (TransformSequence.Num() > 0)
{
return false;
}
return true;
}
};

View file

@ -0,0 +1,342 @@
// Copyright HyperTwist, Inc. All Rights Reserved.
#include "Misc/AutomationTest.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "JsonObjectConverter.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistMelindaBound3CoreTestInternal
{
template <typename TStruct>
FString SerializeStructToJson(const TStruct& Value)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
return Json;
}
bool DeserializeTransformEncoding(
const FHyperTwistTransformation& Transformation,
FHyperTwistMelinda2x2x2x2TransformEncoding& OutEncoding
)
{
return FJsonObjectConverter::JsonObjectStringToUStruct(
Transformation.TransformEncoding.PayloadJson,
&OutEncoding,
0,
0
);
}
bool HaveMatchingMelindaStatePayload(
const FHyperTwistPuzzleState& LeftState,
const FHyperTwistPuzzleState& RightState
)
{
return LeftState.Definition.PuzzleId == RightState.Definition.PuzzleId
&& LeftState.StateEncoding.EncodingProfile == RightState.StateEncoding.EncodingProfile
&& LeftState.StateEncoding.PayloadJson == RightState.StateEncoding.PayloadJson
&& LeftState.OrientationFrame.Reference == RightState.OrientationFrame.Reference
&& LeftState.bIsSolved == RightState.bIsSolved;
}
FHyperTwistTransformation MakeMelindaTransformation(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FString& MoveId,
const FString& Notation,
const TArray<int32>& PositionPullMap,
const TArray<int32>& OrientationDeltaPerNewPosition
)
{
FHyperTwistMelinda2x2x2x2TransformEncoding Encoding;
Encoding.MoveId = MoveId;
Encoding.PositionPullMap = PositionPullMap;
Encoding.OrientationDeltaPerNewPosition = OrientationDeltaPerNewPosition;
FHyperTwistTransformation Transformation;
Transformation.Definition = Definition;
Transformation.TransformKind = EHyperTwistTransformKind::SingleMove;
Transformation.Notation = Notation;
Transformation.TransformEncoding.EncodingProfile = Encoding.EncodingProfile;
Transformation.TransformEncoding.PayloadJson = SerializeStructToJson(Encoding);
Transformation.OriginalNotation = Notation;
return Transformation;
}
FHyperTwistTransformation MakeIdentityTransformation(const FHyperTwistPuzzleDefinitionRef& Definition)
{
TArray<int32> PositionPullMap;
PositionPullMap.Reserve(16);
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
{
PositionPullMap.Add(PositionIndex);
}
TArray<int32> OrientationDeltaPerNewPosition;
OrientationDeltaPerNewPosition.Init(0, 16);
return MakeMelindaTransformation(
Definition,
TEXT("identity"),
TEXT("identity"),
PositionPullMap,
OrientationDeltaPerNewPosition
);
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaBound3MoveAlgebraTest,
"HyperTwist.CleanRoom.HactarCE.Bound3.MoveAlgebra",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaBound3MoveAlgebraTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
const FHyperTwistRandomStateGenerationResult GeneratedBaseState =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 2026);
TestTrue(TEXT("Bound 3 move algebra should start from a solvable generated state."), GeneratedBaseState.bGenerated);
const FHyperTwistPuzzleState BaseState = GeneratedBaseState.State;
const FString SerializedBaseState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(BaseState);
const FHyperTwistTransformation TransformA =
HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
Definition,
TEXT("bound3.a"),
TEXT("A"),
{3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{3, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
);
const FHyperTwistTransformation TransformB =
HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
Definition,
TEXT("bound3.b"),
TEXT("B"),
{1, 2, 3, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{4, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
);
const FHyperTwistTransformation IdentityTransformation =
HyperTwistMelindaBound3CoreTestInternal::MakeIdentityTransformation(Definition);
const FHyperTwistTransformationInversionResult InverseA =
UHyperTwistCoreLibrary::InvertTransformation(TransformA);
TestTrue(TEXT("Bound 3 should invert Melinda transforms exactly."), InverseA.bInverted);
TestTrue(TEXT("Bound 3 inverse surface should remain exact."), InverseA.bExactInverse);
const FHyperTwistApplyTransformationResult AppliedA =
UHyperTwistCoreLibrary::ApplyTransformation(BaseState, TransformA);
TestTrue(TEXT("Transform A should apply exactly."), AppliedA.bApplied && AppliedA.bExactStateUpdate);
const FHyperTwistApplyTransformationResult RoundTripA =
UHyperTwistCoreLibrary::ApplyTransformation(AppliedA.State, InverseA.Transformation);
TestTrue(TEXT("Applying a transform then its inverse should apply exactly."), RoundTripA.bApplied && RoundTripA.bExactStateUpdate);
TestEqual(
TEXT("Applying a transform then its inverse should return the original state."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(RoundTripA.State),
SerializedBaseState
);
const FHyperTwistApplyTransformationResult SequentialAB =
UHyperTwistCoreLibrary::ApplyTransformation(AppliedA.State, TransformB);
TestTrue(TEXT("Sequential Melinda application should remain exact."), SequentialAB.bApplied && SequentialAB.bExactStateUpdate);
const FHyperTwistTransformationCompositionResult ComposedAB =
UHyperTwistCoreLibrary::ComposeTransformations(TransformA, TransformB);
TestTrue(TEXT("Bound 3 should compose Melinda transforms exactly."), ComposedAB.bComposed);
TestTrue(TEXT("Bound 3 composition surface should remain exact."), ComposedAB.bExactComposition);
const FHyperTwistApplyTransformationResult AppliedComposedAB =
UHyperTwistCoreLibrary::ApplyTransformation(BaseState, ComposedAB.Transformation);
TestTrue(TEXT("Applying a composed Melinda transform should remain exact."), AppliedComposedAB.bApplied && AppliedComposedAB.bExactStateUpdate);
TestEqual(
TEXT("Applying Compose(A, B) once should match sequential application of A then B."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedComposedAB.State),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(SequentialAB.State)
);
const FHyperTwistTransformationCompositionResult IdentityThenA =
UHyperTwistCoreLibrary::ComposeTransformations(IdentityTransformation, TransformA);
TestTrue(TEXT("Compose(identity, A) should succeed."), IdentityThenA.bComposed && IdentityThenA.bExactComposition);
const FHyperTwistApplyTransformationResult AppliedIdentityThenA =
UHyperTwistCoreLibrary::ApplyTransformation(BaseState, IdentityThenA.Transformation);
TestEqual(
TEXT("Compose(identity, A) should preserve exact application behavior."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedIdentityThenA.State),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedA.State)
);
const FHyperTwistTransformationCompositionResult AThenIdentity =
UHyperTwistCoreLibrary::ComposeTransformations(TransformA, IdentityTransformation);
TestTrue(TEXT("Compose(A, identity) should succeed."), AThenIdentity.bComposed && AThenIdentity.bExactComposition);
const FHyperTwistApplyTransformationResult AppliedAThenIdentity =
UHyperTwistCoreLibrary::ApplyTransformation(BaseState, AThenIdentity.Transformation);
TestEqual(
TEXT("Compose(A, identity) should preserve exact application behavior."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedAThenIdentity.State),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedA.State)
);
const FHyperTwistTransformationInversionResult InverseB =
UHyperTwistCoreLibrary::InvertTransformation(TransformB);
TestTrue(TEXT("Transform B should also invert exactly."), InverseB.bInverted && InverseB.bExactInverse);
const FHyperTwistTransformationInversionResult InverseComposedAB =
UHyperTwistCoreLibrary::InvertTransformation(ComposedAB.Transformation);
TestTrue(TEXT("Inverse(Compose(A, B)) should succeed."), InverseComposedAB.bInverted && InverseComposedAB.bExactInverse);
const FHyperTwistTransformationCompositionResult ComposeInverseBInverseA =
UHyperTwistCoreLibrary::ComposeTransformations(InverseB.Transformation, InverseA.Transformation);
TestTrue(
TEXT("Compose(Inverse(B), Inverse(A)) should succeed."),
ComposeInverseBInverseA.bComposed && ComposeInverseBInverseA.bExactComposition
);
FHyperTwistMelinda2x2x2x2TransformEncoding InverseComposedEncoding;
FHyperTwistMelinda2x2x2x2TransformEncoding ReverseOrderInverseEncoding;
TestTrue(
TEXT("Inverse(Compose(A, B)) payload should deserialize."),
HyperTwistMelindaBound3CoreTestInternal::DeserializeTransformEncoding(
InverseComposedAB.Transformation,
InverseComposedEncoding
)
);
TestTrue(
TEXT("Compose(Inverse(B), Inverse(A)) payload should deserialize."),
HyperTwistMelindaBound3CoreTestInternal::DeserializeTransformEncoding(
ComposeInverseBInverseA.Transformation,
ReverseOrderInverseEncoding
)
);
TestTrue(
TEXT("Inverse-of-compose should preserve the exact position pull map."),
InverseComposedEncoding.PositionPullMap == ReverseOrderInverseEncoding.PositionPullMap
);
TestTrue(
TEXT("Inverse-of-compose should preserve the exact orientation deltas."),
InverseComposedEncoding.OrientationDeltaPerNewPosition
== ReverseOrderInverseEncoding.OrientationDeltaPerNewPosition
);
const FHyperTwistApplyTransformationResult InverseComposeRoundTrip =
UHyperTwistCoreLibrary::ApplyTransformation(SequentialAB.State, InverseComposedAB.Transformation);
const FHyperTwistApplyTransformationResult ReverseOrderRoundTrip =
UHyperTwistCoreLibrary::ApplyTransformation(SequentialAB.State, ComposeInverseBInverseA.Transformation);
TestEqual(
TEXT("Inverse(Compose(A, B)) should return the original state."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(InverseComposeRoundTrip.State),
SerializedBaseState
);
TestEqual(
TEXT("Compose(Inverse(B), Inverse(A)) should return the original state."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(ReverseOrderRoundTrip.State),
SerializedBaseState
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaBound3ScramblePacketTest,
"HyperTwist.CleanRoom.HactarCE.Bound3.ScramblePacket",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaBound3ScramblePacketTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
const FHyperTwistRandomStateGenerationResult GeneratedStateResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 77);
TestTrue(TEXT("Generated-state scramble packets should start from a solvable state."), GeneratedStateResult.bGenerated);
const FHyperTwistMelindaScramblePacketBuildResult GeneratedPacketResult =
UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromGeneratedState(GeneratedStateResult);
TestTrue(TEXT("Bound 3 should build a generated-state scramble packet."), GeneratedPacketResult.bBuilt);
TestTrue(TEXT("Generated-state scramble packets should be structurally valid."), GeneratedPacketResult.Packet.IsStructurallyValid());
TestTrue(
TEXT("Generated-state scramble packets should preserve solvable target-state validation."),
GeneratedPacketResult.TargetStateValidation.bIsSolvable
);
TestTrue(
TEXT("Generated-state scramble packets should stay marked as generated-state packets."),
GeneratedPacketResult.Packet.PacketSource == EHyperTwistMelindaScramblePacketSource::GeneratedState
);
TestTrue(TEXT("Generated-state scramble packets should carry a target state."), GeneratedPacketResult.Packet.bCarriesTargetState);
TestFalse(
TEXT("Generated-state scramble packets should not pretend to carry an exact transform sequence."),
GeneratedPacketResult.Packet.bCarriesExactTransformSequence
);
TestEqual(
TEXT("Generated-state scramble packets should preserve the generated target state exactly."),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedPacketResult.Packet.TargetState),
UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedStateResult.State)
);
TestEqual(
TEXT("Generated-state scramble packets should not include a transform sequence."),
GeneratedPacketResult.Packet.TransformSequence.Num(),
0
);
const FHyperTwistTransformation TransformA =
HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
Definition,
TEXT("bound3.packet.a"),
TEXT("PA"),
{3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{3, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
);
const FHyperTwistTransformationInversionResult InverseA =
UHyperTwistCoreLibrary::InvertTransformation(TransformA);
TestTrue(TEXT("Sequence-backed scramble packets should be able to use exact inverses."), InverseA.bInverted && InverseA.bExactInverse);
TArray<FHyperTwistTransformation> TransformSequence;
TransformSequence.Add(TransformA);
TransformSequence.Add(InverseA.Transformation);
const FHyperTwistMelindaScramblePacketBuildResult SequencePacketResult =
UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromTransformSequence(Definition, TransformSequence);
TestTrue(TEXT("Bound 3 should build a transform-sequence scramble packet."), SequencePacketResult.bBuilt);
TestTrue(TEXT("Transform-sequence scramble packets should be structurally valid."), SequencePacketResult.Packet.IsStructurallyValid());
TestTrue(
TEXT("Transform-sequence scramble packets should remain solvable at their target state."),
SequencePacketResult.TargetStateValidation.bIsSolvable
);
TestTrue(
TEXT("Transform-sequence scramble packets should stay marked as transform-sequence packets."),
SequencePacketResult.Packet.PacketSource == EHyperTwistMelindaScramblePacketSource::TransformSequence
);
TestTrue(
TEXT("Transform-sequence scramble packets should carry an exact transform sequence."),
SequencePacketResult.Packet.bCarriesExactTransformSequence
);
TestEqual(
TEXT("Transform-sequence scramble packets should preserve the exact sequence length."),
SequencePacketResult.Packet.TransformSequence.Num(),
2
);
const FHyperTwistPuzzleState SolvedState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
const FHyperTwistApplyTransformationResult AppliedNetTransform =
UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, SequencePacketResult.Packet.NetTransform);
TestTrue(TEXT("Packet net transform should apply exactly from solved state."), AppliedNetTransform.bApplied && AppliedNetTransform.bExactStateUpdate);
TestTrue(
TEXT("Applying the packet net transform should reproduce the packet target state."),
HyperTwistMelindaBound3CoreTestInternal::HaveMatchingMelindaStatePayload(
AppliedNetTransform.State,
SequencePacketResult.Packet.TargetState
)
);
TestTrue(
TEXT("A transform and its inverse should collapse the packet target state back to solved."),
HyperTwistMelindaBound3CoreTestInternal::HaveMatchingMelindaStatePayload(
SequencePacketResult.Packet.TargetState,
SolvedState
)
);
return true;
}
#endif // WITH_AUTOMATION_TESTS