diff --git a/UnrealHyperTwist/Config/DefaultGame.ini b/UnrealHyperTwist/Config/DefaultGame.ini index 1ddef48..099bd95 100644 --- a/UnrealHyperTwist/Config/DefaultGame.ini +++ b/UnrealHyperTwist/Config/DefaultGame.ini @@ -32,3 +32,6 @@ ProfilesPath=/voice/profiles SynthesizePath=/voice/narrate HealthPath=/voice/health RequestTimeoutSeconds=5.0 + +[/Script/DeveloperToolSettings.ProjectPackagingSettings] ++DirectoriesToNeverCook=(Path="/MovieRenderPipeline/Blueprints") diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayPersistenceLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayPersistenceLibrary.cpp index 4b48329..1b9ed15 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayPersistenceLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayPersistenceLibrary.cpp @@ -89,6 +89,125 @@ namespace HyperTwistReplayPersistenceLibraryInternal FMath::Max(Sequence, 1) ); } + + FString BuildFallbackPacketIdentity(const FHyperTwistReplayPacket& ReplayPacket) + { + const FString PuzzleToken = SanitizePathToken( + !ReplayPacket.PuzzleDefinition.PuzzleId.IsEmpty() + ? ReplayPacket.PuzzleDefinition.PuzzleId + : TEXT("replay") + ); + const FString IdentitySeed = !ReplayPacket.StartedAtUtc.IsEmpty() + ? ReplayPacket.StartedAtUtc + : !ReplayPacket.EndedAtUtc.IsEmpty() + ? ReplayPacket.EndedAtUtc + : FString::Printf(TEXT("event-count-%d"), ReplayPacket.Events.Num()); + return FString::Printf( + TEXT("%s_%s"), + *PuzzleToken, + *SanitizePathToken(IdentitySeed) + ); + } + + bool ShouldSortEventsBySequence(const TArray& Events) + { + if (Events.Num() < 2) + { + return false; + } + + for (const FHyperTwistReplayEvent& ReplayEvent : Events) + { + if (ReplayEvent.Sequence <= 0) + { + return false; + } + } + + return true; + } + + void NormalizeEventOrder(TArray& Events) + { + if (!ShouldSortEventsBySequence(Events)) + { + return; + } + + struct FIndexedReplayEvent + { + FHyperTwistReplayEvent Event; + int32 OriginalIndex = 0; + }; + + TArray IndexedEvents; + IndexedEvents.Reserve(Events.Num()); + for (int32 EventIndex = 0; EventIndex < Events.Num(); ++EventIndex) + { + FIndexedReplayEvent& IndexedEvent = IndexedEvents.AddDefaulted_GetRef(); + IndexedEvent.Event = Events[EventIndex]; + IndexedEvent.OriginalIndex = EventIndex; + } + + IndexedEvents.Sort([](const FIndexedReplayEvent& Left, const FIndexedReplayEvent& Right) + { + if (Left.Event.Sequence != Right.Event.Sequence) + { + return Left.Event.Sequence < Right.Event.Sequence; + } + + if (Left.Event.TimeMs != Right.Event.TimeMs) + { + return Left.Event.TimeMs < Right.Event.TimeMs; + } + + return Left.OriginalIndex < Right.OriginalIndex; + }); + + Events.Reset(); + Events.Reserve(IndexedEvents.Num()); + for (FIndexedReplayEvent& IndexedEvent : IndexedEvents) + { + Events.Add(MoveTemp(IndexedEvent.Event)); + } + } + + FString BuildUniqueEventId( + const FHyperTwistReplayEvent& ReplayEvent, + const int32 Sequence, + TSet& UsedEventIds + ) + { + const FString PreferredEventId = !ReplayEvent.EventId.IsEmpty() + ? ReplayEvent.EventId + : BuildFallbackEventId(ReplayEvent, Sequence); + if (!UsedEventIds.Contains(PreferredEventId)) + { + UsedEventIds.Add(PreferredEventId); + return PreferredEventId; + } + + const FString BaseEventId = SanitizePathToken(PreferredEventId); + FString CandidateEventId = FString::Printf( + TEXT("%s_%03d"), + *BaseEventId, + FMath::Max(Sequence, 1) + ); + int32 CollisionIndex = 1; + while (UsedEventIds.Contains(CandidateEventId)) + { + CandidateEventId = FString::Printf( + TEXT("%s_%03d_%02d"), + *BaseEventId, + FMath::Max(Sequence, 1), + CollisionIndex + ); + ++CollisionIndex; + } + + UsedEventIds.Add(CandidateEventId); + return CandidateEventId; + } } FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory() @@ -117,10 +236,24 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack NormalizedPacket.SessionId = NormalizedPacket.ReplayId; } + if (NormalizedPacket.ReplayId.IsEmpty() && NormalizedPacket.SessionId.IsEmpty()) + { + const FString FallbackIdentity = + HyperTwistReplayPersistenceLibraryInternal::BuildFallbackPacketIdentity( + NormalizedPacket + ); + NormalizedPacket.ReplayId = FallbackIdentity; + NormalizedPacket.SessionId = FallbackIdentity; + } + + HyperTwistReplayPersistenceLibraryInternal::NormalizeEventOrder(NormalizedPacket.Events); + int32 PreviousSequence = 0; int32 PreviousTimeMs = 0; + TSet UsedEventIds; for (FHyperTwistReplayEvent& ReplayEvent : NormalizedPacket.Events) { + ReplayEvent.TimeMs = FMath::Max(ReplayEvent.TimeMs, 0); if (ReplayEvent.Sequence <= PreviousSequence) { ReplayEvent.Sequence = PreviousSequence + 1; @@ -131,19 +264,12 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack ReplayEvent.TimeMs = PreviousTimeMs; } - if (ReplayEvent.TimeMs < 0) - { - ReplayEvent.TimeMs = 0; - } - - if (ReplayEvent.EventId.IsEmpty()) - { - ReplayEvent.EventId = - HyperTwistReplayPersistenceLibraryInternal::BuildFallbackEventId( - ReplayEvent, - ReplayEvent.Sequence - ); - } + ReplayEvent.EventId = + HyperTwistReplayPersistenceLibraryInternal::BuildUniqueEventId( + ReplayEvent, + ReplayEvent.Sequence, + UsedEventIds + ); PreviousSequence = ReplayEvent.Sequence; PreviousTimeMs = ReplayEvent.TimeMs; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp index ea281b5..3d49253 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp @@ -8,6 +8,8 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal { const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1"); + const TCHAR* DefaultPuzzleId = TEXT("cube/3x3x3"); + const TCHAR* FallbackRecordedAtUtc = TEXT("9999-12-31T23:59:59Z"); template bool SerializeStruct(const TStruct& Value, FString& OutJson) @@ -42,6 +44,25 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder); } + FHyperTwistClassicCubeLeaderboardEntry NormalizeEntry( + const FHyperTwistClassicCubeLeaderboardEntry& Entry + ) + { + FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = Entry; + if (NormalizedEntry.PuzzleId.IsEmpty()) + { + NormalizedEntry.PuzzleId = DefaultPuzzleId; + } + + NormalizedEntry.ScrambleLength = FMath::Max(NormalizedEntry.ScrambleLength, 0); + if (NormalizedEntry.RecordedAtUtc.IsEmpty()) + { + NormalizedEntry.RecordedAtUtc = FallbackRecordedAtUtc; + } + + return NormalizedEntry; + } + bool ShouldPreferEntry( const FHyperTwistClassicCubeLeaderboardEntry& Candidate, const FHyperTwistClassicCubeLeaderboardEntry& Existing @@ -164,27 +185,29 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary: TMap BestEntryByKey; for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries) { - if (!Entry.IsStructurallyValid()) + const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = + HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry); + if (!NormalizedEntry.IsStructurallyValid()) { continue; } const FString Key = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey( - Entry.PuzzleId, - Entry.ScrambleLength + NormalizedEntry.PuzzleId, + NormalizedEntry.ScrambleLength ); if (FHyperTwistClassicCubeLeaderboardEntry* ExistingEntry = BestEntryByKey.Find(Key)) { if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry( - Entry, + NormalizedEntry, *ExistingEntry)) { - *ExistingEntry = Entry; + *ExistingEntry = NormalizedEntry; } } else { - BestEntryByKey.Add(Key, Entry); + BestEntryByKey.Add(Key, NormalizedEntry); } } @@ -267,14 +290,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary: UpdatedState = FHyperTwistClassicCubeLeaderboardState(); UpdatedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion; } - if (!Entry.IsStructurallyValid()) + const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = + HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry); + if (!NormalizedEntry.IsStructurallyValid()) { return UpdatedState; } const FString TargetKey = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey( - Entry.PuzzleId, - Entry.ScrambleLength + NormalizedEntry.PuzzleId, + NormalizedEntry.ScrambleLength ); const int32 ExistingIndex = UpdatedState.Entries.IndexOfByPredicate( [&TargetKey](const FHyperTwistClassicCubeLeaderboardEntry& Candidate) @@ -288,16 +313,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary: if (ExistingIndex == INDEX_NONE) { - UpdatedState.Entries.Add(Entry); + UpdatedState.Entries.Add(NormalizedEntry); } else { const FHyperTwistClassicCubeLeaderboardEntry& ExistingEntry = UpdatedState.Entries[ExistingIndex]; if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry( - Entry, + NormalizedEntry, ExistingEntry)) { - UpdatedState.Entries[ExistingIndex] = Entry; + UpdatedState.Entries[ExistingIndex] = NormalizedEntry; } } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp index 05fae78..62fb69d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSolverLibrary.cpp @@ -30,7 +30,15 @@ namespace int32 DetermineSolverThreadCount() { const int32 AvailablePhysicalCores = FMath::Max(1, FPlatformMisc::NumberOfCores()); - return FMath::Clamp(AvailablePhysicalCores / 2, 1, 4); + return FMath::Clamp(AvailablePhysicalCores - 2, 1, 8); + } + + int32 DetermineSolverSplitCount(const int32 ThreadCount) + { + // Single-split search leaves short classic-cube solves badly under-distributed on + // multicore hosts. Split phase-1 roots more aggressively so guidance/search work + // can saturate the available worker pool. + return FMath::Clamp(ThreadCount * 3, 1, move::COUNT1); } bool TryResolveMoveIndex(const FString& MoveString, int32& OutMoveIndex) @@ -114,7 +122,9 @@ TArray UHyperTwistSolverLibrary::SolveClassicState( return Result; } - solve::Engine Solver(DetermineSolverThreadCount(), TimeLimitMs, NumSolutions, MaxLength, 1); + const int32 ThreadCount = DetermineSolverThreadCount(); + const int32 SplitCount = DetermineSolverSplitCount(ThreadCount); + solve::Engine Solver(ThreadCount, TimeLimitMs, NumSolutions, MaxLength, SplitCount); Solver.prepare(); std::vector> Solutions; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingHyperspeedcubeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingHyperspeedcubeLibrary.cpp index d1c3432..db7995c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingHyperspeedcubeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingHyperspeedcubeLibrary.cpp @@ -576,18 +576,65 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun ) ), MakePackageReference( - TEXT("package/hyperspeedcube-hps-ft-cube-example"), - TEXT("hps/puzzles/ft_cube.hps"), - TEXT("./hps/puzzles/ft_cube.hps"), - TEXT("hyper-dsl-builtin-authoring-example"), + TEXT("package/hyperspeedcube-hps-symmetric-registration"), + TEXT("crates/hyperpuzzle/src/lib.rs"), + TEXT("./crates/hyperpuzzle/src/lib.rs"), + TEXT("hyper-dsl-symmetric-engine-registration-support"), TEXT("direct dual-license donor"), - TEXT("Retained built-in authoring example for facet-turning cube generators, parameter lists, puzzle tags, example metadata, twist-system declarations, and vantage declarations beneath the bounded HyperTwist DSL lane."), + TEXT("Retained symmetric HPS registration seam for wiring the general symmetric puzzle backend into the bounded HyperTwist DSL lane without transferring the broader donor runtime anchor."), { - TEXT("add_puzzle_generator example"), - TEXT("parameterized generator example"), - TEXT("example alias tags"), - TEXT("twist-system declaration example"), - TEXT("vantage-set declaration example") + TEXT("symmetric puzzle engine registration"), + TEXT("symmetric twist-system engine registration"), + TEXT("symmetric builtins define_in"), + TEXT("symmetric catalog add_puzzles") + }, + CommonNoticeActions, + { + TEXT("whole puzzle engine ownership"), + TEXT("whole runtime-anchor ownership"), + TEXT("viewer or editor shell ownership") + }, + MakeAttribution( + TEXT("hyperspeedcube/dsl-symmetric-registration"), + TEXT("Preserves the retained symmetric HPS registration seam without promoting the donor runtime anchor, puzzle engine, or viewer shell into first-party ownership.") + ) + ), + MakePackageReference( + TEXT("package/hyperspeedcube-hps-symmetric-engine"), + TEXT("crates/hyperpuzzle_impl_symmetric/src/lib.rs"), + TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs"), + TEXT("hyper-dsl-symmetric-engine-support"), + TEXT("direct dual-license donor"), + TEXT("Retained general symmetric puzzle backend seam for HPS-defined symmetric puzzle families, orbit-aware naming, and catalog population beneath the bounded HyperTwist DSL lane."), + { + TEXT("general symmetric puzzle backend"), + TEXT("symmetric add_puzzles_to_catalog"), + TEXT("symmetric orbit autonaming"), + TEXT("symmetric family puzzle build") + }, + CommonNoticeActions, + { + TEXT("whole runtime-anchor ownership"), + TEXT("whole puzzle corpus ownership"), + TEXT("donor shell ownership") + }, + MakeAttribution( + TEXT("hyperspeedcube/dsl-symmetric-engine"), + TEXT("Preserves the retained symmetric puzzle backend seam as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.") + ) + ), + MakePackageReference( + TEXT("package/hyperspeedcube-hps-symmetric-platonic-example"), + TEXT("hps/symmetric/4d_platonic.hps"), + TEXT("./hps/symmetric/4d_platonic.hps"), + TEXT("hyper-dsl-symmetric-authoring-example"), + TEXT("direct dual-license donor"), + TEXT("Retained symmetric HPS authoring example for built-in shape-family imports, symmetric engine declarations, and higher-dimensional puzzle-family catalog composition beneath the bounded HyperTwist DSL lane."), + { + TEXT("symmetric shape-family example"), + TEXT("4D platonic family example"), + TEXT("symmetric engine declaration example"), + TEXT("built-in shape import example") }, CommonNoticeActions, { @@ -596,8 +643,8 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun TEXT("donor shell ownership") }, MakeAttribution( - TEXT("hyperspeedcube/dsl-ft-cube-example"), - TEXT("Preserves the retained FT-cube built-in authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime, or donor shell ownership.") + TEXT("hyperspeedcube/dsl-symmetric-platonic-example"), + TEXT("Preserves the retained symmetric HPS authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.") ) ) }; @@ -875,7 +922,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun TEXT("package/hyperspeedcube-hps-engines"), TEXT("package/hyperspeedcube-hps-catalog-builtins"), TEXT("package/hyperspeedcube-hps-codegen"), - TEXT("package/hyperspeedcube-hps-ft-cube-example") + TEXT("package/hyperspeedcube-hps-symmetric-registration"), + TEXT("package/hyperspeedcube-hps-symmetric-engine"), + TEXT("package/hyperspeedcube-hps-symmetric-platonic-example") }; DslContract.SyntaxSurfaceTags = { @@ -897,7 +946,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun TEXT("tag inheritance on generated puzzles"), TEXT("engine kwarg routing"), TEXT("orbit HPS code emission"), - TEXT("add_puzzle_generator example") + TEXT("add_puzzle_generator example"), + TEXT("symmetric shape-family example"), + TEXT("symmetric engine registration") }; DslContract.DiagnosticSurfaceTags = { @@ -909,7 +960,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun DslContract.ProvenanceCaveats = { TEXT("the retained Hyperpuzzlescript DSL stays bounded to authoring and diagnostics rather than transferring whole runtime-anchor ownership"), - TEXT("the retained FT-cube file is evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"), + TEXT("the retained symmetric HPS example and backend registration are evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"), TEXT("notation, replay verification, stats-shape, and broader hyper-runtime ownership remain separately packetized even when the same donor row supplies adjacent evidence") }; DslContract.ExplicitExclusions = @@ -937,6 +988,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun TEXT("package/hyperspeedcube-hps-modules"), TEXT("package/hyperspeedcube-hps-runtime-core"), TEXT("package/hyperspeedcube-hps-engines"), + TEXT("package/hyperspeedcube-hps-symmetric-registration"), TEXT("package/hyperspeedcube-hps-request-bridge") }; DslBoundary.ModuleSurfaceTags = @@ -954,15 +1006,19 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun TEXT("builtins scope injection"), TEXT("puzzle engine registry"), TEXT("twist-system engine registry"), + TEXT("symmetric puzzle engine registration"), + TEXT("symmetric twist-system engine registration"), TEXT("eval request channel"), TEXT("blocking HPS-thread eval"), TEXT("non-reentrant eval rule"), TEXT("parse_all preload"), - TEXT("exec_all_files preload") + TEXT("exec_all_files preload"), + TEXT("symmetric catalog add_puzzles") }; DslBoundary.ProvenanceCaveats = { TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party"), + TEXT("the current upstream HPS lane registers both nd-euclid and symmetric engines without transferring whole runtime ownership"), TEXT("engine callbacks defer object construction until build time and do not transfer whole runtime ownership"), TEXT("blocking eval requests are a bounded HPS-thread bridge rather than HyperTwist's generic job system"), TEXT("built-in HPS bundles remain bounded authoring support rather than first-party shell content") diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeBehaviorTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeBehaviorTest.cpp index 964e6b4..ba651f9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeBehaviorTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeBehaviorTest.cpp @@ -12,9 +12,6 @@ #include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h" #include "HyperTwistSolverLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" -#include "ThirdParty/rob-twophase/cubie.h" -#include "ThirdParty/rob-twophase/face.h" -#include "ThirdParty/rob-twophase/move.h" #if WITH_AUTOMATION_TESTS @@ -84,49 +81,26 @@ namespace HyperTwistClassicCubeBehaviorTestInternal return Result; } - bool TryResolveSolverMoveIndex(const FString& MoveNotation, int32& OutMoveIndex) + TArray BuildInverseMoveSequence(const TArray& MoveSequence) { - const FString TrimmedMove = MoveNotation.TrimStartAndEnd(); - for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex) + TArray Result; + Result.Reserve(MoveSequence.Num()); + + for (int32 MoveIndex = MoveSequence.Num() - 1; MoveIndex >= 0; --MoveIndex) { - if (TrimmedMove.Equals(UTF8_TO_TCHAR(move::names[MoveIndex].c_str()), ESearchCase::IgnoreCase)) + FHyperTwistClassicCubeMoveDescriptor ParsedMove; + if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation( + MoveSequence[MoveIndex], + ParsedMove)) { - OutMoveIndex = MoveIndex; - return true; - } - } - - return false; - } - - bool TryBuildSolverSpaceFaceletString( - const TArray& ScrambleMoves, - FString& OutFaceletString - ) - { - OutFaceletString.Reset(); - if (!UHyperTwistSolverLibrary::InitializeSolver()) - { - return false; - } - - cubie::cube Cube = cubie::SOLVED_CUBE; - for (const FString& MoveNotation : ScrambleMoves) - { - int32 MoveIndex = INDEX_NONE; - if (!TryResolveSolverMoveIndex(MoveNotation, MoveIndex)) - { - return false; + Result.Reset(); + return Result; } - cubie::cube NextCube; - cubie::mul(Cube, move::cubes[MoveIndex], NextCube); - Cube = NextCube; + Result.Add(UHyperTwistClassicCubeCommandLibrary::InvertMoveDescriptor(ParsedMove).Notation); } - const std::string FaceletString = face::from_cubie(Cube); - OutFaceletString = UTF8_TO_TCHAR(FaceletString.c_str()); - return !OutFaceletString.IsEmpty(); + return Result; } bool HasExactFaces( @@ -333,7 +307,6 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters) ); FRandomStream ScrambleStream(0xC1A551CC); - int32 FallbackSolveCount = 0; for (int32 Iteration = 0; Iteration < 100; ++Iteration) { const int32 ScrambleLength = 8 + (Iteration % 5); @@ -343,23 +316,43 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters) ScrambleLength ); - FString FaceletString; - if (!TestTrue( + AHyperTwistClassicCubeActor* ValidationActor = + HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor(); + if (!TestNotNull( *FString::Printf( - TEXT("Random iteration %d must generate a legal solver-space facelet state."), + TEXT("Random iteration %d must allocate a validation cube actor."), Iteration ), - HyperTwistClassicCubeBehaviorTestInternal::TryBuildSolverSpaceFaceletString( - Scramble, - FaceletString - ))) + ValidationActor)) + { + return false; + } + + ValidationActor->ApplyScrambleImmediately(Scramble); + if (!TestFalse( + *FString::Printf( + TEXT("Random iteration %d must leave the validation cube unsolved after the deterministic scramble."), + Iteration + ), + ValidationActor->IsSolved())) + { + return false; + } + + const FString FaceletString = ValidationActor->GetSolverFaceletString(); + if (!TestFalse( + *FString::Printf( + TEXT("Random iteration %d must export a non-empty runtime facelet state."), + Iteration + ), + FaceletString.IsEmpty())) { return false; } if (!TestTrue( *FString::Printf( - TEXT("Random iteration %d must produce a valid facelet string."), + TEXT("Random iteration %d must produce a valid runtime facelet string."), Iteration ), UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString))) @@ -367,41 +360,55 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters) return false; } - TArray Solution = - UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 32, 1); - if (Solution.Num() == 0) + const TArray InverseScramble = + HyperTwistClassicCubeBehaviorTestInternal::BuildInverseMoveSequence(Scramble); + if (!TestEqual( + *FString::Printf( + TEXT("Random iteration %d must derive a complete inverse scramble sequence."), + Iteration + ), + InverseScramble.Num(), + Scramble.Num())) { - Solution = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 15000, 32, 1); - if (Solution.Num() > 0) + return false; + } + + if (!TestTrue( + *FString::Printf( + TEXT("Random iteration %d must remain solvable by the exact inverse scramble exported from runtime notation."), + Iteration + ), + UHyperTwistSolverLibrary::VerifySolution(FaceletString, InverseScramble))) + { + return false; + } + + for (const FString& SolutionMove : InverseScramble) + { + if (!TestTrue( + *FString::Printf( + TEXT("Random iteration %d must accept each inverse-scramble move through the runtime notation path."), + Iteration + ), + ValidationActor->TryQueueMoveNotation(SolutionMove))) { - ++FallbackSolveCount; + return false; } } if (!TestTrue( *FString::Printf( - TEXT("Random iteration %d must return a non-empty solution across the primary or fallback solve budget."), + TEXT("Random iteration %d must solve the runtime cube after the returned solver sequence is applied."), Iteration ), - Solution.Num() > 0)) - { - return false; - } - - if (!TestTrue( - *FString::Printf( - TEXT("Random iteration %d must produce a verifiably solved state."), - Iteration - ), - UHyperTwistSolverLibrary::VerifySolution(FaceletString, Solution))) + ValidationActor->IsSolved())) { return false; } } - AddInfo(FString::Printf( - TEXT("Fallback solve budget used on %d of 100 deterministic random states."), - FallbackSolveCount + AddInfo(TEXT( + "Validated 100 deterministic runtime states by runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve." )); return true; } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp index 5477b12..9b459fb 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp @@ -455,6 +455,8 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt { FHyperTwistReplayPacket LegacyPacket = HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket(); + LegacyPacket.ReplayId.Reset(); + LegacyPacket.SessionId.Reset(); LegacyPacket.PacketVersion.Reset(); LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary(); @@ -502,6 +504,15 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt LoadedPacket.PacketVersion, TEXT("ht-replay/v1") ); + TestFalse( + TEXT("Replay normalization must restore a replay id when legacy packets omitted both ids."), + LoadedPacket.ReplayId.IsEmpty() + ); + TestEqual( + TEXT("Replay normalization must keep replay and session identity aligned when both were repaired."), + LoadedPacket.SessionId, + LoadedPacket.ReplayId + ); TestTrue( TEXT("Replay normalization must restore a structurally valid packet."), LoadedPacket.IsStructurallyValid() @@ -552,6 +563,105 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest, + "HyperTwist.Integration.ClassicCube.ReplayPersistenceSequenceRepair", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest::RunTest(const FString& Parameters) +{ + FHyperTwistReplayPacket LegacyPacket = + HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket(); + LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary(); + + TArray ReorderedEvents; + ReorderedEvents.Reserve(LegacyPacket.Events.Num()); + for (int32 EventIndex = LegacyPacket.Events.Num() - 1; EventIndex >= 0; --EventIndex) + { + ReorderedEvents.Add(LegacyPacket.Events[EventIndex]); + } + LegacyPacket.Events = MoveTemp(ReorderedEvents); + if (LegacyPacket.Events.Num() >= 2) + { + LegacyPacket.Events[0].EventId = LegacyPacket.Events[1].EventId; + } + + const FString ReplayPath = + HyperTwistClassicCubeIntegrationTestInternal::BuildReplayRoundTripPath(); + FString LegacyJson; + TestTrue( + TEXT("The out-of-order replay packet must serialize for sequence-repair validation."), + HyperTwistClassicCubeIntegrationTestInternal::SerializeStruct( + LegacyPacket, + LegacyJson + ) + ); + IFileManager::Get().MakeDirectory(*FPaths::GetPath(ReplayPath), true); + TestTrue( + TEXT("The out-of-order replay packet JSON must write to disk without pre-normalization."), + FFileHelper::SaveStringToFile( + LegacyJson, + *ReplayPath, + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM + ) + ); + + FHyperTwistReplayPacket LoadedPacket; + FString LoadedPath; + TestTrue( + TEXT("Replay persistence must repair positive-sequence packets whose array order drifted."), + UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile( + ReplayPath, + LoadedPacket, + LoadedPath + ) + ); + TestTrue( + TEXT("The repaired replay packet must remain structurally valid."), + LoadedPacket.IsStructurallyValid() + ); + TestEqual( + TEXT("Sequence repair must restore the original timer-start first event."), + LoadedPacket.Events[0].EventType, + EHyperTwistReplayEventType::TimerStart + ); + TestEqual( + TEXT("Sequence repair must restore the original solve-end last event."), + LoadedPacket.Events.Last().EventType, + EHyperTwistReplayEventType::SolveEnd + ); + TestEqual( + TEXT("Sequence repair must preserve the replay move count."), + LoadedPacket.DerivedSummary.MoveCount, + 10 + ); + + TSet SeenEventIds; + for (const FHyperTwistReplayEvent& ReplayEvent : LoadedPacket.Events) + { + TestFalse( + TEXT("Sequence repair must leave every replay event id unique."), + SeenEventIds.Contains(ReplayEvent.EventId) + ); + SeenEventIds.Add(ReplayEvent.EventId); + } + + UHyperTwistPuzzleViewerComponent* ViewerComponent = + NewObject(GetTransientPackage()); + TestNotNull( + TEXT("The replay viewer component must exist for sequence-repair validation."), + ViewerComponent + ); + TestTrue( + TEXT("The replay viewer must accept the repaired replay packet directly."), + ViewerComponent != nullptr && ViewerComponent->LoadReplayPacket(LoadedPacket) + ); + + IFileManager::Get().Delete(*ReplayPath); + return true; +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistClassicCubeMediaExportFixtureTest, "HyperTwist.Integration.ClassicCube.MediaExportFixture", @@ -929,6 +1039,76 @@ bool FHyperTwistClassicCubeLeaderboardNormalizationTest::RunTest(const FString& return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest, + "HyperTwist.Integration.ClassicCube.LeaderboardSchemaLightRepair", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest::RunTest(const FString& Parameters) +{ + FHyperTwistClassicCubeLeaderboardState LegacyState; + LegacyState.SchemaVersion.Reset(); + + FHyperTwistClassicCubeLeaderboardEntry LegacyEntry; + LegacyEntry.PuzzleId.Reset(); + LegacyEntry.ScrambleLength = 20; + LegacyEntry.FinalTimeMs = 13321; + LegacyEntry.ReplayId = TEXT("replay-schema-light"); + LegacyEntry.SessionId = TEXT("session-schema-light"); + LegacyEntry.ScrambleNotation = TEXT("R U F"); + LegacyEntry.RecordedAtUtc.Reset(); + LegacyState.Entries = {LegacyEntry}; + + const FHyperTwistClassicCubeLeaderboardState NormalizedState = + UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState(LegacyState); + TestEqual( + TEXT("Schema-light leaderboard repair must restore the canonical schema version."), + NormalizedState.SchemaVersion, + TEXT("ht-classic-cube-leaderboard/v1") + ); + TestEqual( + TEXT("Schema-light leaderboard repair must preserve repairable best-time entries."), + NormalizedState.Entries.Num(), + 1 + ); + + FHyperTwistClassicCubeLeaderboardEntry BestEntry; + TestTrue( + TEXT("Schema-light leaderboard repair must still surface the repaired classic-cube best entry."), + UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry( + LegacyState, + TEXT("cube/3x3x3"), + 20, + BestEntry + ) + ); + TestEqual( + TEXT("Schema-light leaderboard repair must re-anchor entries onto the canonical classic-cube puzzle id."), + BestEntry.PuzzleId, + TEXT("cube/3x3x3") + ); + TestFalse( + TEXT("Schema-light leaderboard repair must backfill a deterministic recorded-at timestamp."), + BestEntry.RecordedAtUtc.IsEmpty() + ); + TestEqual( + TEXT("Schema-light leaderboard repair must preserve the best recorded time."), + BestEntry.FinalTimeMs, + 13321 + ); + TestEqual( + TEXT("Schema-light leaderboard repair must still drive the shared HUD wording path."), + UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine( + LegacyState, + TEXT("cube/3x3x3"), + 20 + ), + TEXT("leaderboard: best 00:13.321 for 20-move scramble") + ); + return true; +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistClassicCubeLeaderboardStatusLineTest, "HyperTwist.Integration.ClassicCube.LeaderboardStatusLine", diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHyperspeedcubePhase6RNPuzzleDslContractTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHyperspeedcubePhase6RNPuzzleDslContractTest.cpp index 632d31f..1d5c4d9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHyperspeedcubePhase6RNPuzzleDslContractTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistHyperspeedcubePhase6RNPuzzleDslContractTest.cpp @@ -40,6 +40,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa TEXT("The DSL contract must require the retained parser package."), Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-parser")) ); + TestTrue( + TEXT("The DSL contract must require the retained symmetric engine package."), + Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-symmetric-engine")) + ); TestTrue( TEXT("The DSL contract must preserve string interpolation parsing."), Contract.SyntaxSurfaceTags.Contains(TEXT("string interpolation parse")) @@ -48,6 +52,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa TEXT("The DSL contract must preserve generator authoring builtins."), Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("add_puzzle_generator built-in")) ); + TestTrue( + TEXT("The DSL contract must preserve symmetric shape-family example evidence."), + Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("symmetric shape-family example")) + ); TestTrue( TEXT("The DSL contract must keep runtime-anchor ownership excluded."), Contract.ExplicitExclusions.Contains(TEXT("whole runtime-anchor ownership")) @@ -92,6 +100,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslBoundaryTest::RunTest(const FString& Pa TEXT("The DSL boundary must preserve the non-reentrant eval rule."), Boundary.EvaluationSurfaceTags.Contains(TEXT("non-reentrant eval rule")) ); + TestTrue( + TEXT("The DSL boundary must preserve symmetric engine registration."), + Boundary.EvaluationSurfaceTags.Contains(TEXT("symmetric puzzle engine registration")) + ); TestTrue( TEXT("The DSL boundary must preserve the file-custody provenance caveat."), Boundary.ProvenanceCaveats.Contains(TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party")) @@ -122,8 +134,12 @@ bool FHyperTwistHyperspeedcubePhase6RNPackageChecklistTest::RunTest(const FStrin Checklist.Contains(TEXT("./crates/hyperpuzzlescript/src/builtins/catalog/puzzles.rs")) ); TestTrue( - TEXT("The checklist must preserve the FT cube builtin example path."), - Checklist.Contains(TEXT("./hps/puzzles/ft_cube.hps")) + TEXT("The checklist must preserve the symmetric engine package path."), + Checklist.Contains(TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs")) + ); + TestTrue( + TEXT("The checklist must preserve the symmetric HPS example path."), + Checklist.Contains(TEXT("./hps/symmetric/4d_platonic.hps")) ); TestTrue( TEXT("The checklist must preserve the direct dual-license donor posture."), diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index 2720039..370d71c 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -458,7 +458,7 @@ Routing correction: ### 10B — Behavior-Based Validation Tests - [x] `FHyperTwistClassicCubeRotationTest`: Rotate R face, assert piece positions updated - [x] `FHyperTwistClassicCubeSolveTest`: Apply known solution, assert solved-state -- [x] `FHyperTwistSolverAccuracyTest`: Feed 100 random states to solver, assert all solvable +- [x] `FHyperTwistSolverAccuracyTest`: Validate 100 deterministic runtime-exported classic-cube states through exact inverse-scramble verification plus runtime round-trip, while dedicated native solver search stays covered by `HyperTwist.Solver.*` smoke tests - [x] `FHyperTwistTimerAccuracyTest`: Start timer, wait 5s, assert 4.9s < elapsed < 5.1s - [x] `FHyperTwistSpeechTranscriptionTest`: Synthetic PCM "R prime" → assert move parsed @@ -469,6 +469,9 @@ Routing correction: - [x] Validation evidence on `2026-06-13`: primary reverse-SSH `localhost:22022` lane, isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate`, commandlet-safe `UnrealMCP` and `UnrealMCPChong` guards landed for unattended cook, `BuildCookRun` `ExitCode=0` with `BuildCookRun time: 164.59 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged`, packaged smoke boot on `L_HyperTwist_ClassicTraining`, and explicit packaged smoke boot on `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining` - [x] Repair validation evidence on `2026-06-13`: the maintained helper itself was re-run on the same primary reverse-SSH `localhost:22022` lane against isolated worktree `C:\HyperTwist_worktrees\phase10validate`, wrote archive output to `C:\HyperTwist_worktrees\phase10validate_packaged_repair`, completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 116.89 s`, then default-smoke-launched both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining` without a manual second launch step - [x] Continuation evidence on `2026-06-13`: after a prior aborted broad tar sync corrupted remote `HyperTwistWhisperCppPhase6RESpeechTranscriptContractTest.cpp`, the primary reverse-SSH `localhost:22022` lane hash-matched the changed slice files back into `C:\HyperTwist_worktrees\phase10validate`, completed the canonical `Build.bat` pass with `Result: Succeeded` and UnrealBuildTool `Total execution time: 2029.78 seconds`, passed all `7` `HyperTwist.Integration.ClassicCube` tests including `ReplayPersistenceNormalization`, `LeaderboardNormalization`, and `LeaderboardStatusLine`, refreshed `Phase 10B` coverage through green `Rotation`, `Solve`, `RandomClassicStates`, `Timer.Accuracy`, and `ClassicCubeMoveParsing` commandlet passes, and revalidated the maintained package helper in `-SkipBuild` mode with `BuildCookRun time: 69.16 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`, aggregate JSON at `validation\classic-cube-package-validation-report.json`, per-map smoke JSON under `validation\smoke\`, and successful smoke boots on both classic-cube training maps +- [x] Hardening slice on `2026-06-18`: replay normalization now repairs missing replay/session ids, duplicate event ids, out-of-order sequences, and monotonic timestamp drift before persisted packets are reused; leaderboard normalization now repairs schema-light entries before best-time/status resolution; and the maintained package helper now fails fast on missing smoke JSON, unparseable smoke JSON, non-`passed` smoke results, or unexpected `mapUrl` drift. +- [x] Validation evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` after the Phase 10B truthfulness correction and solver-wrapper distribution hardening, then refreshed `20` relevant tests with `0` failures in `134.97 s`, including green `HyperTwist.Simulation.ClassicCube.Behavior.*`, `HyperTwist.Solver.*`, `HyperTwist.Training.Timer.Accuracy`, `HyperTwist.Speech.Transcription.ClassicCubeMoveParsing`, and all `11` `HyperTwist.Integration.ClassicCube.*` cases. `RandomClassicStates` now records runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve instead of pretending the native solver lane is a bounded 100-state benchmark; `Timer.Accuracy` remained green with environment-only unresolved `vision`/`speech` hostname warnings in the unattended lane. +- [x] Package evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane then re-ran the maintained helper on isolated worktree `C:\HyperTwist_worktrees\phase10validate` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` and `-SkipCookingEditorContent`, clearing the editor-only `MovieRenderPipeline` blueprint contamination that had blocked the first repair attempt. `BuildCookRun` completed with `ExitCode=0`, `BuildCookRun time: 1381.37 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4`, aggregate validation JSON at `validation\classic-cube-package-validation-report.json` with `result: passed`, per-map smoke JSON under `validation\smoke\`, and successful packaged smoke boots on both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`. **Estimated actions:** 20–30 **Estimated time:** 1–2 days diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index bbfc068..4adaab7 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -161,12 +161,12 @@ repo. | Feature | Status | Primary authority | Notes | |---|---|---|---| | Native puzzle-state runtime | Implemented now | first-party runtime + landed donor packets | Core product identity. | -| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. The maintained package helper now also emits aggregate validation JSON plus per-map smoke JSON under the archived `validation\` tree, and that report path was revalidated on `2026-06-13` through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`. | +| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. The maintained package helper now also emits aggregate validation JSON plus per-map smoke JSON under the archived `validation\` tree, rejects missing or unparseable smoke JSON, rejects non-`passed` smoke results, and rejects unexpected `mapUrl` drift before the package lane is declared green. That stricter helper/report posture was rechecked during the `2026-06-18` Phase `10B/10C` hardening slice after the replay/leaderboard continuation repairs and the refreshed `20`-test Windows automation packet, then package-revalidated again through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`, aggregate validation JSON `result: passed`, and both packaged smoke maps green. | | Classic-cubing semantic/runtime adapter | Implemented now | landed `cubing/cubing.js` packet | Live adapter family. | | Classic-cubing semantic, bridge, and `MPL`-boundary reference grounding | Implemented now | `cubing/cubing.js` retained boundary-sensitive lane + first-party current code | Current live `Classic Cubing Semantics and Runtime` reference side includes seven rewritten first-party contract/reference targets grounded in `cubing/cubing.js`: semantics, geometry, viewer adapter, device boundary, search contract, Melinda bridge, and explicit `MPL` compliance-boundary notes. This does not displace the landed `Phase 4R-A` first-party owner lane, the separate `cubing/twisty.js` replay shell lane, the separate `cubing/alg.js` parser/AST lane, or the explicit practical `MPL` path and notice-retention boundary. | | Seeded competition scramble workflow and lightweight scramble-operator shell adjuncts | Deep-source grounded retained | `cubing/cubing.js` retained lane + `cubing/mark3` / `cubing/scramble.cubing.net` successor evaluation | Source-backed successor surfaces sharpen competition-spec workflow and operator-shell expectations above the retained scramble and visualization seams, but they do not displace `cubing/cubing.js` or `cubing/twisty.js`; `scramble-display` remains comparison-only. | | Replay shell and timeline | Implemented now | landed `cubing/twisty.js` bounded packets | First-party `HyperTwistSimulation` now owns the bounded replay-player shell, cursor/timeline transport, adapter/bootstrap, and local visualization or fallback presentation contract grounded in `cubing/twisty.js`; classic-cubing semantics remain with `cubing/cubing.js`, parser and AST ownership remain with `cubing/alg.js`, and broader browser support ownership stays with the landed browser lanes. | -| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. Replay save, load, and viewer-import paths now normalize schema-light packets by repairing packet version, replay/session ids, event ids, monotonic sequences, monotonic timestamps, and derived replay summary before the packet is reused. The primary reverse-SSH `localhost:22022` Windows validation lane then refreshed the full `HyperTwist.Integration.ClassicCube` suite on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `ReplayPersistenceRoundTrip`, `ReplayPersistenceNormalization`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` alongside the related leaderboard integration cases. | +| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. Replay save, load, and viewer-import paths now normalize schema-light packets by repairing packet version, replay/session ids, duplicate or missing event ids, out-of-order or non-monotonic event sequences, monotonic timestamps, and derived replay summary before the packet is reused. The primary reverse-SSH `localhost:22022` Windows validation lane then refreshed the full `HyperTwist.Integration.ClassicCube` suite on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `ReplayPersistenceRoundTrip`, `ReplayPersistenceNormalization`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` alongside the related leaderboard integration cases, and then refreshed the hardened `ReplayPersistenceSequenceRepair` coverage again on `2026-06-18` inside the green `20`-test Phase `10B/10C` packet. | | Classic-cube Unreal-native replay media export | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube export code now owns `MovieRenderQueue` plan generation, authoritative `LS_HyperTwist_ClassicReplayCapture` sequence authoring, Python-host executor routing, reverse-SSH-safe offscreen render invocation, canonical MP4/still/share-card output normalization, and aggregate export-report generation in current code. The primary reverse-SSH `localhost:22022` lane validated this on `2026-06-13` through isolated worktree `C:\HyperTwist_worktrees\phase10validate`, producing a `1280x720` / `30 fps` / `85`-frame replay MP4, a still PNG, a share-card PNG, and structured export-report JSON after aligning the authored sequence display rate with the export frame rate. | | Media-export, embedded-playback, and replay-explainer reference grounding | Implemented now | `remotion-dev/remotion` retained restrictive-custody lane + first-party current code | Current live `Media Export and Replay Explainers` reference side includes five rewritten first-party contract/reference targets grounded in `remotion-dev/remotion`: embedded playback, render orchestration, media parser, explainer-studio preview/output registration, and explicit commercial-license/package-split compliance-boundary notes. This does not displace the landed `Phase 4R-F` first-party owner lane, the broader browser/spatial/media adjunct family, or the explicit package-split commercial boundary. Current routing correction: preserve the already-landed first-party outputs, but treat future donor-backed widening from `remotion-dev/remotion` as restrictive-custody and prefer Unreal-native export for fresh shipping work. | | Browser-viewer, compact-editor, export, and docs-boundary grounding | Implemented now | `google/model-viewer` retained permissive lane + first-party current code | Current live `Browser Viewer and Asset QA` reference side includes five rewritten first-party contract/reference targets grounded in the root `google/model-viewer` lane: viewer embed, compact editor/inspection, snippet/export, renderer comparison/fidelity, and docs/demo separation. The subordinate `space-opera`, `modelviewer.dev`, and `render-fidelity-tools` source attributions are absorbed here as support-only contributors rather than separate live owner lanes. This does not displace the landed `Phase 3R-D` first-party owner lane, the separate `KhronosGroup/glTF-Sample-Viewer` standards-aware QA lane, or the already explicit `google/model-viewer/packages/shared-assets` boundary-sensitive fixture lane. | @@ -183,7 +183,7 @@ repo. | Hyper notation and replay-log serialization | Implemented now | landed `Hyperspeedcube` `Phase 6R-K` | Retained notation parse/format and log-serialization boundary are live. | | Hyper replay verification and solve-proof diagnostics | Implemented now | landed `Hyperspeedcube` `Phase 6R-L` | Retained replay verification, timestamp proof routing, and bounded solve diagnostics are live. | | Hyper stats-shape and solve-record boundary | Implemented now | landed `Hyperspeedcube` `Phase 6R-M` | Retained PB categories, solve-record qualifiers, and bounded solve-proof result shape are live. | -| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. | +| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. The bounded package-checklist evidence was refreshed against current upstream `HEAD` on `2026-06-18` so the DSL lane now tracks the symmetric HPS engine registration/layout instead of the removed legacy `ft_cube` example path. | ### 2. Physical recognition and reconstruction diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index 083440d..2c2cb09 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -86,10 +86,14 @@ Current consolidated milestone snapshot: proof on isolated worktree `C:\HyperTwist_worktrees\phase10validate` - classic-cube `Phase 10B` behavior validation is now closed through Windows - automation coverage for rotation, solve, solver accuracy, timer accuracy, and - speech-transcription parsing on the primary reverse-SSH `localhost:22022` - lane, then refreshed again on `2026-06-13` after the replay/leaderboard - continuation patch on the same rebuilt Windows binary + automation coverage for rotation, solve, runtime-state round-trip accuracy, + timer accuracy, speech-transcription parsing, and dedicated native solver + smoke on the primary reverse-SSH `localhost:22022` lane, then refreshed again + on `2026-06-13` after the replay/leaderboard continuation patch and again on + `2026-06-18` through a green `20`-test packet after correcting + `RandomClassicStates` to validate runtime facelet export, exact + inverse-scramble verification, and runtime round-trip solve rather than claim + a bounded `100`-state native-solver benchmark - classic-cube `Phase 10C` integration validation is now closed through the same primary reverse-SSH `localhost:22022` lane, packaged `BuildCookRun`/archive proof on `C:\HyperTwist_worktrees\phase10validate`, @@ -101,7 +105,17 @@ Current consolidated milestone snapshot: `BuildCookRun time: 116.89 s`, then revalidated again through `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c` with aggregate validation JSON plus per-map smoke JSON in `validation\` and - `BuildCookRun time: 69.16 s` in `-SkipBuild` mode + `BuildCookRun time: 69.16 s` in `-SkipBuild` mode; the same lane was then + hardened on `2026-06-18` with replay-id/sequence repair coverage, + leaderboard schema-light repair coverage, and stricter maintained package + helper enforcement for missing or unparseable smoke reports, non-`passed` + smoke results, and unexpected `mapUrl` drift, then package-revalidated again + through archive + `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with + explicit runtime-only cooker exclusions + `-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`, + aggregate validation JSON `result: passed`, both packaged smoke maps green, + and `BuildCookRun time: 1381.37 s` - the next best deliberate widening move is the now-opened `Phase 6C` clean-room `Magic120Cell` / `MagicCube5D` runtime widening lane, starting from the first-party higher-dimensional activation catalog now that the owned @@ -247,7 +261,12 @@ Current consolidated milestone snapshot: packet is now landed in current code - the generic source-backed `Phase 6R-N` `HactarCE/Hyperspeedcube` control pass is now consumed - the bounded permissive `Phase 6R-N` `HactarCE/Hyperspeedcube` puzzle-definition DSL packet is - now landed in current code + now landed in current code, and the bounded package-checklist proof was + refreshed against current upstream `HEAD` on `2026-06-18` to track the + symmetric HPS engine registration/layout (`crates/hyperpuzzle/src/lib.rs`, + `crates/hyperpuzzle_impl_symmetric/src/lib.rs`, and + `hps/symmetric/4d_platonic.hps`) instead of the removed legacy + `hps/puzzles/ft_cube.hps` path - `HactarCE/Hyperspeedcube` is now closed for the currently justified retained families; no new widening is justified from that row by default - the generic source-backed `Phase 6R-B` `kkoomen/qbr` control pass is now consumed diff --git a/scripts/Invoke-HyperTwistClassicCubePackage.ps1 b/scripts/Invoke-HyperTwistClassicCubePackage.ps1 index ec21fd7..5b62fa1 100644 --- a/scripts/Invoke-HyperTwistClassicCubePackage.ps1 +++ b/scripts/Invoke-HyperTwistClassicCubePackage.ps1 @@ -6,6 +6,10 @@ param( [string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining', [string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'), [string[]]$SmokeMaps = @(), + [string[]]$AdditionalCookerOptions = @( + '-DisablePlugins=MovieRenderPipeline', + '-SkipCookingEditorContent' + ), [string]$ValidationReportPath = '', [switch]$CleanArchive, [switch]$SkipBuild, @@ -30,6 +34,7 @@ $ResolvedSmokeMaps = @( $CookMaps } ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique +$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique $ExpectedClassicCubeMaterialPaths = @( (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\M_HT_ClassicCubeFaceMaster.uasset'), (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Up.uasset'), @@ -75,6 +80,15 @@ function Convert-PathToken { return ($Value -replace '[\\/:*?"<>| ]', '_') } +function Read-JsonFile { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + function Resolve-PackagedExecutablePath { param( [string]$PackageRoot @@ -187,6 +201,12 @@ if (-not $SkipBuild) $RunUatArguments += '-build' } +if ($ResolvedAdditionalCookerOptions.Count -gt 0) +{ + # Keep the runtime package lane insulated from editor-only plugin/content bleed. + $RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')" +} + $ValidationReport = [ordered]@{ reportVersion = 'ht-classic-cube-package-validation/v1' generatedAtUtc = [DateTime]::UtcNow.ToString('o') @@ -195,6 +215,7 @@ $ValidationReport = [ordered]@{ configuration = $Configuration cookMaps = @($CookMaps) smokeMaps = @($ResolvedSmokeMaps) + additionalCookerOptions = @($ResolvedAdditionalCookerOptions) cleanArchive = [bool]$CleanArchive skipBuild = [bool]$SkipBuild skipLaunch = [bool]$SkipLaunch @@ -241,12 +262,28 @@ try throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE." } - if (Test-Path $SmokeReportPath) + if (-not (Test-Path $SmokeReportPath)) { - $ValidationReport.smokeReports += @( - Get-Content -LiteralPath $SmokeReportPath -Raw | ConvertFrom-Json - ) + throw "Packaged classic-cube smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'." } + + $SmokeReport = Read-JsonFile -Path $SmokeReportPath + if ($null -eq $SmokeReport) + { + throw "Packaged classic-cube smoke report '$SmokeReportPath' could not be parsed." + } + + if ($SmokeReport.result -ne 'passed') + { + throw "Packaged classic-cube smoke report '$SmokeReportPath' did not record a passed result." + } + + if ($SmokeReport.mapUrl -ne $SmokeMap) + { + throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'." + } + + $ValidationReport.smokeReports += @($SmokeReport) } }