Repair classic-cube validation helper and leaderboard normalization
This commit is contained in:
parent
046653703e
commit
dbd48d0965
8 changed files with 305 additions and 37 deletions
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace HyperTwistClassicCubeLeaderboardLibraryInternal
|
||||
{
|
||||
const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1");
|
||||
|
||||
template <typename TStruct>
|
||||
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<FHyperTwistClassicCubeLeaderboardEntry>& 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<FString, FHyperTwistClassicCubeLeaderboardEntry> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue