Implement Melinda bound 2 validity and random generation
This commit is contained in:
parent
80265e838b
commit
ba59e59a00
3 changed files with 598 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue