Implement Melinda bound 2 validity and random generation

This commit is contained in:
axiomlogicnexus 2026-05-16 17:39:25 +02:00
parent 80265e838b
commit ba59e59a00
3 changed files with 598 additions and 0 deletions

View file

@ -5,6 +5,13 @@
namespace HyperTwistCoreLibraryInternal
{
struct FMelindaOrientationLookup
{
TArray<int32> ParityByOrientationIndex;
TArray<int32> TwistByOrientationIndex;
TArray<int32> OrientationIndicesByParityAndTwist[2][3];
};
template <typename TStruct>
bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue)
{
@ -162,6 +169,141 @@ namespace HyperTwistCoreLibraryInternal
return FindMelindaOrientationIndex(ComposedPermutation);
}
int32 CountPermutationInversions(const TArray<int32>& Values)
{
int32 Inversions = 0;
for (int32 LeftIndex = 0; LeftIndex < Values.Num(); ++LeftIndex)
{
for (int32 RightIndex = LeftIndex + 1; RightIndex < Values.Num(); ++RightIndex)
{
if (Values[LeftIndex] > Values[RightIndex])
{
++Inversions;
}
}
}
return Inversions;
}
bool IsEvenPermutation(const TArray<int32>& Values)
{
return (CountPermutationInversions(Values) % 2) == 0;
}
bool HasExpectedMelindaSizeVector(const TArray<int32>& SizeVector)
{
return SizeVector.Num() == 0
|| (SizeVector.Num() == 4
&& SizeVector[0] == 2
&& SizeVector[1] == 2
&& SizeVector[2] == 2
&& SizeVector[3] == 2);
}
bool IsSupportedMelindaDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
{
return Definition.IsStructurallyValid()
&& Definition.PuzzleId == TEXT("hypercube/2x2x2x2")
&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube
&& Definition.Dimension == 4
&& HasExpectedMelindaSizeVector(Definition.SizeVector);
}
int32 GetMelindaHandednessBit(const int32 SignatureId)
{
int32 WorkingValue = SignatureId & 0xF;
int32 BitParity = 0;
while (WorkingValue != 0)
{
BitParity ^= (WorkingValue & 1);
WorkingValue >>= 1;
}
return BitParity;
}
int32 NormalizeMelindaTwistBalance(const int32 RawTwistSum)
{
int32 ModuloBalance = RawTwistSum % 3;
if (ModuloBalance < 0)
{
ModuloBalance += 3;
}
switch (ModuloBalance)
{
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
}
int32 MakeMelindaAxisPairKey(int32 AxisA, int32 AxisB)
{
if (AxisA > AxisB)
{
Swap(AxisA, AxisB);
}
return (AxisA * 4) + AxisB;
}
int32 DetermineMelindaTwistValue(const TArray<int32>& Permutation)
{
// Twist class is defined by which solved axis-pairing the orientation induces:
// (01)(23), (02)(13), or (03)(12).
int32 PairKeyA = MakeMelindaAxisPairKey(Permutation[0], Permutation[1]);
int32 PairKeyB = MakeMelindaAxisPairKey(Permutation[2], Permutation[3]);
if (PairKeyA > PairKeyB)
{
Swap(PairKeyA, PairKeyB);
}
if (PairKeyA == MakeMelindaAxisPairKey(0, 1) && PairKeyB == MakeMelindaAxisPairKey(2, 3))
{
return 0;
}
if (PairKeyA == MakeMelindaAxisPairKey(0, 2) && PairKeyB == MakeMelindaAxisPairKey(1, 3))
{
return 1;
}
return -1;
}
const FMelindaOrientationLookup& GetMelindaOrientationLookup()
{
static FMelindaOrientationLookup Lookup;
if (Lookup.ParityByOrientationIndex.Num() == 0)
{
const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
Lookup.ParityByOrientationIndex.SetNum(KnownPermutations.Num());
Lookup.TwistByOrientationIndex.SetNum(KnownPermutations.Num());
TArray<int32> OrientationIndicesByParity[2];
for (int32 OrientationIndex = 0; OrientationIndex < KnownPermutations.Num(); ++OrientationIndex)
{
const int32 OrientationParity = CountPermutationInversions(KnownPermutations[OrientationIndex]) % 2;
const int32 TwistValue = DetermineMelindaTwistValue(KnownPermutations[OrientationIndex]);
Lookup.ParityByOrientationIndex[OrientationIndex] = OrientationParity;
Lookup.TwistByOrientationIndex[OrientationIndex] = TwistValue;
Lookup.OrientationIndicesByParityAndTwist[OrientationParity][TwistValue + 1].Add(OrientationIndex);
}
}
return Lookup;
}
bool IsSolvedMelindaState(const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState)
{
return MelindaState.IsStructurallyValid()
@ -169,6 +311,22 @@ namespace HyperTwistCoreLibraryInternal
&& IsZeroOrientation(MelindaState.PieceOrientation);
}
FHyperTwistPuzzleState MakeMelindaPuzzleState(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState
)
{
FHyperTwistPuzzleState State;
State.Definition = Definition;
State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
State.StateEncoding.EncodingProfile = MelindaState.EncodingProfile;
State.StateEncoding.PayloadJson = SerializePayload(MelindaState);
State.OrientationFrame.Reference = MelindaState.FrameProfile;
State.bIsSolved = IsSolvedMelindaState(MelindaState);
State.Source = EHyperTwistStateSource::Runtime;
return State;
}
bool TryApplyMelindaTransform(
const FHyperTwistPuzzleState& State,
const FHyperTwistTransformation& Transformation,
@ -375,6 +533,195 @@ bool UHyperTwistCoreLibrary::IsSolved(const FHyperTwistPuzzleState& State)
return false;
}
FHyperTwistPuzzleStateValidationResult UHyperTwistCoreLibrary::ValidatePuzzleState(const FHyperTwistPuzzleState& State)
{
FHyperTwistPuzzleStateValidationResult Result;
Result.bStructureValid = State.IsStructurallyValid();
if (!Result.bStructureValid)
{
Result.Errors.Add(TEXT("invalid-puzzle-state-structure"));
return Result;
}
if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
|| !HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(State.Definition))
{
Result.Errors.Add(TEXT("unsupported-melinda-state"));
return Result;
}
Result.bStateSupported = true;
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, MelindaState)
|| !MelindaState.IsStructurallyValid())
{
Result.Errors.Add(TEXT("invalid-melinda-state-payload"));
return Result;
}
if (State.StateEncodingKind != EHyperTwistStateEncodingKind::FamilySpecific)
{
Result.Warnings.Add(TEXT("melinda-state-encoding-kind-mismatch"));
}
if (State.OrientationFrame.Reference != MelindaState.FrameProfile)
{
Result.Warnings.Add(TEXT("melinda-frame-reference-mismatch"));
}
const HyperTwistCoreLibraryInternal::FMelindaOrientationLookup& OrientationLookup =
HyperTwistCoreLibraryInternal::GetMelindaOrientationLookup();
Result.bPermutationParityEven = HyperTwistCoreLibraryInternal::IsEvenPermutation(MelindaState.PositionToPiece);
if (!Result.bPermutationParityEven)
{
Result.Errors.Add(TEXT("melinda-permutation-parity-odd"));
}
int32 RawTwistSum = 0;
for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
{
const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
const int32 OrientationIndex = MelindaState.PieceOrientation[PieceId];
const int32 ExpectedParity =
HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PieceId)
^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PositionIndex);
const int32 OrientationParity = OrientationLookup.ParityByOrientationIndex[OrientationIndex];
if (OrientationParity != ExpectedParity)
{
++Result.OrientationParityMismatchCount;
}
RawTwistSum += OrientationLookup.TwistByOrientationIndex[OrientationIndex];
}
Result.bOrientationParityCompatible = Result.OrientationParityMismatchCount == 0;
if (!Result.bOrientationParityCompatible)
{
Result.Errors.Add(TEXT("melinda-orientation-parity-mismatch"));
}
Result.TwistBalance = HyperTwistCoreLibraryInternal::NormalizeMelindaTwistBalance(RawTwistSum);
Result.bTwistConserved = Result.TwistBalance == 0;
if (!Result.bTwistConserved)
{
Result.Errors.Add(TEXT("melinda-twist-balance-mismatch"));
}
Result.bIsSolvable = Result.bStateSupported
&& Result.bStructureValid
&& Result.bPermutationParityEven
&& Result.bOrientationParityCompatible
&& Result.bTwistConserved;
return Result;
}
FHyperTwistRandomStateGenerationResult UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
const FHyperTwistPuzzleDefinitionRef& Definition,
int32 RandomSeed
)
{
FHyperTwistRandomStateGenerationResult Result;
Result.SeedUsed = RandomSeed;
if (!Definition.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-puzzle-definition"));
return Result;
}
if (!HyperTwistCoreLibraryInternal::IsSupportedMelindaDefinition(Definition))
{
Result.Warnings.Add(TEXT("unsupported-random-state-definition"));
return Result;
}
const HyperTwistCoreLibraryInternal::FMelindaOrientationLookup& OrientationLookup =
HyperTwistCoreLibraryInternal::GetMelindaOrientationLookup();
FRandomStream RandomStream(RandomSeed);
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
MelindaState.PositionToPiece.Reserve(16);
for (int32 PieceId = 0; PieceId < 16; ++PieceId)
{
MelindaState.PositionToPiece.Add(PieceId);
}
for (int32 PositionIndex = MelindaState.PositionToPiece.Num() - 1; PositionIndex > 0; --PositionIndex)
{
const int32 SwapIndex = RandomStream.RandRange(0, PositionIndex);
if (SwapIndex != PositionIndex)
{
Swap(MelindaState.PositionToPiece[PositionIndex], MelindaState.PositionToPiece[SwapIndex]);
}
}
if (!HyperTwistCoreLibraryInternal::IsEvenPermutation(MelindaState.PositionToPiece))
{
Swap(MelindaState.PositionToPiece[14], MelindaState.PositionToPiece[15]);
}
MelindaState.PieceOrientation.Init(0, 16);
int32 RawTwistSum = 0;
const int32 AnchorPosition = 15;
for (int32 PositionIndex = 0; PositionIndex < AnchorPosition; ++PositionIndex)
{
const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
const int32 RequiredParity =
HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PieceId)
^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(PositionIndex);
const int32 TargetTwist = RandomStream.RandRange(-1, 1);
const TArray<int32>& OrientationBucket =
OrientationLookup.OrientationIndicesByParityAndTwist[RequiredParity][TargetTwist + 1];
if (OrientationBucket.Num() == 0)
{
Result.Warnings.Add(TEXT("melinda-orientation-bucket-empty"));
return Result;
}
const int32 ChosenOrientation =
OrientationBucket[RandomStream.RandRange(0, OrientationBucket.Num() - 1)];
MelindaState.PieceOrientation[PieceId] = ChosenOrientation;
RawTwistSum += OrientationLookup.TwistByOrientationIndex[ChosenOrientation];
}
const int32 AnchorPieceId = MelindaState.PositionToPiece[AnchorPosition];
const int32 AnchorRequiredParity =
HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(AnchorPieceId)
^ HyperTwistCoreLibraryInternal::GetMelindaHandednessBit(AnchorPosition);
const int32 AnchorTwist = -HyperTwistCoreLibraryInternal::NormalizeMelindaTwistBalance(RawTwistSum);
const TArray<int32>& AnchorBucket =
OrientationLookup.OrientationIndicesByParityAndTwist[AnchorRequiredParity][AnchorTwist + 1];
if (AnchorBucket.Num() == 0)
{
Result.Warnings.Add(TEXT("melinda-anchor-orientation-bucket-empty"));
return Result;
}
MelindaState.PieceOrientation[AnchorPieceId] =
AnchorBucket[RandomStream.RandRange(0, AnchorBucket.Num() - 1)];
Result.State = HyperTwistCoreLibraryInternal::MakeMelindaPuzzleState(Definition, MelindaState);
Result.AttemptsUsed = 1;
Result.Validation = ValidatePuzzleState(Result.State);
Result.bGenerated = Result.Validation.bIsSolvable;
if (!Result.bGenerated)
{
Result.Warnings.Add(TEXT("generated-melinda-state-failed-validation"));
}
return Result;
}
FHyperTwistApplyTransformationResult UHyperTwistCoreLibrary::ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation)
{
FHyperTwistApplyTransformationResult Result;

View file

@ -47,6 +47,66 @@ struct FHyperTwistApplyTransformationResult
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistPuzzleStateValidationResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Errors;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bStateSupported = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bStructureValid = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bPermutationParityEven = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bOrientationParityCompatible = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bTwistConserved = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bIsSolvable = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TwistBalance = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 OrientationParityMismatchCount = 0;
};
USTRUCT(BlueprintType)
struct FHyperTwistRandomStateGenerationResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistPuzzleState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistPuzzleStateValidationResult Validation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SeedUsed = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AttemptsUsed = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bGenerated = false;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistCoreLibrary : public UBlueprintFunctionLibrary
{
@ -67,6 +127,15 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static bool IsSolved(const FHyperTwistPuzzleState& State);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation")
static FHyperTwistPuzzleStateValidationResult ValidatePuzzleState(const FHyperTwistPuzzleState& State);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static FHyperTwistRandomStateGenerationResult GenerateRandomPuzzleState(
const FHyperTwistPuzzleDefinitionRef& Definition,
int32 RandomSeed
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Core")
static FHyperTwistApplyTransformationResult ApplyTransformation(const FHyperTwistPuzzleState& State, const FHyperTwistTransformation& Transformation);
};

View file

@ -0,0 +1,182 @@
// 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 HyperTwistMelindaBound2CoreTestInternal
{
template <typename TStruct>
FString SerializeStructToJson(const TStruct& Value)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
return Json;
}
FHyperTwistPuzzleState MakeStateWithPayload(
const FHyperTwistPuzzleState& BaseState,
const FHyperTwistMelinda2x2x2x2StateEncoding& Payload
)
{
FHyperTwistPuzzleState State = BaseState;
State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
State.StateEncoding.EncodingProfile = Payload.EncodingProfile;
State.StateEncoding.PayloadJson = SerializeStructToJson(Payload);
State.OrientationFrame.Reference = Payload.FrameProfile;
State.bIsSolved = UHyperTwistCoreLibrary::IsSolved(State);
return State;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaBound2ValiditySurfaceTest,
"HyperTwist.CleanRoom.HactarCE.Bound2.ValiditySurface",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaBound2ValiditySurfaceTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleState SolvedState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
const FHyperTwistPuzzleStateValidationResult SolvedValidation = UHyperTwistCoreLibrary::ValidatePuzzleState(SolvedState);
TestTrue(TEXT("Solved sample state must stay supported."), SolvedValidation.bStateSupported);
TestTrue(TEXT("Solved sample state must stay structurally valid."), SolvedValidation.bStructureValid);
TestTrue(TEXT("Solved sample state must have even permutation parity."), SolvedValidation.bPermutationParityEven);
TestTrue(TEXT("Solved sample state must satisfy handedness/orientation parity compatibility."), SolvedValidation.bOrientationParityCompatible);
TestTrue(TEXT("Solved sample state must conserve twist."), SolvedValidation.bTwistConserved);
TestTrue(TEXT("Solved sample state must be solvable."), SolvedValidation.bIsSolvable);
TestEqual(TEXT("Solved sample state must have zero twist balance."), SolvedValidation.TwistBalance, 0);
FHyperTwistMelinda2x2x2x2StateEncoding SolvedPayload;
TestTrue(
TEXT("Solved sample payload must deserialize."),
FJsonObjectConverter::JsonObjectStringToUStruct(SolvedState.StateEncoding.PayloadJson, &SolvedPayload, 0, 0)
);
FHyperTwistMelinda2x2x2x2StateEncoding OddPermutationPayload = SolvedPayload;
Swap(OddPermutationPayload.PositionToPiece[0], OddPermutationPayload.PositionToPiece[1]);
const FHyperTwistPuzzleStateValidationResult OddPermutationValidation =
UHyperTwistCoreLibrary::ValidatePuzzleState(
HyperTwistMelindaBound2CoreTestInternal::MakeStateWithPayload(SolvedState, OddPermutationPayload)
);
TestFalse(TEXT("A single swap must fail even-permutation validation."), OddPermutationValidation.bPermutationParityEven);
TestTrue(
TEXT("Odd permutation validation must report the parity failure."),
OddPermutationValidation.Errors.Contains(TEXT("melinda-permutation-parity-odd"))
);
TestFalse(TEXT("Odd permutation state must not be solvable."), OddPermutationValidation.bIsSolvable);
FHyperTwistMelinda2x2x2x2StateEncoding OrientationMismatchPayload = SolvedPayload;
OrientationMismatchPayload.PieceOrientation[0] = 1;
const FHyperTwistPuzzleStateValidationResult OrientationMismatchValidation =
UHyperTwistCoreLibrary::ValidatePuzzleState(
HyperTwistMelindaBound2CoreTestInternal::MakeStateWithPayload(SolvedState, OrientationMismatchPayload)
);
TestTrue(TEXT("The mismatch case should keep even permutation parity."), OrientationMismatchValidation.bPermutationParityEven);
TestFalse(
TEXT("The mismatch case must fail handedness/orientation parity compatibility."),
OrientationMismatchValidation.bOrientationParityCompatible
);
TestTrue(
TEXT("The mismatch case must report the handedness/orientation parity failure."),
OrientationMismatchValidation.Errors.Contains(TEXT("melinda-orientation-parity-mismatch"))
);
TestFalse(TEXT("The mismatch case must not be solvable."), OrientationMismatchValidation.bIsSolvable);
FHyperTwistMelinda2x2x2x2StateEncoding TwistMismatchPayload = SolvedPayload;
TwistMismatchPayload.PieceOrientation[0] = 3;
const FHyperTwistPuzzleStateValidationResult TwistMismatchValidation =
UHyperTwistCoreLibrary::ValidatePuzzleState(
HyperTwistMelindaBound2CoreTestInternal::MakeStateWithPayload(SolvedState, TwistMismatchPayload)
);
TestTrue(TEXT("The twist mismatch case should keep even permutation parity."), TwistMismatchValidation.bPermutationParityEven);
TestTrue(
TEXT("The twist mismatch case should keep handedness/orientation parity compatibility."),
TwistMismatchValidation.bOrientationParityCompatible
);
TestFalse(TEXT("The twist mismatch case must fail twist conservation."), TwistMismatchValidation.bTwistConserved);
TestTrue(
TEXT("The twist mismatch case must report twist imbalance."),
TwistMismatchValidation.Errors.Contains(TEXT("melinda-twist-balance-mismatch"))
);
TestEqual(TEXT("The twist mismatch case must report a +1 twist balance."), TwistMismatchValidation.TwistBalance, 1);
TestFalse(TEXT("The twist mismatch case must not be solvable."), TwistMismatchValidation.bIsSolvable);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaBound2RandomStateGenerationTest,
"HyperTwist.CleanRoom.HactarCE.Bound2.RandomStateGeneration",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaBound2RandomStateGenerationTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
const FHyperTwistRandomStateGenerationResult FirstResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 1337);
TestTrue(TEXT("Seeded Melinda generation must succeed."), FirstResult.bGenerated);
TestEqual(TEXT("Bound 2 generation should remain single-pass."), FirstResult.AttemptsUsed, 1);
TestTrue(TEXT("Generated state must remain structurally valid."), FirstResult.State.IsStructurallyValid());
TestEqual(
TEXT("Generated state must keep the Melinda state profile."),
FirstResult.State.StateEncoding.EncodingProfile,
FString(TEXT("melinda-2x2x2x2-state-v1"))
);
TestTrue(TEXT("Generated validation must report solvability."), FirstResult.Validation.bIsSolvable);
TestTrue(TEXT("Generated validation must preserve even permutation parity."), FirstResult.Validation.bPermutationParityEven);
TestTrue(
TEXT("Generated validation must preserve handedness/orientation compatibility."),
FirstResult.Validation.bOrientationParityCompatible
);
TestTrue(TEXT("Generated validation must preserve twist conservation."), FirstResult.Validation.bTwistConserved);
const FHyperTwistPuzzleStateValidationResult Revalidated =
UHyperTwistCoreLibrary::ValidatePuzzleState(FirstResult.State);
TestTrue(TEXT("Generated state must revalidate through the owned validity surface."), Revalidated.bIsSolvable);
const FString FirstSerializedState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(FirstResult.State);
const FHyperTwistRandomStateGenerationResult RepeatedResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 1337);
const FString RepeatedSerializedState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(RepeatedResult.State);
TestTrue(TEXT("Repeated seeded generation must also succeed."), RepeatedResult.bGenerated);
TestEqual(
TEXT("Seeded Melinda generation must be deterministic."),
RepeatedSerializedState,
FirstSerializedState
);
const FHyperTwistRandomStateGenerationResult DifferentSeedResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 1338);
TestTrue(TEXT("A different seed must still generate a solvable state."), DifferentSeedResult.bGenerated);
TestTrue(TEXT("Different-seed state must still validate."), DifferentSeedResult.Validation.bIsSolvable);
for (int32 Seed = 0; Seed < 32; ++Seed)
{
const FHyperTwistRandomStateGenerationResult LoopResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, Seed);
TestTrue(
FString::Printf(TEXT("Seed %d must generate a solvable Melinda state."), Seed),
LoopResult.bGenerated
);
TestTrue(
FString::Printf(TEXT("Seed %d must revalidate through the owned validity surface."), Seed),
LoopResult.Validation.bIsSolvable
);
}
return true;
}
#endif // WITH_AUTOMATION_TESTS