diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp index 609e239..e29a37f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistCore/HyperTwistCoreLibrary.cpp @@ -169,6 +169,27 @@ namespace HyperTwistCoreLibraryInternal return FindMelindaOrientationIndex(ComposedPermutation); } + int32 InvertMelindaOrientationIndex(const int32 OrientationIndex) + { + if (!IsValidMelindaOrientationIndex(OrientationIndex)) + { + return INDEX_NONE; + } + + const TArray>& KnownPermutations = GetMelindaOrientationPermutations(); + const TArray& OrientationPermutation = KnownPermutations[OrientationIndex]; + + TArray InversePermutation; + InversePermutation.SetNum(4); + + for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex) + { + InversePermutation[OrientationPermutation[AxisIndex]] = AxisIndex; + } + + return FindMelindaOrientationIndex(InversePermutation); + } + int32 CountPermutationInversions(const TArray& 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& 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& 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& 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; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h index 619e481..452e440 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreLibrary.h @@ -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 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 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 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& TransformSequence + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Core") static FHyperTwistApplyTransformationResult ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreTypes.h index db99658..b53449c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistCore/HyperTwistCoreTypes.h @@ -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 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; + } +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaBound3CoreTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaBound3CoreTest.cpp new file mode 100644 index 0000000..83f0484 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaBound3CoreTest.cpp @@ -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 + 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& PositionPullMap, + const TArray& 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 PositionPullMap; + PositionPullMap.Reserve(16); + for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex) + { + PositionPullMap.Add(PositionIndex); + } + + TArray 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 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