feat(scoreboard): unit 2 — session lifecycle (start clears, End freezes)
ScoreboardRecorder owns the one scope the numbers promise. StartSession resets and marks running; Record ignores everything outside a session (a stale tally from the last stream is exactly the 'today' lie this design avoids); EndSession freezes the numbers but stops them moving, so an End screen can still show them. Changed fires only when a displayed value actually moves, which is what keeps the strip's raster cache honest.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
namespace ytLive.Models;
|
||||
|
||||
/// <summary>Owns the session's <see cref="ScoreboardState"/> and the one lifecycle rule that
|
||||
/// matters: a new session starts from zero.
|
||||
///
|
||||
/// <para>Kept separate from the view-model so the rule is testable without a window, and so the
|
||||
/// scoreboard layer has exactly one place that decides when counting begins.</para></summary>
|
||||
public sealed class ScoreboardRecorder
|
||||
{
|
||||
public ScoreboardState State { get; private set; } = new();
|
||||
|
||||
/// <summary>Called when a session starts (go-live, test, or a local recording). Every start
|
||||
/// clears the tallies — "session" is the only scope the numbers promise.</summary>
|
||||
public void StartSession()
|
||||
{
|
||||
State = new ScoreboardState();
|
||||
IsRunning = true;
|
||||
RaiseChanged();
|
||||
}
|
||||
|
||||
/// <summary>Folds a chat event in. Outside a session nothing is counted: a stale tally left over
|
||||
/// from the last stream would be the "today" lie this design exists to avoid.</summary>
|
||||
public void Record(ChatMessage message)
|
||||
{
|
||||
if (!IsRunning) return;
|
||||
var before = Fingerprint(State);
|
||||
State.Apply(message);
|
||||
if (Fingerprint(State) != before) RaiseChanged();
|
||||
}
|
||||
|
||||
/// <summary>True while the tallies are live.</summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
/// <summary>Marks the session finished. The tallies stay visible (the End screen may show
|
||||
/// them) but stop moving.</summary>
|
||||
public void EndSession()
|
||||
{
|
||||
IsRunning = false;
|
||||
RaiseChanged();
|
||||
}
|
||||
|
||||
public event System.Action? Changed;
|
||||
|
||||
private void RaiseChanged() => Changed?.Invoke();
|
||||
|
||||
/// <summary>Cheap change detector: only re-render the strip when a displayed value moved.</summary>
|
||||
private static string Fingerprint(ScoreboardState state) =>
|
||||
$"{state.NewMembers}|{state.MilestoneCount}|{state.LastMemberName}|{state.LastGifterName}|" +
|
||||
$"{state.LastGiftCount}|{state.LastSuperChatAuthor}|{state.LastSuperChatDisplay}|" +
|
||||
$"{state.SessionTotal.Currency}|{state.SessionTotal.Total:0.######}";
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>The session is the only scope the scoreboard promises, so the rules are: a start clears
|
||||
/// everything, events outside a session are ignored, and an ended session keeps its numbers but
|
||||
/// stops moving.</summary>
|
||||
public class ScoreboardRecorderTests
|
||||
{
|
||||
private static ChatMessage Member(string name) => new() { Kind = ChatEventKind.NewMember, AuthorName = name };
|
||||
|
||||
[Fact]
|
||||
public void Starting_a_session_clears_the_tallies_and_only_a_live_session_counts()
|
||||
{
|
||||
var recorder = new ScoreboardRecorder();
|
||||
var changes = 0;
|
||||
recorder.Changed += () => changes++;
|
||||
|
||||
// Outside a session: nothing is counted (no stale carry-over from the last stream).
|
||||
recorder.Record(Member("Ignored"));
|
||||
Assert.Equal(0, recorder.State.NewMembers);
|
||||
|
||||
recorder.StartSession();
|
||||
Assert.True(recorder.IsRunning);
|
||||
recorder.Record(Member("First"));
|
||||
recorder.Record(Member("Second"));
|
||||
Assert.Equal(2, recorder.State.NewMembers);
|
||||
Assert.True(changes >= 2, "each visible change must raise Changed so the strip re-renders");
|
||||
|
||||
// End freezes: numbers stay, movement stops.
|
||||
recorder.EndSession();
|
||||
Assert.False(recorder.IsRunning);
|
||||
recorder.Record(Member("AfterEnd"));
|
||||
Assert.Equal(2, recorder.State.NewMembers);
|
||||
|
||||
// The next session starts from zero.
|
||||
recorder.StartSession();
|
||||
Assert.Equal(0, recorder.State.NewMembers);
|
||||
Assert.Null(recorder.State.LastMemberName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user