- Добавлен асинхронный процесс аппаратной выдачи физических призов (хоппер и витрина) в GameplayHudController. - Реализована блокировка выдачи по 10-секундному таймауту датчика хоппера с гарантированной отправкой команды отключения двигателя '300'. - Реализован автоматический таймаут неактивности на экране QR-кода с последующим переходом в главное меню. - Расширен сервис статистики PrizeSessionStateService новыми долгосрочными счетчиками (сыгранные игры, очки, монеты, сбои связи COM, физ. выдачи и ошибки). - Обновлен построитель сводки статистики в меню настроек для отображения новых счетчиков и списка последних 5 сессий. - Написаны EditMode тесты для проверки инкремента счетчиков и запуска выдачи призов.
260 lines
11 KiB
C#
260 lines
11 KiB
C#
using DanroJump.Prizes;
|
|
using NUnit.Framework;
|
|
|
|
namespace DanroJump.Tests.EditMode
|
|
{
|
|
public sealed class PrizeSessionStateServiceTests
|
|
{
|
|
[Test]
|
|
public void TryBeginSession_CreatesActivePendingRecord()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
var result = new PrizeFlowResult(PrizeRewardKind.QRCode, 120, 4, 55f, "run-1");
|
|
|
|
var started = service.TryBeginSession(result, out var record);
|
|
|
|
Assert.That(started, Is.True);
|
|
Assert.That(record, Is.Not.Null);
|
|
Assert.That(record.SessionId, Is.EqualTo("run-1"));
|
|
Assert.That(record.Status, Is.EqualTo(PrizeSessionStatus.Pending));
|
|
Assert.That(service.ActiveSession, Is.SameAs(record));
|
|
Assert.That(service.History.Count, Is.EqualTo(1));
|
|
Assert.That(service.StartedCount, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void TryBeginSession_BlocksDuplicateSessionId()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
var result = new PrizeFlowResult(PrizeRewardKind.PrizeStand, 200, 2, 80f, "same-run");
|
|
|
|
Assert.That(service.TryBeginSession(result, out _), Is.True);
|
|
Assert.That(service.TryBeginSession(result, out var duplicate), Is.False);
|
|
|
|
Assert.That(duplicate.Status, Is.EqualTo(PrizeSessionStatus.DuplicateBlocked));
|
|
Assert.That(service.DuplicateBlockedCount, Is.EqualTo(1));
|
|
Assert.That(service.StartedCount, Is.EqualTo(1));
|
|
Assert.That(service.History.Count, Is.EqualTo(2));
|
|
}
|
|
|
|
[Test]
|
|
public void TryBeginSession_BlocksNewPrizeWhileAnotherSessionIsActive()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
service.TryBeginSession(new PrizeFlowResult(PrizeRewardKind.QRCode, 120, 0, 40f, "active-run"), out var active);
|
|
|
|
var started = service.TryBeginSession(
|
|
new PrizeFlowResult(PrizeRewardKind.Big, 300, 1, 100f, "next-run"),
|
|
out var blocked);
|
|
|
|
Assert.That(started, Is.False);
|
|
Assert.That(service.ActiveSession, Is.SameAs(active));
|
|
Assert.That(blocked.Status, Is.EqualTo(PrizeSessionStatus.DuplicateBlocked));
|
|
Assert.That(blocked.Note, Does.Contain("ещё активна"));
|
|
Assert.That(service.DuplicateBlockedCount, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void CompleteActiveSession_ClearsActiveSessionAndUpdatesStatistics()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
service.TryBeginSession(new PrizeFlowResult(PrizeRewardKind.Big, 300, 0, 120f, "run-2"), out _);
|
|
service.MarkActiveSessionShown();
|
|
|
|
var completed = service.CompleteActiveSession("closed");
|
|
|
|
Assert.That(completed, Is.True);
|
|
Assert.That(service.ActiveSession, Is.Null);
|
|
Assert.That(service.CompletedCount, Is.EqualTo(1));
|
|
Assert.That(service.History[0].Status, Is.EqualTo(PrizeSessionStatus.Completed));
|
|
Assert.That(service.History[0].Note, Is.EqualTo("closed"));
|
|
}
|
|
|
|
[Test]
|
|
public void TryBeginSession_BlocksSameSessionAfterCompletion()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
var result = new PrizeFlowResult(PrizeRewardKind.Hopper, 400, 0, 130f, "completed-run");
|
|
service.TryBeginSession(result, out _);
|
|
service.CompleteActiveSession("done");
|
|
|
|
var restarted = service.TryBeginSession(result, out var blocked);
|
|
|
|
Assert.That(restarted, Is.False);
|
|
Assert.That(blocked.Status, Is.EqualTo(PrizeSessionStatus.DuplicateBlocked));
|
|
Assert.That(service.ActiveSession, Is.Null);
|
|
Assert.That(service.DuplicateBlockedCount, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void FailActiveSession_ClearsActiveSessionAndUpdatesStatistics()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
service.TryBeginSession(new PrizeFlowResult(PrizeRewardKind.QRCode, 150, 1, 60f, "run-3"), out _);
|
|
|
|
var failed = service.FailActiveSession("no phone");
|
|
|
|
Assert.That(failed, Is.True);
|
|
Assert.That(service.ActiveSession, Is.Null);
|
|
Assert.That(service.FailedCount, Is.EqualTo(1));
|
|
Assert.That(service.History[0].Status, Is.EqualTo(PrizeSessionStatus.Failed));
|
|
Assert.That(service.History[0].Note, Is.EqualTo("no phone"));
|
|
}
|
|
|
|
[Test]
|
|
public void BuildSummary_IncludesCountersAndLastRecord()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
service.TryBeginSession(new PrizeFlowResult(PrizeRewardKind.Small, 90, 0, 20f, "run-4"), out _);
|
|
service.MarkActiveSessionShown();
|
|
|
|
var summary = service.BuildSummary();
|
|
|
|
Assert.That(summary, Does.Contain("начато 1"));
|
|
Assert.That(summary, Does.Contain("Активная выдача"));
|
|
Assert.That(summary, Does.Contain("малый приз"));
|
|
Assert.That(summary, Does.Contain("показана"));
|
|
}
|
|
|
|
[Test]
|
|
public void Constructor_RestoresCompletedSessionAndStillBlocksDuplicateAfterRestart()
|
|
{
|
|
var repository = new StubPrizeSessionStateRepository(new PrizeSessionStateSnapshot(
|
|
new[]
|
|
{
|
|
new PrizeSessionRecord(
|
|
"restored-run",
|
|
new PrizeFlowResult(PrizeRewardKind.Hopper, 400, 0, 120f, "restored-run"),
|
|
PrizeSessionStatus.Completed,
|
|
Utc(10),
|
|
Utc(11),
|
|
"done")
|
|
},
|
|
new[] { "restored-run" },
|
|
activeSessionId: string.Empty,
|
|
startedCount: 1,
|
|
completedCount: 1,
|
|
failedCount: 0,
|
|
duplicateBlockedCount: 0,
|
|
fallbackSessionCounter: 0));
|
|
|
|
var service = new PrizeSessionStateService(repository);
|
|
var restarted = service.TryBeginSession(
|
|
new PrizeFlowResult(PrizeRewardKind.Hopper, 400, 0, 120f, "restored-run"),
|
|
out var blocked);
|
|
|
|
Assert.That(restarted, Is.False);
|
|
Assert.That(blocked, Is.Not.Null);
|
|
Assert.That(blocked.Status, Is.EqualTo(PrizeSessionStatus.DuplicateBlocked));
|
|
Assert.That(service.DuplicateBlockedCount, Is.EqualTo(1));
|
|
Assert.That(service.History.Count, Is.EqualTo(2));
|
|
}
|
|
|
|
[Test]
|
|
public void Constructor_RecoversUnfinishedActiveSessionAsFailedAndPersistsUpdatedSnapshot()
|
|
{
|
|
var repository = new StubPrizeSessionStateRepository(new PrizeSessionStateSnapshot(
|
|
new[]
|
|
{
|
|
new PrizeSessionRecord(
|
|
"crash-run",
|
|
new PrizeFlowResult(PrizeRewardKind.QRCode, 150, 1, 60f, "crash-run"),
|
|
PrizeSessionStatus.Shown,
|
|
Utc(20),
|
|
Utc(21),
|
|
"qr opened")
|
|
},
|
|
new[] { "crash-run" },
|
|
activeSessionId: "crash-run",
|
|
startedCount: 1,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
duplicateBlockedCount: 0,
|
|
fallbackSessionCounter: 0));
|
|
|
|
var service = new PrizeSessionStateService(repository);
|
|
|
|
Assert.That(service.ActiveSession, Is.Null);
|
|
Assert.That(service.FailedCount, Is.EqualTo(1));
|
|
Assert.That(service.History.Count, Is.EqualTo(1));
|
|
Assert.That(service.History[0].Status, Is.EqualTo(PrizeSessionStatus.Failed));
|
|
Assert.That(service.History[0].Note, Does.Contain("восстановлена после перезапуска"));
|
|
Assert.That(repository.SaveCalls, Is.EqualTo(1));
|
|
Assert.That(repository.LastSavedSnapshot, Is.Not.Null);
|
|
Assert.That(repository.LastSavedSnapshot.ActiveSessionId, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void StateChanges_PersistSnapshotOnBeginAndCompletion()
|
|
{
|
|
var repository = new StubPrizeSessionStateRepository();
|
|
var service = new PrizeSessionStateService(repository);
|
|
|
|
Assert.That(service.TryBeginSession(
|
|
new PrizeFlowResult(PrizeRewardKind.Big, 320, 0, 90f, "persist-run"),
|
|
out _), Is.True);
|
|
Assert.That(service.CompleteActiveSession("done"), Is.True);
|
|
|
|
Assert.That(repository.SaveCalls, Is.GreaterThanOrEqualTo(2));
|
|
Assert.That(repository.LastSavedSnapshot, Is.Not.Null);
|
|
Assert.That(repository.LastSavedSnapshot.CompletedCount, Is.EqualTo(1));
|
|
Assert.That(repository.LastSavedSnapshot.ActiveSessionId, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void StatisticsCounters_IncrementCorrectly()
|
|
{
|
|
var service = new PrizeSessionStateService();
|
|
service.RecordGameplaySession(100, 5);
|
|
Assert.That(service.TotalGamesPlayed, Is.EqualTo(1));
|
|
Assert.That(service.TotalScore, Is.EqualTo(100));
|
|
Assert.That(service.TotalCoins, Is.EqualTo(5));
|
|
|
|
service.RecordQrShow();
|
|
Assert.That(service.QrShows, Is.EqualTo(1));
|
|
|
|
service.RecordComError();
|
|
Assert.That(service.ComErrors, Is.EqualTo(1));
|
|
|
|
service.RecordPhysicalDispense(true);
|
|
Assert.That(service.PhysicalDispenses, Is.EqualTo(1));
|
|
Assert.That(service.PhysicalDispenseFailures, Is.EqualTo(0));
|
|
|
|
service.RecordPhysicalDispense(false);
|
|
Assert.That(service.PhysicalDispenses, Is.EqualTo(1));
|
|
Assert.That(service.PhysicalDispenseFailures, Is.EqualTo(1));
|
|
}
|
|
|
|
private static System.DateTime Utc(int minutes)
|
|
{
|
|
return new System.DateTime(2026, 6, 16, 12, minutes, 0, System.DateTimeKind.Utc);
|
|
}
|
|
|
|
private sealed class StubPrizeSessionStateRepository : IPrizeSessionStateRepository
|
|
{
|
|
private readonly PrizeSessionStateSnapshot snapshot;
|
|
|
|
public StubPrizeSessionStateRepository(PrizeSessionStateSnapshot snapshot = null)
|
|
{
|
|
this.snapshot = snapshot;
|
|
}
|
|
|
|
public int SaveCalls { get; private set; }
|
|
public PrizeSessionStateSnapshot LastSavedSnapshot { get; private set; }
|
|
|
|
public bool TryLoad(out PrizeSessionStateSnapshot loadedSnapshot)
|
|
{
|
|
loadedSnapshot = snapshot;
|
|
return loadedSnapshot != null;
|
|
}
|
|
|
|
public bool Save(PrizeSessionStateSnapshot newSnapshot)
|
|
{
|
|
SaveCalls++;
|
|
LastSavedSnapshot = newSnapshot;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|