Repair classic-cube validation helper and leaderboard normalization

This commit is contained in:
axiomlogicnexus 2026-06-13 01:06:13 +00:00
parent 046653703e
commit dbd48d0965
8 changed files with 305 additions and 37 deletions

View file

@ -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();
}

View file

@ -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,

View file

@ -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",

View file

@ -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:** 4060
**Estimated time:** 23 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:** 2030
**Estimated time:** 12 days

View file

@ -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

View file

@ -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. |

View file

@ -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

View file

@ -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."
}
}
}