From dbd48d096578ca8f3ea7b57fd47737e9cdeddd7a Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Sat, 13 Jun 2026 01:06:13 +0000 Subject: [PATCH] Repair classic-cube validation helper and leaderboard normalization --- ...yperTwistClassicCubeLeaderboardLibrary.cpp | 192 +++++++++++++++--- .../HyperTwistClassicCubeLeaderboardLibrary.h | 5 + .../HyperTwistClassicCubeIntegrationTest.cpp | 86 ++++++++ ...NSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md | 4 + ...BUILD_VALIDATION_REQUIREMENT_2026-06-01.md | 13 ++ .../HyperTwist/FEATURE_REGISTRY.md | 4 +- .../HyperTwist/ROADMAP.md | 11 +- .../Invoke-HyperTwistClassicCubePackage.ps1 | 27 ++- 8 files changed, 305 insertions(+), 37 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp index 80872e5..2a64796 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.cpp @@ -7,6 +7,8 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal { + const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1"); + template bool SerializeStruct(const TStruct& Value, FString& OutJson) { @@ -30,6 +32,87 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal { return FString::Printf(TEXT("%s|%d"), *PuzzleId, ScrambleLength); } + + bool ShouldPreferEntry( + const FHyperTwistClassicCubeLeaderboardEntry& Candidate, + const FHyperTwistClassicCubeLeaderboardEntry& Existing + ) + { + if (!Candidate.IsStructurallyValid()) + { + return false; + } + + if (!Existing.IsStructurallyValid()) + { + return true; + } + + if (Candidate.FinalTimeMs != Existing.FinalTimeMs) + { + return Candidate.FinalTimeMs < Existing.FinalTimeMs; + } + + if (!Candidate.RecordedAtUtc.IsEmpty()) + { + if (Existing.RecordedAtUtc.IsEmpty()) + { + return true; + } + + if (Candidate.RecordedAtUtc != Existing.RecordedAtUtc) + { + return Candidate.RecordedAtUtc < Existing.RecordedAtUtc; + } + } + + if (Candidate.ReplayId != Existing.ReplayId) + { + return Candidate.ReplayId < Existing.ReplayId; + } + + if (Candidate.SessionId != Existing.SessionId) + { + return Candidate.SessionId < Existing.SessionId; + } + + return Candidate.ScrambleNotation < Existing.ScrambleNotation; + } + + void SortEntries(TArray& Entries) + { + Entries.Sort([]( + const FHyperTwistClassicCubeLeaderboardEntry& Left, + const FHyperTwistClassicCubeLeaderboardEntry& Right) + { + if (Left.PuzzleId != Right.PuzzleId) + { + return Left.PuzzleId < Right.PuzzleId; + } + + if (Left.ScrambleLength != Right.ScrambleLength) + { + return Left.ScrambleLength < Right.ScrambleLength; + } + + if (Left.FinalTimeMs != Right.FinalTimeMs) + { + return Left.FinalTimeMs < Right.FinalTimeMs; + } + + if (Left.RecordedAtUtc != Right.RecordedAtUtc) + { + return Left.RecordedAtUtc < Right.RecordedAtUtc; + } + + if (Left.ReplayId != Right.ReplayId) + { + return Left.ReplayId < Right.ReplayId; + } + + return Left.SessionId < Right.SessionId; + }); + } } FString UHyperTwistClassicCubeLeaderboardLibrary::GetDefaultLeaderboardPath() @@ -52,6 +135,49 @@ FString UHyperTwistClassicCubeLeaderboardLibrary::ResolveLeaderboardPath( return FPaths::ConvertRelativePathToFull(EffectivePath); } +FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState( + const FHyperTwistClassicCubeLeaderboardState& LeaderboardState +) +{ + FHyperTwistClassicCubeLeaderboardState NormalizedState = LeaderboardState; + if (NormalizedState.SchemaVersion.IsEmpty()) + { + NormalizedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion; + } + + TMap BestEntryByKey; + for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries) + { + if (!Entry.IsStructurallyValid()) + { + continue; + } + + const FString Key = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey( + Entry.PuzzleId, + Entry.ScrambleLength + ); + if (FHyperTwistClassicCubeLeaderboardEntry* ExistingEntry = BestEntryByKey.Find(Key)) + { + if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry( + Entry, + *ExistingEntry)) + { + *ExistingEntry = Entry; + } + } + else + { + BestEntryByKey.Add(Key, Entry); + } + } + + NormalizedState.Entries.Reset(); + BestEntryByKey.GenerateValueArray(NormalizedState.Entries); + HyperTwistClassicCubeLeaderboardLibraryInternal::SortEntries(NormalizedState.Entries); + return NormalizedState; +} + bool UHyperTwistClassicCubeLeaderboardLibrary::SaveLeaderboardStateToFile( const FHyperTwistClassicCubeLeaderboardState& LeaderboardState, const FString& LeaderboardPath, @@ -59,14 +185,16 @@ bool UHyperTwistClassicCubeLeaderboardLibrary::SaveLeaderboardStateToFile( ) { OutResolvedPath = ResolveLeaderboardPath(LeaderboardPath); - if (!LeaderboardState.IsStructurallyValid() || OutResolvedPath.IsEmpty()) + const FHyperTwistClassicCubeLeaderboardState NormalizedState = + NormalizeLeaderboardState(LeaderboardState); + if (!NormalizedState.IsStructurallyValid() || OutResolvedPath.IsEmpty()) { return false; } FString Json; if (!HyperTwistClassicCubeLeaderboardLibraryInternal::SerializeStruct( - LeaderboardState, + NormalizedState, Json)) { return false; @@ -99,11 +227,16 @@ bool UHyperTwistClassicCubeLeaderboardLibrary::LoadLeaderboardStateFromFile( return false; } - return HyperTwistClassicCubeLeaderboardLibraryInternal::DeserializeStruct( + FHyperTwistClassicCubeLeaderboardState LoadedState; + if (!HyperTwistClassicCubeLeaderboardLibraryInternal::DeserializeStruct( Json, - OutLeaderboardState - ) - && OutLeaderboardState.IsStructurallyValid(); + LoadedState)) + { + return false; + } + + OutLeaderboardState = NormalizeLeaderboardState(LoadedState); + return OutLeaderboardState.IsStructurallyValid(); } FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary::RecordBestTime( @@ -111,10 +244,12 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary: const FHyperTwistClassicCubeLeaderboardEntry& Entry ) { - FHyperTwistClassicCubeLeaderboardState UpdatedState = LeaderboardState; + FHyperTwistClassicCubeLeaderboardState UpdatedState = + NormalizeLeaderboardState(LeaderboardState); if (!UpdatedState.IsStructurallyValid()) { UpdatedState = FHyperTwistClassicCubeLeaderboardState(); + UpdatedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion; } if (!Entry.IsStructurallyValid()) { @@ -142,27 +277,15 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary: else { const FHyperTwistClassicCubeLeaderboardEntry& ExistingEntry = UpdatedState.Entries[ExistingIndex]; - const bool bShouldReplace = Entry.FinalTimeMs < ExistingEntry.FinalTimeMs - || (Entry.FinalTimeMs == ExistingEntry.FinalTimeMs - && !Entry.RecordedAtUtc.IsEmpty() - && (ExistingEntry.RecordedAtUtc.IsEmpty() - || Entry.RecordedAtUtc < ExistingEntry.RecordedAtUtc)); - if (bShouldReplace) + if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry( + Entry, + ExistingEntry)) { UpdatedState.Entries[ExistingIndex] = Entry; } } - UpdatedState.Entries.Sort([]( - const FHyperTwistClassicCubeLeaderboardEntry& Left, - const FHyperTwistClassicCubeLeaderboardEntry& Right) - { - if (Left.PuzzleId != Right.PuzzleId) - { - return Left.PuzzleId < Right.PuzzleId; - } - return Left.ScrambleLength < Right.ScrambleLength; - }); + HyperTwistClassicCubeLeaderboardLibraryInternal::SortEntries(UpdatedState.Entries); return UpdatedState; } @@ -174,19 +297,32 @@ bool UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry( ) { OutEntry = FHyperTwistClassicCubeLeaderboardEntry(); - if (!LeaderboardState.IsStructurallyValid() || PuzzleId.IsEmpty() || ScrambleLength < 0) + if (PuzzleId.IsEmpty() || ScrambleLength < 0) { return false; } - for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries) + const FHyperTwistClassicCubeLeaderboardState NormalizedState = + NormalizeLeaderboardState(LeaderboardState); + if (!NormalizedState.IsStructurallyValid()) { - if (Entry.PuzzleId == PuzzleId && Entry.ScrambleLength == ScrambleLength) + return false; + } + + bool bFoundBestEntry = false; + for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : NormalizedState.Entries) + { + if (Entry.PuzzleId == PuzzleId + && Entry.ScrambleLength == ScrambleLength + && (!bFoundBestEntry + || HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry( + Entry, + OutEntry))) { OutEntry = Entry; - return OutEntry.IsStructurallyValid(); + bFoundBestEntry = true; } } - return false; + return bFoundBestEntry && OutEntry.IsStructurallyValid(); } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.h index 223112b..35341ca 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.h @@ -81,6 +81,11 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard") static FString ResolveLeaderboardPath(const FString& LeaderboardPath); + UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|Leaderboard") + static FHyperTwistClassicCubeLeaderboardState NormalizeLeaderboardState( + const FHyperTwistClassicCubeLeaderboardState& LeaderboardState + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Leaderboard") static bool SaveLeaderboardStateToFile( const FHyperTwistClassicCubeLeaderboardState& LeaderboardState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp index df299a8..10cf15f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubeIntegrationTest.cpp @@ -530,6 +530,92 @@ bool FHyperTwistClassicCubeLeaderboardPersistenceTest::RunTest(const FString& Pa return true; } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubeLeaderboardNormalizationTest, + "HyperTwist.Integration.ClassicCube.LeaderboardNormalization", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubeLeaderboardNormalizationTest::RunTest(const FString& Parameters) +{ + FHyperTwistClassicCubeLeaderboardState LegacyState; + LegacyState.SchemaVersion.Reset(); + + FHyperTwistClassicCubeLeaderboardEntry SlowerEntry; + SlowerEntry.PuzzleId = TEXT("cube/3x3x3"); + SlowerEntry.ScrambleLength = 20; + SlowerEntry.FinalTimeMs = 15321; + SlowerEntry.ReplayId = TEXT("replay-slower"); + SlowerEntry.SessionId = TEXT("session-slower"); + SlowerEntry.ScrambleNotation = TEXT("R U F"); + SlowerEntry.RecordedAtUtc = TEXT("2026-06-12T10:10:00Z"); + + FHyperTwistClassicCubeLeaderboardEntry FasterLaterEntry = SlowerEntry; + FasterLaterEntry.FinalTimeMs = 14321; + FasterLaterEntry.ReplayId = TEXT("replay-faster-later"); + FasterLaterEntry.SessionId = TEXT("session-faster-later"); + FasterLaterEntry.RecordedAtUtc = TEXT("2026-06-12T10:15:00Z"); + + FHyperTwistClassicCubeLeaderboardEntry FasterEarlierEntry = FasterLaterEntry; + FasterEarlierEntry.ReplayId = TEXT("replay-faster-earlier"); + FasterEarlierEntry.SessionId = TEXT("session-faster-earlier"); + FasterEarlierEntry.RecordedAtUtc = TEXT("2026-06-12T10:05:00Z"); + + FHyperTwistClassicCubeLeaderboardEntry OtherBucketEntry = SlowerEntry; + OtherBucketEntry.ScrambleLength = 25; + OtherBucketEntry.FinalTimeMs = 18234; + OtherBucketEntry.ReplayId = TEXT("replay-other-bucket"); + OtherBucketEntry.SessionId = TEXT("session-other-bucket"); + OtherBucketEntry.RecordedAtUtc = TEXT("2026-06-12T10:20:00Z"); + + LegacyState.Entries = { + SlowerEntry, + FasterLaterEntry, + OtherBucketEntry, + FasterEarlierEntry + }; + + const FHyperTwistClassicCubeLeaderboardState NormalizedState = + UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState(LegacyState); + TestEqual( + TEXT("Normalization must restore the canonical schema version for schema-light legacy state."), + NormalizedState.SchemaVersion, + TEXT("ht-classic-cube-leaderboard/v1") + ); + TestEqual( + TEXT("Normalization must collapse duplicate scramble buckets down to one retained best entry."), + NormalizedState.Entries.Num(), + 2 + ); + + FHyperTwistClassicCubeLeaderboardEntry BestEntry; + TestTrue( + TEXT("Normalization must still allow the best entry to resolve from the repaired leaderboard state."), + UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry( + LegacyState, + TEXT("cube/3x3x3"), + 20, + BestEntry + ) + ); + TestEqual( + TEXT("The normalized best entry must preserve the fastest solve time for the duplicate bucket."), + BestEntry.FinalTimeMs, + 14321 + ); + TestEqual( + TEXT("Tied fastest solves must prefer the earliest recorded timestamp for deterministic repair."), + BestEntry.ReplayId, + TEXT("replay-faster-earlier") + ); + TestEqual( + TEXT("The repaired best entry must preserve its session identity."), + BestEntry.SessionId, + TEXT("session-faster-earlier") + ); + return true; +} + IMPLEMENT_SIMPLE_AUTOMATION_TEST( FHyperTwistClassicCubeRuntimeReplaySmokeTest, "HyperTwist.Integration.ClassicCube.RuntimeReplaySmoke", 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 12f0480..dcb2e98 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -406,6 +406,7 @@ Closure read: - [x] Record move sequence + timestamps during solve - [x] Export to `.json` replay file - [x] Load replay, reconstruct animation +- [x] Legacy-safe replay packet consumers now preserve current behavior without depending on hand-curated local state ordering ### 9B — Media Export - [ ] Use UE `MovieRenderQueue` as the primary video-export route; treat `remotion-dev/remotion` only as restrictive-custody reference context unless a later clean-room/specification pass explicitly reopens it @@ -419,6 +420,7 @@ Routing correction: - [x] Local SQLite or JSON leaderboard (no backend yet) - [x] Store best times per puzzle type + scramble length - [x] Display in HUD +- [x] Normalize schema-light or duplicate local JSON buckets before resolving the displayed best time **Estimated actions:** 40–60 **Estimated time:** 2–3 days @@ -443,7 +445,9 @@ Routing correction: ### 10C — Integration Tests - [x] Launch `L_HyperTwist_ClassicTraining`, simulate 10 moves, verify no crash - [x] Package build test: verify `UnrealHyperTwist.exe` launches and cube is visible +- [x] The packaged validation helper now smoke-boots every cooked classic-cube training map by default instead of depending on a remembered second manual launch - [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 **Estimated actions:** 20–30 **Estimated time:** 1–2 days diff --git a/docs/ops/HYPERTWIST_UNREAL_BUILD_VALIDATION_REQUIREMENT_2026-06-01.md b/docs/ops/HYPERTWIST_UNREAL_BUILD_VALIDATION_REQUIREMENT_2026-06-01.md index 5c5903f..699ad6b 100644 --- a/docs/ops/HYPERTWIST_UNREAL_BUILD_VALIDATION_REQUIREMENT_2026-06-01.md +++ b/docs/ops/HYPERTWIST_UNREAL_BUILD_VALIDATION_REQUIREMENT_2026-06-01.md @@ -139,6 +139,19 @@ smoke boots succeeded on both `L_HyperTwist_ClassicTraining` and build-plus-integration proof to primary-lane package/archive/launch proof on the same current source state. +The maintained helper `scripts\Invoke-HyperTwistClassicCubePackage.ps1` now +defaults its smoke-launch pass across every cooked classic-cube training map in +that package set, so the canonical package-validation path no longer depends on +an operator remembering a second manual packaged boot for follow-along. + +That repaired helper path was itself revalidated on `2026-06-13` against +isolated worktree `C:\HyperTwist_worktrees\phase10validate`: archive output was +written to `C:\HyperTwist_worktrees\phase10validate_packaged_repair`, +`BuildCookRun` completed with `ExitCode=0` and `BuildCookRun time: 116.89 s`, +and the helper then smoke-launched both `L_HyperTwist_ClassicTraining` and +`L_HyperTwist_FollowAlongTraining` in its default pass without a manual second +launch step. + Additional live proof on `2026-06-13` also validated the new `Phase 6C` higher-dimensional activation packet on that same primary lane: the isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate` invalidated its 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 e3dc8d7..617bcda 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -166,7 +166,7 @@ repo. | 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. The primary reverse-SSH `localhost:22022` Windows validation lane passed `ReplayPersistenceRoundTrip`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. | +| 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. The primary reverse-SSH `localhost:22022` Windows validation lane passed `ReplayPersistenceRoundTrip`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. Current replay-adjacent local consumers are also now hardened so schema-light or duplicate local state does not silently distort the retained best-attempt view. | | 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. | | Standards-aware asset-validation and statistics grounding | Implemented now | `KhronosGroup/glTF-Sample-Viewer` retained permissive support lane + first-party current code | Current live `Browser Viewer and Asset QA` reference side includes one rewritten first-party target grounded in `KhronosGroup/glTF-Sample-Viewer`: standards-aware asset validation and statistics. This does not displace the landed `Phase 3R-D` first-party browser viewer owner, the separate root `google/model-viewer` viewer/reference lane, or the already explicit shared-assets boundary-sensitive fixture lane. | @@ -249,7 +249,7 @@ repo. | Feature | Status | Primary authority | Notes | |---|---|---|---| | Publication/leaderboard projection | Implemented now | landed `kash/cubedesk` `Bound 4` | Live bounded slice. | -| Classic-cube local leaderboard stub | Implemented now | first-party current code + landed timer/training substrate | Current classic-cube runtime persists best local solve times by puzzle id and scramble-length bucket to JSON, carries replay/session identity forward on winning entries, and projects the best current-bucket line into the in-game HUD without requiring a backend. The primary reverse-SSH `localhost:22022` Windows validation lane passed `LeaderboardPersistence` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. | +| Classic-cube local leaderboard stub | Implemented now | first-party current code + landed timer/training substrate | Current classic-cube runtime persists best local solve times by puzzle id and scramble-length bucket to JSON, carries replay/session identity forward on winning entries, repairs schema-light or duplicate local leaderboard buckets before resolving the displayed best entry, and projects the best current-bucket line into the in-game HUD without requiring a backend. The primary reverse-SSH `localhost:22022` Windows validation lane passed `LeaderboardPersistence` on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-12`. | | Entitlement gating | Implemented now | landed `kash/cubedesk` `Bound 4` | Live bounded slice. | | Local social challenge bundle | Implemented now | landed `kash/cubedesk` `Bound 5` | Live bounded slice. | | Broader admin/report lane | Deep-source grounded retained | repo-local justification required | `Bound 6` is deferred and not currently justified for implementation. | diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index b5363e3..f13eef0 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -72,7 +72,8 @@ Current consolidated milestone snapshot: `C:\HyperTwist_worktrees\phase10validate` - classic-cube `Phase 9C` local leaderboard stub is now closed through first-party JSON persistence, scramble-length bucket best-time retention, HUD - display, and live Windows integration proof on isolated worktree + display, legacy-state normalization for schema-light or duplicate local + buckets, and live Windows integration 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 @@ -81,8 +82,12 @@ Current consolidated milestone snapshot: - 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`, - and packaged smoke launches on both `L_HyperTwist_ClassicTraining` and - `L_HyperTwist_FollowAlongTraining` + packaged smoke launches on both `L_HyperTwist_ClassicTraining` and + `L_HyperTwist_FollowAlongTraining`, and helper-script default smoke coverage + across the full cooked classic-cube training map set; that helper default was + revalidated on `2026-06-13` through archive + `C:\HyperTwist_worktrees\phase10validate_packaged_repair` with + `BuildCookRun time: 116.89 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 diff --git a/scripts/Invoke-HyperTwistClassicCubePackage.ps1 b/scripts/Invoke-HyperTwistClassicCubePackage.ps1 index 13ae92a..b0d810f 100644 --- a/scripts/Invoke-HyperTwistClassicCubePackage.ps1 +++ b/scripts/Invoke-HyperTwistClassicCubePackage.ps1 @@ -5,6 +5,7 @@ param( [string]$Configuration = 'Development', [string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining', [string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'), + [string[]]$SmokeMaps = @(), [switch]$CleanArchive, [switch]$SkipBuild, [switch]$SkipLaunch @@ -17,8 +18,17 @@ $UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproje $LaunchScriptPath = Join-Path $ProjectRoot 'scripts\Launch-HyperTwistClassicCubePackage.ps1' $GameTargetReceiptPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.target' $UnrealBuildToolSavedPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\UnrealBuildTool' -$ClassicCubeBootUrl = $CookMap $CookMaps = @($CookMap) + $AdditionalCookMaps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique +$ResolvedSmokeMaps = @( + if ($SmokeMaps.Count -gt 0) + { + $SmokeMaps + } + else + { + $CookMaps + } +) | 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'), @@ -73,6 +83,11 @@ foreach ($TargetCookMap in $CookMaps) Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetCookMap } +foreach ($TargetSmokeMap in $ResolvedSmokeMaps) +{ + Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetSmokeMap +} + foreach ($ExpectedMaterialPath in $ExpectedClassicCubeMaterialPaths) { if (-not (Test-Path $ExpectedMaterialPath)) @@ -131,9 +146,13 @@ if (-not $SkipLaunch) throw "Launch script was not found at '$LaunchScriptPath'." } - & $LaunchScriptPath -PackageRoot $ArchiveDirectory -MapUrl $ClassicCubeBootUrl - if ($LASTEXITCODE -ne 0) + foreach ($SmokeMap in $ResolvedSmokeMaps) { - throw "Packaged classic-cube smoke launch failed with exit code $LASTEXITCODE." + Write-Host "Smoke validating packaged classic-cube map '$SmokeMap'..." + & $LaunchScriptPath -PackageRoot $ArchiveDirectory -MapUrl $SmokeMap + if ($LASTEXITCODE -ne 0) + { + throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE." + } } }