- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy - Implement narrative presentation coverage tracking for better event visibility - Update harness scenarios to include new narrative ingestion and persistence tests - Refactor event emission to include narrative development IDs and presentations - Expand InterestCandidate to support narrative presentation models and roles
131 lines
5.2 KiB
C#
131 lines
5.2 KiB
C#
using System.Collections.Generic;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public sealed class NarrativeTelemetryEntry
|
|
{
|
|
public float At;
|
|
public string ThreadId = "";
|
|
public long ProtagonistId;
|
|
public string ProposedKey = "";
|
|
public string ActualKey = "";
|
|
public string Reason = "";
|
|
public NarrativeInterruptClass InterruptClass;
|
|
public EpisodeShotRole ShotRole;
|
|
public float CoverageDebt;
|
|
public bool ActualRelated;
|
|
}
|
|
|
|
/// <summary>Bounded in-memory trace for shadow-vs-actual story scheduling.</summary>
|
|
public static class NarrativeTelemetry
|
|
{
|
|
private const int Cap = 256;
|
|
private static readonly List<NarrativeTelemetryEntry> Entries = new List<NarrativeTelemetryEntry>(Cap);
|
|
public static int Count => Entries.Count;
|
|
public static int RelatedActualCount { get; private set; }
|
|
public static int ComparedCount { get; private set; }
|
|
public static int RelatedProposalCount { get; private set; }
|
|
public static int CriticalProposalCount { get; private set; }
|
|
public static int AlignedSampleCount { get; private set; }
|
|
public static int PermittedInterruptSampleCount { get; private set; }
|
|
public static int PermittedContinuitySampleCount { get; private set; }
|
|
public static int UnalignedSampleCount { get; private set; }
|
|
public static int UnrelatedActualCount => ComparedCount - RelatedActualCount;
|
|
|
|
public static void Clear()
|
|
{
|
|
Entries.Clear();
|
|
RelatedActualCount = 0;
|
|
ComparedCount = 0;
|
|
RelatedProposalCount = 0;
|
|
CriticalProposalCount = 0;
|
|
AlignedSampleCount = 0;
|
|
PermittedInterruptSampleCount = 0;
|
|
PermittedContinuitySampleCount = 0;
|
|
UnalignedSampleCount = 0;
|
|
}
|
|
|
|
public static void NoteProposal(EpisodePlan episode, EpisodeShotProposal proposal, float now)
|
|
{
|
|
if (episode == null || proposal?.Candidate == null) return;
|
|
if (proposal.InterruptClass == NarrativeInterruptClass.RelatedEscalation) RelatedProposalCount++;
|
|
if (proposal.InterruptClass == NarrativeInterruptClass.WorldCritical) CriticalProposalCount++;
|
|
Entries.Add(new NarrativeTelemetryEntry
|
|
{
|
|
At = now,
|
|
ThreadId = episode.ThreadId,
|
|
ProtagonistId = episode.ProtagonistId,
|
|
ProposedKey = proposal.Candidate.Key ?? "",
|
|
Reason = proposal.Reason ?? "",
|
|
InterruptClass = proposal.InterruptClass,
|
|
ShotRole = proposal.Role,
|
|
CoverageDebt = NarrativeExposureLedger.ThreadDebt(episode.ThreadId)
|
|
});
|
|
Trim();
|
|
}
|
|
|
|
public static void NoteActual(EpisodePlan episode, NarrativeThread thread, InterestCandidate actual)
|
|
{
|
|
if (episode == null || actual == null) return;
|
|
bool related = InterruptPolicy.IsRelated(actual, episode, thread);
|
|
ComparedCount++;
|
|
if (related) RelatedActualCount++;
|
|
NarrativeTelemetryEntry last = Entries.Count > 0 ? Entries[Entries.Count - 1] : null;
|
|
if (last != null && last.ThreadId == episode.ThreadId && string.IsNullOrEmpty(last.ActualKey))
|
|
{
|
|
last.ActualKey = actual.Key ?? "";
|
|
last.ActualRelated = related;
|
|
}
|
|
}
|
|
|
|
public static void NoteCurrentAlignment(
|
|
EpisodePlan episode,
|
|
NarrativeThread thread,
|
|
InterestCandidate current)
|
|
{
|
|
if (episode == null || current == null) return;
|
|
if (InterruptPolicy.IsRelated(current, episode, thread))
|
|
{
|
|
AlignedSampleCount++;
|
|
return;
|
|
}
|
|
|
|
NarrativeInterruptClass kind = InterruptPolicy.Classify(current, episode, thread);
|
|
if (InterruptPolicy.MayInterrupt(kind))
|
|
{
|
|
AlignedSampleCount++;
|
|
PermittedInterruptSampleCount++;
|
|
return;
|
|
}
|
|
|
|
// Live sticky scenes are deliberately allowed to finish their immediate outcome
|
|
// before an episode reclaims the camera; report them separately from true drift.
|
|
if (current.Completion == InterestCompletionKind.CombatActive
|
|
|| current.Completion == InterestCompletionKind.WarFront
|
|
|| current.Completion == InterestCompletionKind.PlotActive
|
|
|| current.Completion == InterestCompletionKind.StatusOutbreak)
|
|
{
|
|
AlignedSampleCount++;
|
|
PermittedContinuitySampleCount++;
|
|
return;
|
|
}
|
|
|
|
UnalignedSampleCount++;
|
|
}
|
|
|
|
public static string Summary => "compared=" + ComparedCount
|
|
+ " actualRelated=" + RelatedActualCount
|
|
+ " actualUnrelated=" + UnrelatedActualCount
|
|
+ " proposals=" + Count
|
|
+ " relatedProposals=" + RelatedProposalCount
|
|
+ " criticalProposals=" + CriticalProposalCount
|
|
+ " alignedSamples=" + AlignedSampleCount
|
|
+ " permittedInterruptSamples=" + PermittedInterruptSampleCount
|
|
+ " permittedContinuitySamples=" + PermittedContinuitySampleCount
|
|
+ " unalignedSamples=" + UnalignedSampleCount;
|
|
|
|
private static void Trim()
|
|
{
|
|
while (Entries.Count > Cap) Entries.RemoveAt(0);
|
|
}
|
|
}
|