Explain handoff-aware deck shaping

This commit is contained in:
axiomlogicnexus 2026-04-27 23:03:38 +02:00
parent 382c7e5d1c
commit 393e24b40f
3 changed files with 157 additions and 36 deletions

View file

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

View file

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

View file

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