Implement Melinda bound 1 core state encoding
This commit is contained in:
parent
23abc68383
commit
80265e838b
4 changed files with 392 additions and 6 deletions
|
|
@ -172,18 +172,16 @@ FHyperTwistPuzzleState UHyperTwistContractLibrary::MakeSampleClassicRecognitionP
|
|||
|
||||
FHyperTwistPuzzleState UHyperTwistContractLibrary::MakeSampleHyperPuzzleState()
|
||||
{
|
||||
FHyperTwistHypercubePieceMapState HyperState;
|
||||
HyperState.Corners = {TEXT("c0"), TEXT("c1"), TEXT("c2"), TEXT("c3")};
|
||||
HyperState.Edges = {TEXT("e0"), TEXT("e1"), TEXT("e2"), TEXT("e3")};
|
||||
HyperState.Faces = {TEXT("f0"), TEXT("f1"), TEXT("f2"), TEXT("f3")};
|
||||
HyperState.Cells = {TEXT("cell0"), TEXT("cell1"), TEXT("cell2"), TEXT("cell3")};
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding HyperState;
|
||||
HyperState.PositionToPiece = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
|
||||
HyperState.PieceOrientation = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
FHyperTwistPuzzleState State;
|
||||
State.Definition = MakeSampleHyperPuzzleDefinition();
|
||||
State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
|
||||
State.StateEncoding.EncodingProfile = HyperState.EncodingProfile;
|
||||
State.StateEncoding.PayloadJson = HyperTwistContractLibraryInternal::SerializeStructToJson(HyperState);
|
||||
State.OrientationFrame.Reference = TEXT("hypercube-canonical-v1");
|
||||
State.OrientationFrame.Reference = HyperState.FrameProfile;
|
||||
State.bIsSolved = true;
|
||||
State.Source = EHyperTwistStateSource::Runtime;
|
||||
State.CapturedAtUtc = TEXT("2026-04-25T12:00:00Z");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ namespace HyperTwistCoreLibraryInternal
|
|||
return FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0);
|
||||
}
|
||||
|
||||
template <typename TStruct>
|
||||
FString SerializePayload(const TStruct& Value)
|
||||
{
|
||||
FString Json;
|
||||
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
|
||||
return Json;
|
||||
}
|
||||
|
||||
bool IsIdentityPermutation(const TArray<int32>& Values)
|
||||
{
|
||||
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||
|
|
@ -73,6 +81,176 @@ namespace HyperTwistCoreLibraryInternal
|
|||
|
||||
return Labels.Num() > 0;
|
||||
}
|
||||
|
||||
const TArray<TArray<int32>>& GetMelindaOrientationPermutations()
|
||||
{
|
||||
static TArray<TArray<int32>> Permutations;
|
||||
|
||||
if (Permutations.Num() == 0)
|
||||
{
|
||||
for (int32 A = 0; A < 4; ++A)
|
||||
{
|
||||
for (int32 B = 0; B < 4; ++B)
|
||||
{
|
||||
if (B == A)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32 C = 0; C < 4; ++C)
|
||||
{
|
||||
if (C == A || C == B)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32 D = 0; D < 4; ++D)
|
||||
{
|
||||
if (D == A || D == B || D == C)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Permutations.Add({A, B, C, D});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Permutations;
|
||||
}
|
||||
|
||||
bool IsValidMelindaOrientationIndex(const int32 OrientationIndex)
|
||||
{
|
||||
return OrientationIndex >= 0 && OrientationIndex < GetMelindaOrientationPermutations().Num();
|
||||
}
|
||||
|
||||
int32 FindMelindaOrientationIndex(const TArray<int32>& Permutation)
|
||||
{
|
||||
const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
|
||||
for (int32 Index = 0; Index < KnownPermutations.Num(); ++Index)
|
||||
{
|
||||
if (KnownPermutations[Index] == Permutation)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
int32 ComposeMelindaOrientationIndices(const int32 DeltaIndex, const int32 CurrentIndex)
|
||||
{
|
||||
if (!IsValidMelindaOrientationIndex(DeltaIndex) || !IsValidMelindaOrientationIndex(CurrentIndex))
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const TArray<TArray<int32>>& KnownPermutations = GetMelindaOrientationPermutations();
|
||||
const TArray<int32>& DeltaPermutation = KnownPermutations[DeltaIndex];
|
||||
const TArray<int32>& CurrentPermutation = KnownPermutations[CurrentIndex];
|
||||
|
||||
TArray<int32> ComposedPermutation;
|
||||
ComposedPermutation.SetNum(4);
|
||||
|
||||
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
|
||||
{
|
||||
ComposedPermutation[AxisIndex] = CurrentPermutation[DeltaPermutation[AxisIndex]];
|
||||
}
|
||||
|
||||
return FindMelindaOrientationIndex(ComposedPermutation);
|
||||
}
|
||||
|
||||
bool IsSolvedMelindaState(const FHyperTwistMelinda2x2x2x2StateEncoding& MelindaState)
|
||||
{
|
||||
return MelindaState.IsStructurallyValid()
|
||||
&& IsIdentityPermutation(MelindaState.PositionToPiece)
|
||||
&& IsZeroOrientation(MelindaState.PieceOrientation);
|
||||
}
|
||||
|
||||
bool TryApplyMelindaTransform(
|
||||
const FHyperTwistPuzzleState& State,
|
||||
const FHyperTwistTransformation& Transformation,
|
||||
FHyperTwistApplyTransformationResult& Result
|
||||
)
|
||||
{
|
||||
if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
|
||||
|| Transformation.TransformEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-transform-v1"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (State.Definition.PuzzleId != TEXT("hypercube/2x2x2x2")
|
||||
|| Transformation.Definition.PuzzleId != State.Definition.PuzzleId)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-definition-mismatch"));
|
||||
return true;
|
||||
}
|
||||
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
|
||||
if (!DeserializePayload(State.StateEncoding, MelindaState) || !MelindaState.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-state-payload"));
|
||||
return true;
|
||||
}
|
||||
|
||||
FHyperTwistMelinda2x2x2x2TransformEncoding MelindaTransform;
|
||||
if (!DeserializePayload(Transformation.TransformEncoding, MelindaTransform) || !MelindaTransform.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-transform-payload"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (MelindaState.FrameProfile != MelindaTransform.FrameProfile
|
||||
|| MelindaState.TopologyVersion != MelindaTransform.TopologyVersion)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-frame-or-topology-mismatch"));
|
||||
return true;
|
||||
}
|
||||
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding NextState = MelindaState;
|
||||
TArray<int32> NextPositionToPiece;
|
||||
NextPositionToPiece.SetNum(16);
|
||||
TArray<int32> NextPieceOrientation = MelindaState.PieceOrientation;
|
||||
|
||||
for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
|
||||
{
|
||||
const int32 OldPosition = MelindaTransform.PositionPullMap[NewPosition];
|
||||
const int32 PieceId = MelindaState.PositionToPiece[OldPosition];
|
||||
const int32 DeltaIndex = MelindaTransform.OrientationDeltaPerNewPosition[NewPosition];
|
||||
const int32 CurrentOrientationIndex = MelindaState.PieceOrientation[PieceId];
|
||||
const int32 NextOrientationIndex = ComposeMelindaOrientationIndices(DeltaIndex, CurrentOrientationIndex);
|
||||
|
||||
if (NextOrientationIndex == INDEX_NONE)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-orientation-composition-failed"));
|
||||
return true;
|
||||
}
|
||||
|
||||
NextPositionToPiece[NewPosition] = PieceId;
|
||||
NextPieceOrientation[PieceId] = NextOrientationIndex;
|
||||
}
|
||||
|
||||
NextState.PositionToPiece = MoveTemp(NextPositionToPiece);
|
||||
NextState.PieceOrientation = MoveTemp(NextPieceOrientation);
|
||||
|
||||
if (!NextState.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-next-state"));
|
||||
return true;
|
||||
}
|
||||
|
||||
Result.State.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
|
||||
Result.State.StateEncoding.EncodingProfile = NextState.EncodingProfile;
|
||||
Result.State.StateEncoding.PayloadJson = SerializePayload(NextState);
|
||||
Result.State.OrientationFrame.Reference = NextState.FrameProfile;
|
||||
Result.State.bIsSolved = IsSolvedMelindaState(NextState);
|
||||
Result.State.Source = EHyperTwistStateSource::Runtime;
|
||||
Result.State.Notes = FString();
|
||||
Result.bExactStateUpdate = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FHyperTwistNotationNormalizationResult UHyperTwistCoreLibrary::NormalizeNotation(const FString& RawNotation)
|
||||
|
|
@ -132,6 +310,17 @@ bool UHyperTwistCoreLibrary::IsSolved(const FHyperTwistPuzzleState& State)
|
|||
return false;
|
||||
}
|
||||
|
||||
if (State.StateEncoding.EncodingProfile == TEXT("melinda-2x2x2x2-state-v1"))
|
||||
{
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
|
||||
if (!HyperTwistCoreLibraryInternal::DeserializePayload(State.StateEncoding, MelindaState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return HyperTwistCoreLibraryInternal::IsSolvedMelindaState(MelindaState);
|
||||
}
|
||||
|
||||
if (State.bIsSolved)
|
||||
{
|
||||
return true;
|
||||
|
|
@ -214,6 +403,16 @@ FHyperTwistApplyTransformationResult UHyperTwistCoreLibrary::ApplyTransformation
|
|||
|
||||
Result.bApplied = true;
|
||||
|
||||
if (HyperTwistCoreLibraryInternal::TryApplyMelindaTransform(State, Transformation, Result))
|
||||
{
|
||||
if (!Result.bExactStateUpdate)
|
||||
{
|
||||
Result.bApplied = false;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
if (Normalization.NormalizedNotation.Equals(TEXT("I"), ESearchCase::IgnoreCase)
|
||||
|| Normalization.NormalizedNotation.Equals(TEXT("identity"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -282,6 +282,60 @@ struct FHyperTwistHypercubePieceMapState
|
|||
FString TopologyVersion = TEXT("2x2x2x2/v1");
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelinda2x2x2x2StateEncoding
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EncodingProfile = TEXT("melinda-2x2x2x2-state-v1");
|
||||
|
||||
// For each position in [0, 15], stores the piece currently occupying it.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<int32> PositionToPiece;
|
||||
|
||||
// For each piece in [0, 15], stores an S4 orientation index in [0, 23].
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<int32> PieceOrientation;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString FrameProfile = TEXT("hypercube-canonical-v1");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TopologyVersion = TEXT("2x2x2x2/v1");
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (PositionToPiece.Num() != 16 || PieceOrientation.Num() != 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<bool> SeenPieces;
|
||||
SeenPieces.Init(false, 16);
|
||||
|
||||
for (const int32 PieceId : PositionToPiece)
|
||||
{
|
||||
if (PieceId < 0 || PieceId >= 16 || SeenPieces[PieceId])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SeenPieces[PieceId] = true;
|
||||
}
|
||||
|
||||
for (const int32 OrientationIndex : PieceOrientation)
|
||||
{
|
||||
if (OrientationIndex < 0 || OrientationIndex >= 24)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !FrameProfile.IsEmpty() && !TopologyVersion.IsEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistClassicTransformEncoding
|
||||
{
|
||||
|
|
@ -314,3 +368,64 @@ struct FHyperTwistHypercubeTransformEncoding
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TurnAmount = 1;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelinda2x2x2x2TransformEncoding
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString EncodingProfile = TEXT("melinda-2x2x2x2-transform-v1");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString MoveId = TEXT("identity");
|
||||
|
||||
// For each new position in [0, 15], stores the old position it pulls from.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<int32> PositionPullMap;
|
||||
|
||||
// For each new position in [0, 15], stores an S4 orientation delta index.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
TArray<int32> OrientationDeltaPerNewPosition;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString FrameProfile = TEXT("hypercube-canonical-v1");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TopologyVersion = TEXT("2x2x2x2/v1");
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (MoveId.IsEmpty()
|
||||
|| PositionPullMap.Num() != 16
|
||||
|| OrientationDeltaPerNewPosition.Num() != 16
|
||||
|| FrameProfile.IsEmpty()
|
||||
|| TopologyVersion.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<bool> SeenPositions;
|
||||
SeenPositions.Init(false, 16);
|
||||
|
||||
for (const int32 SourcePosition : PositionPullMap)
|
||||
{
|
||||
if (SourcePosition < 0 || SourcePosition >= 16 || SeenPositions[SourcePosition])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SeenPositions[SourcePosition] = true;
|
||||
}
|
||||
|
||||
for (const int32 OrientationIndex : OrientationDeltaPerNewPosition)
|
||||
{
|
||||
if (OrientationIndex < 0 || OrientationIndex >= 24)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
// 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 HyperTwistMelindaBound1CoreTestInternal
|
||||
{
|
||||
template <typename TStruct>
|
||||
FString SerializeStructToJson(const TStruct& Value)
|
||||
{
|
||||
FString Json;
|
||||
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
|
||||
return Json;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistMelindaBound1CoreTest,
|
||||
"HyperTwist.CleanRoom.HactarCE.Bound1.CoreStateAndTransform",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistMelindaBound1CoreTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FHyperTwistPuzzleState SolvedState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
|
||||
|
||||
TestEqual(TEXT("Bound 1 keeps the Melinda puzzle id."), SolvedState.Definition.PuzzleId, FString(TEXT("hypercube/2x2x2x2")));
|
||||
TestEqual(TEXT("Bound 1 uses the Melinda state profile."), SolvedState.StateEncoding.EncodingProfile, FString(TEXT("melinda-2x2x2x2-state-v1")));
|
||||
TestTrue(TEXT("Sample Melinda state must stay structurally valid."), SolvedState.IsStructurallyValid());
|
||||
TestTrue(TEXT("Sample Melinda state must be structurally solved."), UHyperTwistCoreLibrary::IsSolved(SolvedState));
|
||||
|
||||
const FString SerializedSolvedState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(SolvedState);
|
||||
FHyperTwistPuzzleState RoundTrippedState;
|
||||
TestTrue(TEXT("Sample Melinda state must deserialize after JSON round-trip."), UHyperTwistContractLibrary::DeserializePuzzleStateFromJson(SerializedSolvedState, RoundTrippedState));
|
||||
TestTrue(TEXT("Round-tripped Melinda state must remain solved."), UHyperTwistCoreLibrary::IsSolved(RoundTrippedState));
|
||||
|
||||
FHyperTwistMelinda2x2x2x2TransformEncoding MoveEncoding;
|
||||
MoveEncoding.MoveId = TEXT("bound1.xy-demo");
|
||||
MoveEncoding.PositionPullMap = {3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
|
||||
MoveEncoding.OrientationDeltaPerNewPosition = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
FHyperTwistTransformation Move;
|
||||
Move.Definition = SolvedState.Definition;
|
||||
Move.TransformKind = EHyperTwistTransformKind::SingleMove;
|
||||
Move.Notation = TEXT("XY");
|
||||
Move.TransformEncoding.EncodingProfile = MoveEncoding.EncodingProfile;
|
||||
Move.TransformEncoding.PayloadJson = HyperTwistMelindaBound1CoreTestInternal::SerializeStructToJson(MoveEncoding);
|
||||
Move.OriginalNotation = Move.Notation;
|
||||
|
||||
const FHyperTwistApplyTransformationResult Applied = UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, Move);
|
||||
TestTrue(TEXT("Melinda transform should apply."), Applied.bApplied);
|
||||
TestTrue(TEXT("Melinda transform should produce an exact state update."), Applied.bExactStateUpdate);
|
||||
TestFalse(TEXT("Exact Melinda transforms should not emit the placeholder warning."), Applied.Warnings.Contains(TEXT("exact-piece-update-pending")));
|
||||
TestFalse(TEXT("A non-identity Melinda transform should disturb the solved state."), UHyperTwistCoreLibrary::IsSolved(Applied.State));
|
||||
TestFalse(TEXT("A non-identity Melinda transform should clear bIsSolved."), Applied.State.bIsSolved);
|
||||
TestTrue(TEXT("Exact Melinda transforms should clear placeholder notes."), Applied.State.Notes.IsEmpty());
|
||||
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding AppliedPayload;
|
||||
TestTrue(TEXT("Applied Melinda state payload must deserialize."), FJsonObjectConverter::JsonObjectStringToUStruct(Applied.State.StateEncoding.PayloadJson, &AppliedPayload, 0, 0));
|
||||
TestEqual(TEXT("Move should rotate position 0 from old position 3."), AppliedPayload.PositionToPiece[0], 3);
|
||||
TestEqual(TEXT("Move should rotate position 1 from old position 0."), AppliedPayload.PositionToPiece[1], 0);
|
||||
TestEqual(TEXT("Move should rotate position 2 from old position 1."), AppliedPayload.PositionToPiece[2], 1);
|
||||
TestEqual(TEXT("Move should rotate position 3 from old position 2."), AppliedPayload.PositionToPiece[3], 2);
|
||||
TestTrue(TEXT("Bound 1 keeps identity orientations for the demo transform."), AppliedPayload.PieceOrientation == TArray<int32>({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_AUTOMATION_TESTS
|
||||
Loading…
Add table
Reference in a new issue