From 393e24b40fff699aee8841b42cbdace8195ff0f5 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 23:03:38 +0200 Subject: [PATCH] Explain handoff-aware deck shaping --- .../HyperTwistCoachDashboardWidget.cpp | 20 ++- .../HyperTwistTrainingSubsystem.cpp | 167 ++++++++++++++---- .../HyperTwistTrainingTypes.h | 6 + 3 files changed, 157 insertions(+), 36 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 7ae1aa4..711923d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -5706,12 +5706,24 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (FocusTextBlock != nullptr) { - FocusTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Focus deck: %s | Method: %s | Cases: %d"), + const FString FocusLine = FString::Printf( + TEXT("Focus deck: %s | Method: %s | Cases: %d%s"), CachedCoachPanelState.FocusDeckId.IsEmpty() ? TEXT("n/a") : *CachedCoachPanelState.FocusDeckId, CachedCoachPanelState.FocusMethodSegmentId.IsEmpty() ? TEXT("n/a") : *CachedCoachPanelState.FocusMethodSegmentId, - CachedCoachPanelState.FocusCaseCount - ))); + CachedCoachPanelState.FocusCaseCount, + CachedCoachPanelState.SuggestedLaunchCaseBudget > 0 + ? *FString::Printf( + TEXT(" | Launch cases: %d"), + CachedCoachPanelState.SuggestedLaunchCaseBudget) + : TEXT("") + ); + FocusTextBlock->SetText(FText::FromString(CachedCoachPanelState.LaunchBudgetReasonLine.IsEmpty() + ? FocusLine + : FString::Printf( + TEXT("%s\nBudget reason: %s"), + *FocusLine, + *CachedCoachPanelState.LaunchBudgetReasonLine + ))); } if (ActiveRunHeaderTextBlock != nullptr) { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 180ff7d..3035010 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -12,6 +12,13 @@ namespace HyperTwistTrainingSubsystemInternal { const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active"); + struct FResolvedCoachCaseBudget + { + int32 RequestedCaseCount = 0; + int32 ResolvedCaseCount = 0; + FString ReasonLine; + }; + EHyperTwistCoachPriority CoachPriorityFromScore(const float Score) { if (Score >= 85.0f) @@ -94,27 +101,36 @@ namespace HyperTwistTrainingSubsystemInternal return FString(); } - int32 ResolvePreferredCoachCaseBudget( + FString FormatLaunchBudgetLabel(const int32 CaseCount) + { + return FString::Printf(TEXT("Launch cases: %d"), CaseCount); + } + + FResolvedCoachCaseBudget ResolvePreferredCoachCaseBudget( const int32 MaxCases, const EHyperTwistTrainingCoachGuidanceLane DeckLane, const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, const bool bQueuedLaunch) { - const int32 DesiredCaseCount = MaxCases > 0 ? MaxCases : 3; - int32 AdjustedCaseCount = DesiredCaseCount; + FResolvedCoachCaseBudget Result; + Result.RequestedCaseCount = MaxCases > 0 ? MaxCases : 3; + Result.ResolvedCaseCount = Result.RequestedCaseCount; + TArray BudgetReasons; if (CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp && DeckLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp) { - AdjustedCaseCount = FMath::Max(1, AdjustedCaseCount - 1); + Result.ResolvedCaseCount = FMath::Max(1, Result.ResolvedCaseCount - 1); + BudgetReasons.Add(TEXT("follow-up guidance is tightening the launch slice")); } else if (CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended && DeckLane == EHyperTwistTrainingCoachGuidanceLane::Recommended - && AdjustedCaseCount <= 3 + && Result.ResolvedCaseCount <= 3 && (CoachMemorySnapshot.bHasRepeatRecommendationPressure || CoachMemorySnapshot.RecommendedGuidanceContinueBias > 0 || CoachMemorySnapshot.RecurringCoachCaseIds.Num() >= 3)) { - AdjustedCaseCount += 1; + Result.ResolvedCaseCount += 1; + BudgetReasons.Add(TEXT("recommendation pressure is widening the launch slice")); } if (CoachMemorySnapshot.PreferredCoachHandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue @@ -124,7 +140,8 @@ namespace HyperTwistTrainingSubsystemInternal || CoachMemorySnapshot.ScheduleManualRescheduleEventCount > 0 || CoachMemorySnapshot.bHasQueueExecutionCompletionGap)) { - AdjustedCaseCount += 1; + Result.ResolvedCaseCount += 1; + BudgetReasons.Add(TEXT("queue-first handoff is broadening queued recovery work")); } else if (CoachMemorySnapshot.PreferredCoachHandoffKind == EHyperTwistTrainingCoachHandoffKind::FollowUp && DeckLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp @@ -132,19 +149,25 @@ namespace HyperTwistTrainingSubsystemInternal || CoachMemorySnapshot.ScheduleBlockedFollowUpEventCount > 0 || CoachMemorySnapshot.FollowUpGuidanceContinueBias > 0)) { - AdjustedCaseCount = FMath::Max(1, AdjustedCaseCount - 1); + Result.ResolvedCaseCount = FMath::Max(1, Result.ResolvedCaseCount - 1); + BudgetReasons.Add(TEXT("follow-up-first handoff is keeping the launch tighter")); } else if (CoachMemorySnapshot.PreferredCoachHandoffKind == EHyperTwistTrainingCoachHandoffKind::Recommended && DeckLane == EHyperTwistTrainingCoachGuidanceLane::Recommended - && AdjustedCaseCount <= 4 + && Result.ResolvedCaseCount <= 4 && (CoachMemorySnapshot.bHasRepeatRecommendationPressure || CoachMemorySnapshot.RecommendedGuidanceContinueBias > 0 || CoachMemorySnapshot.RecurringCoachCaseIds.Num() >= 2)) { - AdjustedCaseCount += 1; + Result.ResolvedCaseCount += 1; + BudgetReasons.Add(TEXT("recommended-first handoff is widening recommendation work")); } - return FMath::Max(1, AdjustedCaseCount); + Result.ResolvedCaseCount = FMath::Max(1, Result.ResolvedCaseCount); + Result.ReasonLine = BudgetReasons.Num() > 0 + ? FString::Join(BudgetReasons, TEXT("; ")) + : FString(); + return Result; } const FHyperTwistTrainingCoachSessionQueueStateEntry* FindNextRunnableCoachQueueEntry( @@ -201,7 +224,7 @@ namespace HyperTwistTrainingSubsystemInternal int32 MaxCases ) { - const int32 DesiredCaseCount = ResolvePreferredCoachCaseBudget( + const FResolvedCoachCaseBudget ResolvedBudget = ResolvePreferredCoachCaseBudget( MaxCases, GuidanceLaneFromQueueSourceLabel(QueueEntry.SourceLabel), CoachMemorySnapshot, @@ -244,7 +267,7 @@ namespace HyperTwistTrainingSubsystemInternal if (MatchingCase != nullptr) { SelectedCases.Add(*MatchingCase); - if (SelectedCases.Num() >= DesiredCaseCount) + if (SelectedCases.Num() >= ResolvedBudget.ResolvedCaseCount) { break; } @@ -257,7 +280,7 @@ namespace HyperTwistTrainingSubsystemInternal for (const FHyperTwistTrainingCase& Candidate : SourceDeck.Cases) { SelectedCases.Add(Candidate); - if (SelectedCases.Num() >= DesiredCaseCount) + if (SelectedCases.Num() >= ResolvedBudget.ResolvedCaseCount) { break; } @@ -424,17 +447,45 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP const FHyperTwistTrainingDeck RecommendedDeck = BuildActiveCoachRecommendedDeck(3); const FHyperTwistTrainingDeck FollowUpDeck = BuildActiveCoachFollowUpDeck(3); + const HyperTwistTrainingSubsystemInternal::FResolvedCoachCaseBudget RecommendedBudget = + HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( + 3, + EHyperTwistTrainingCoachGuidanceLane::Recommended, + ActiveCoachMemorySnapshot, + false + ); + const HyperTwistTrainingSubsystemInternal::FResolvedCoachCaseBudget FollowUpBudget = + HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( + 3, + EHyperTwistTrainingCoachGuidanceLane::FollowUp, + ActiveCoachMemorySnapshot, + false + ); PanelState.bCanStartCoachRecommendedRun = RecommendedDeck.IsStructurallyValid(); PanelState.bCanStartCoachFollowUpRun = FollowUpDeck.IsStructurallyValid(); + FHyperTwistTrainingDeck QueueLaunchDeck; + HyperTwistTrainingSubsystemInternal::FResolvedCoachCaseBudget QueueLaunchBudget; + if (NextRunnableEntry != nullptr + && NextRunnableEntry->SourceLabel != TEXT("primary-brief") + && NextRunnableEntry->SourceLabel != TEXT("follow-up-brief")) + { + QueueLaunchBudget = HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( + 3, + HyperTwistTrainingSubsystemInternal::GuidanceLaneFromQueueSourceLabel(NextRunnableEntry->SourceLabel), + ActiveCoachMemorySnapshot, + true + ); + QueueLaunchDeck = HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry( + *NextRunnableEntry, + ActiveRunState, + ActiveCoachMemorySnapshot, + 3 + ); + } PanelState.bCanStartQueuedRun = NextRunnableEntry != nullptr && ((NextRunnableEntry->SourceLabel == TEXT("primary-brief") && PanelState.bCanStartCoachRecommendedRun) || (NextRunnableEntry->SourceLabel == TEXT("follow-up-brief") && PanelState.bCanStartCoachFollowUpRun) - || HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry( - *NextRunnableEntry, - ActiveRunState, - ActiveCoachMemorySnapshot, - 3 - ).IsStructurallyValid()); + || QueueLaunchDeck.IsStructurallyValid()); const bool bPreferFollowUpGuidance = PanelState.bHasFollowUpGuidance @@ -442,10 +493,33 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP if (NextRunnableEntry != nullptr) { PanelState.Headline = NextRunnableEntry->Headline; - PanelState.SecondaryLine = HyperTwistTrainingSubsystemInternal::ResolveQueueActionLabel( + const FString ActionLabel = HyperTwistTrainingSubsystemInternal::ResolveQueueActionLabel( NextRunnableEntry->SourceLabel ); - PanelState.PrimaryActionLabel = PanelState.SecondaryLine; + if (NextRunnableEntry->SourceLabel == TEXT("primary-brief")) + { + PanelState.SuggestedLaunchCaseBudget = RecommendedDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = RecommendedBudget.ReasonLine; + } + else if (NextRunnableEntry->SourceLabel == TEXT("follow-up-brief")) + { + PanelState.SuggestedLaunchCaseBudget = FollowUpDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = FollowUpBudget.ReasonLine; + } + else + { + PanelState.SuggestedLaunchCaseBudget = QueueLaunchDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = QueueLaunchBudget.ReasonLine; + } + PanelState.SecondaryLine = PanelState.SuggestedLaunchCaseBudget > 0 + ? FString::Printf( + TEXT("%s | %s"), + *ActionLabel, + *HyperTwistTrainingSubsystemInternal::FormatLaunchBudgetLabel( + PanelState.SuggestedLaunchCaseBudget) + ) + : ActionLabel; + PanelState.PrimaryActionLabel = ActionLabel; PanelState.NextQueueSourceLabel = NextRunnableEntry->SourceLabel; PanelState.ActiveQueueEntryId = NextRunnableEntry->EntryId; PanelState.FocusDeckId = NextRunnableEntry->FocusDeckId; @@ -457,7 +531,16 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP else if (bPreferFollowUpGuidance) { PanelState.Headline = ActiveCoachFollowUpBrief.Headline; - PanelState.SecondaryLine = ActiveCoachFollowUpBrief.PrimaryActionLabel; + PanelState.SuggestedLaunchCaseBudget = FollowUpDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = FollowUpBudget.ReasonLine; + PanelState.SecondaryLine = PanelState.SuggestedLaunchCaseBudget > 0 + ? FString::Printf( + TEXT("%s | %s"), + *ActiveCoachFollowUpBrief.PrimaryActionLabel, + *HyperTwistTrainingSubsystemInternal::FormatLaunchBudgetLabel( + PanelState.SuggestedLaunchCaseBudget) + ) + : ActiveCoachFollowUpBrief.PrimaryActionLabel; PanelState.PrimaryActionLabel = ActiveCoachFollowUpBrief.PrimaryActionLabel; PanelState.FocusDeckId = ActiveCoachFollowUpBrief.FocusDeckId; PanelState.FocusMethodSegmentId = ActiveCoachFollowUpBrief.FocusMethodSegmentId; @@ -468,7 +551,16 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP else if (PanelState.bHasCoachGuidance) { PanelState.Headline = ActiveCoachBrief.Headline; - PanelState.SecondaryLine = ActiveCoachBrief.PrimaryActionLabel; + PanelState.SuggestedLaunchCaseBudget = RecommendedDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = RecommendedBudget.ReasonLine; + PanelState.SecondaryLine = PanelState.SuggestedLaunchCaseBudget > 0 + ? FString::Printf( + TEXT("%s | %s"), + *ActiveCoachBrief.PrimaryActionLabel, + *HyperTwistTrainingSubsystemInternal::FormatLaunchBudgetLabel( + PanelState.SuggestedLaunchCaseBudget) + ) + : ActiveCoachBrief.PrimaryActionLabel; PanelState.PrimaryActionLabel = ActiveCoachBrief.PrimaryActionLabel; PanelState.FocusDeckId = ActiveCoachBrief.FocusDeckId; PanelState.FocusMethodSegmentId = ActiveCoachBrief.FocusMethodSegmentId; @@ -479,7 +571,16 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP else if (PanelState.bHasFollowUpGuidance) { PanelState.Headline = ActiveCoachFollowUpBrief.Headline; - PanelState.SecondaryLine = ActiveCoachFollowUpBrief.PrimaryActionLabel; + PanelState.SuggestedLaunchCaseBudget = FollowUpDeck.Cases.Num(); + PanelState.LaunchBudgetReasonLine = FollowUpBudget.ReasonLine; + PanelState.SecondaryLine = PanelState.SuggestedLaunchCaseBudget > 0 + ? FString::Printf( + TEXT("%s | %s"), + *ActiveCoachFollowUpBrief.PrimaryActionLabel, + *HyperTwistTrainingSubsystemInternal::FormatLaunchBudgetLabel( + PanelState.SuggestedLaunchCaseBudget) + ) + : ActiveCoachFollowUpBrief.PrimaryActionLabel; PanelState.PrimaryActionLabel = ActiveCoachFollowUpBrief.PrimaryActionLabel; PanelState.FocusDeckId = ActiveCoachFollowUpBrief.FocusDeckId; PanelState.FocusMethodSegmentId = ActiveCoachFollowUpBrief.FocusMethodSegmentId; @@ -1192,7 +1293,8 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachRecommende return FHyperTwistTrainingDeck(); } - const int32 DesiredCaseCount = HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( + const HyperTwistTrainingSubsystemInternal::FResolvedCoachCaseBudget ResolvedBudget = + HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( MaxCases, EHyperTwistTrainingCoachGuidanceLane::Recommended, ActiveCoachMemorySnapshot, @@ -1210,7 +1312,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachRecommende if (MatchingCase != nullptr) { FocusCases.Add(*MatchingCase); - if (FocusCases.Num() >= DesiredCaseCount) + if (FocusCases.Num() >= ResolvedBudget.ResolvedCaseCount) { break; } @@ -1219,7 +1321,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachRecommende if (FocusCases.Num() == 0) { - const FHyperTwistTrainingDeck ReviewDeck = BuildActiveReviewDeck(DesiredCaseCount); + const FHyperTwistTrainingDeck ReviewDeck = BuildActiveReviewDeck(ResolvedBudget.ResolvedCaseCount); if (ReviewDeck.IsStructurallyValid()) { return ReviewDeck; @@ -1228,7 +1330,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachRecommende for (const FHyperTwistTrainingCase& Candidate : ActiveRunState.ActiveDeck.Cases) { FocusCases.Add(Candidate); - if (FocusCases.Num() >= DesiredCaseCount) + if (FocusCases.Num() >= ResolvedBudget.ResolvedCaseCount) { break; } @@ -1271,7 +1373,8 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachFollowUpDe return FHyperTwistTrainingDeck(); } - const int32 DesiredCaseCount = HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( + const HyperTwistTrainingSubsystemInternal::FResolvedCoachCaseBudget ResolvedBudget = + HyperTwistTrainingSubsystemInternal::ResolvePreferredCoachCaseBudget( MaxCases, EHyperTwistTrainingCoachGuidanceLane::FollowUp, ActiveCoachMemorySnapshot, @@ -1289,7 +1392,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachFollowUpDe if (MatchingCase != nullptr) { FocusCases.Add(*MatchingCase); - if (FocusCases.Num() >= DesiredCaseCount) + if (FocusCases.Num() >= ResolvedBudget.ResolvedCaseCount) { break; } @@ -1298,7 +1401,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachFollowUpDe if (FocusCases.Num() == 0) { - return BuildActiveCoachRecommendedDeck(DesiredCaseCount); + return BuildActiveCoachRecommendedDeck(ResolvedBudget.ResolvedCaseCount); } FHyperTwistTrainingDeck FocusDeck = ActiveRunState.ActiveDeck; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index c68b083..6a30c1d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -4413,6 +4413,12 @@ struct FHyperTwistTrainingCoachPanelState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 FocusCaseCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SuggestedLaunchCaseBudget = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LaunchBudgetReasonLine; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 TotalQueueEntryCount = 0;