feat(rank): RankProgressService — pure +100/-50 tier-floored math
Reads RankInfoEntry.LowerLimitPoint from ranks.csv (via IGlobalsRepository) for main-tier floors — no hardcoded thresholds. Master (Point=50000) and Grand Master (MP-based, floor 5000) branch handles the post-Master path. GetAsync is a read-only snapshot used later by AI-start's SelfInfo. 13 unit tests cover first-win, loss floors (0, Beginner→D, D→D0), win crossing 50000, MP awards at Master, GM promotion at MP≥5000, GM floor protecting against Master demotion, format separation, Crossover throw. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
46
SVSim.Database/Services/RankProgress/IRankProgressService.cs
Normal file
46
SVSim.Database/Services/RankProgress/IRankProgressService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.RankProgress;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire-shape result of a single rank-battle finish grant. All fields map 1:1 to
|
||||||
|
/// <c>RankBattleFinishResponseDto</c> keys the client reads via GetValueOrDefault
|
||||||
|
/// in Wizard/RankBattleFinishTask.cs:57-63.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Rank">rank_id post-grant (1..29).</param>
|
||||||
|
/// <param name="AfterBattlePoint">Point post-grant (accumulated).</param>
|
||||||
|
/// <param name="AfterMasterPoint">MasterPoint post-grant (accumulated).</param>
|
||||||
|
/// <param name="BattlePoint">Signed Point delta (+100/-50/0).</param>
|
||||||
|
/// <param name="MasterPoint">Signed MasterPoint delta.</param>
|
||||||
|
/// <param name="IsMasterRank">True iff Rank == 25.</param>
|
||||||
|
/// <param name="IsGrandMasterRank">True iff Rank >= 26.</param>
|
||||||
|
public sealed record RankProgressResult(
|
||||||
|
int Rank,
|
||||||
|
int AfterBattlePoint,
|
||||||
|
int AfterMasterPoint,
|
||||||
|
int BattlePoint,
|
||||||
|
int MasterPoint,
|
||||||
|
bool IsMasterRank,
|
||||||
|
bool IsGrandMasterRank);
|
||||||
|
|
||||||
|
public interface IRankProgressService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a +100 (win) or -50 (loss) delta to the viewer's rank progression in the
|
||||||
|
/// given format, respecting tier floors from <c>ranks.csv</c>'s LowerLimitPoint column.
|
||||||
|
/// Creates a ViewerRankProgress row for (viewer, format) if none exists. Caller must
|
||||||
|
/// have loaded the viewer with <c>.Include(v => v.RankProgress)</c> and must call
|
||||||
|
/// <c>SaveChangesAsync</c> after this returns.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">Must be Format.Rotation or Format.Unlimited.</param>
|
||||||
|
Task<RankProgressResult> GrantAsync(
|
||||||
|
Viewer viewer, Format format, bool isWin, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only current progression snapshot for (viewer, format). Returns a zero-value
|
||||||
|
/// result if no row exists. Does not mutate the viewer or hit the DB.
|
||||||
|
/// </summary>
|
||||||
|
Task<RankProgressResult> GetAsync(
|
||||||
|
Viewer viewer, Format format, CancellationToken ct = default);
|
||||||
|
}
|
||||||
154
SVSim.Database/Services/RankProgress/RankProgressService.cs
Normal file
154
SVSim.Database/Services/RankProgress/RankProgressService.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Repositories.Globals;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.RankProgress;
|
||||||
|
|
||||||
|
public sealed class RankProgressService : IRankProgressService
|
||||||
|
{
|
||||||
|
// Simple constants for v1. Promote to a [ConfigSection("RankProgress")] if per-mode
|
||||||
|
// tuning is needed later (mirror BattleXpConfig's shape).
|
||||||
|
public const int PointsPerWin = 100;
|
||||||
|
public const int PointsPerLoss = 50;
|
||||||
|
|
||||||
|
private const int MasterRankId = 25;
|
||||||
|
private const int FirstGrandMasterRankId = 26;
|
||||||
|
|
||||||
|
private readonly IGlobalsRepository _globals;
|
||||||
|
private readonly ILogger<RankProgressService> _log;
|
||||||
|
|
||||||
|
private Dictionary<int, RankInfoEntry>? _byId;
|
||||||
|
|
||||||
|
public RankProgressService(IGlobalsRepository globals, ILogger<RankProgressService> log)
|
||||||
|
{
|
||||||
|
_globals = globals;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RankProgressResult> GrantAsync(
|
||||||
|
Viewer viewer, Format format, bool isWin, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
EnsureSupportedFormat(format);
|
||||||
|
var byId = await LoadRanksAsync();
|
||||||
|
|
||||||
|
var row = viewer.RankProgress.FirstOrDefault(p => p.Format == format);
|
||||||
|
if (row is null)
|
||||||
|
{
|
||||||
|
row = new ViewerRankProgress { Format = format, Point = 0, MasterPoint = 0 };
|
||||||
|
viewer.RankProgress.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
int deltaPoint = 0;
|
||||||
|
int deltaMp = 0;
|
||||||
|
int masterFloor = byId[MasterRankId].LowerLimitPoint; // 50000
|
||||||
|
|
||||||
|
if (isWin)
|
||||||
|
{
|
||||||
|
if (row.Point < masterFloor)
|
||||||
|
{
|
||||||
|
deltaPoint = PointsPerWin;
|
||||||
|
row.Point += PointsPerWin;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deltaMp = PointsPerWin;
|
||||||
|
row.MasterPoint += PointsPerWin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (row.MasterPoint > 0)
|
||||||
|
{
|
||||||
|
// Once you're at Grand Master (rank_id >= 26), MP can never drop back to
|
||||||
|
// Master. Floor = the Master→GM0 threshold from ranks.csv (5000).
|
||||||
|
int rankBeforeDemotion = CurrentRankId(row, byId);
|
||||||
|
int mpFloor = rankBeforeDemotion >= FirstGrandMasterRankId
|
||||||
|
? byId[MasterRankId].AccumulateMasterPoint
|
||||||
|
: 0;
|
||||||
|
int newMp = Math.Max(row.MasterPoint - PointsPerLoss, mpFloor);
|
||||||
|
deltaMp = newMp - row.MasterPoint;
|
||||||
|
row.MasterPoint = newMp;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int currentRank = CurrentRankId(row, byId);
|
||||||
|
int floor = byId[currentRank].LowerLimitPoint;
|
||||||
|
int newPoint = Math.Max(row.Point - PointsPerLoss, floor);
|
||||||
|
deltaPoint = newPoint - row.Point;
|
||||||
|
row.Point = newPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int finalRank = CurrentRankId(row, byId);
|
||||||
|
return new RankProgressResult(
|
||||||
|
Rank: finalRank,
|
||||||
|
AfterBattlePoint: row.Point,
|
||||||
|
AfterMasterPoint: row.MasterPoint,
|
||||||
|
BattlePoint: deltaPoint,
|
||||||
|
MasterPoint: deltaMp,
|
||||||
|
IsMasterRank: finalRank == MasterRankId,
|
||||||
|
IsGrandMasterRank: finalRank >= FirstGrandMasterRankId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RankProgressResult> GetAsync(
|
||||||
|
Viewer viewer, Format format, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var byId = await LoadRanksAsync();
|
||||||
|
var row = viewer.RankProgress.FirstOrDefault(p => p.Format == format)
|
||||||
|
?? new ViewerRankProgress { Format = format };
|
||||||
|
int rank = CurrentRankId(row, byId);
|
||||||
|
return new RankProgressResult(
|
||||||
|
Rank: rank,
|
||||||
|
AfterBattlePoint: row.Point,
|
||||||
|
AfterMasterPoint: row.MasterPoint,
|
||||||
|
BattlePoint: 0,
|
||||||
|
MasterPoint: 0,
|
||||||
|
IsMasterRank: rank == MasterRankId,
|
||||||
|
IsGrandMasterRank: rank >= FirstGrandMasterRankId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureSupportedFormat(Format format)
|
||||||
|
{
|
||||||
|
if (format != Format.Rotation && format != Format.Unlimited)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(format),
|
||||||
|
$"RankProgressService supports only Rotation/Unlimited, got {format}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Highest rank_id whose entry conditions the viewer meets.
|
||||||
|
/// - MP >= previous rank's AccumulateMasterPoint threshold → GM tier (26..29)
|
||||||
|
/// - Point >= 50000 (MasterRankId.LowerLimitPoint) → 25 (Master)
|
||||||
|
/// - Otherwise: smallest rank_id in 1..24 where Point < AccumulatePoint.
|
||||||
|
/// </summary>
|
||||||
|
private static int CurrentRankId(ViewerRankProgress row, Dictionary<int, RankInfoEntry> byId)
|
||||||
|
{
|
||||||
|
for (int gm = 29; gm >= FirstGrandMasterRankId; gm--)
|
||||||
|
{
|
||||||
|
int threshold = byId[gm - 1].AccumulateMasterPoint;
|
||||||
|
if (threshold > 0 && row.MasterPoint >= threshold)
|
||||||
|
return gm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.Point >= byId[MasterRankId].LowerLimitPoint)
|
||||||
|
return MasterRankId;
|
||||||
|
|
||||||
|
for (int r = 1; r <= 24; r++)
|
||||||
|
{
|
||||||
|
if (row.Point < byId[r].AccumulatePoint) return r;
|
||||||
|
}
|
||||||
|
return 24; // guardrail; shouldn't be reachable.
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<int, RankInfoEntry>> LoadRanksAsync()
|
||||||
|
{
|
||||||
|
if (_byId is not null) return _byId;
|
||||||
|
var rows = await _globals.GetRankInfo();
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
_log.LogWarning("RankProgressService: RankInfo table is empty; grant will no-op.");
|
||||||
|
}
|
||||||
|
_byId = rows.ToDictionary(r => r.Id);
|
||||||
|
return _byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
245
SVSim.UnitTests/Services/RankProgressServiceTests.cs
Normal file
245
SVSim.UnitTests/Services/RankProgressServiceTests.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Repositories.Globals;
|
||||||
|
using SVSim.Database.Services.RankProgress;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.Services;
|
||||||
|
|
||||||
|
public class RankProgressServiceTests
|
||||||
|
{
|
||||||
|
// Values mirror SVSim.Bootstrap/Data/ranks.csv verbatim: RankId, NecessaryPoint,
|
||||||
|
// AccumulatePoint, LowerLimitPoint, AccumulateMasterPoint.
|
||||||
|
private static readonly (int Id, int Necessary, int Accumulate, int Lower, int AccMp)[] Ranks =
|
||||||
|
{
|
||||||
|
(1, 100, 100, 0, 0), (2, 100, 200, 0, 0),
|
||||||
|
(3, 500, 700, 0, 0), (4, 500, 1200, 0, 0),
|
||||||
|
(5, 750, 1950, 1200, 0), (6, 750, 2700, 1200, 0),
|
||||||
|
(7, 800, 3500, 1200, 0), (8, 1000, 4500, 1200, 0),
|
||||||
|
(9, 1500, 6000, 4500, 0), (10,1500, 7500, 4500, 0),
|
||||||
|
(11,1750, 9250, 4500, 0), (12,1750, 11000, 4500, 0),
|
||||||
|
(13,2000, 13000, 11000, 0), (14,2000, 15000, 11000, 0),
|
||||||
|
(15,2000, 17000, 11000, 0), (16,2000, 19000, 11000, 0),
|
||||||
|
(17,3000, 23000, 20000, 0), (18,3000, 26000, 20000, 0),
|
||||||
|
(19,3000, 29000, 20000, 0), (20,3000, 32000, 20000, 0),
|
||||||
|
(21,4000, 37000, 33000, 0), (22,4000, 42000, 33000, 0),
|
||||||
|
(23,4000, 46000, 33000, 0), (24,4000, 50000, 33000, 0),
|
||||||
|
(25, 0, 0, 50000, 5000),
|
||||||
|
(26, 0, 0, 50000, 15000),
|
||||||
|
(27, 0, 0, 50000, 25000),
|
||||||
|
(28, 0, 0, 50000, 35000),
|
||||||
|
(29, 0, 0, 50000, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IGlobalsRepository RankRepo()
|
||||||
|
{
|
||||||
|
var mock = new Mock<IGlobalsRepository>(MockBehavior.Loose);
|
||||||
|
mock.Setup(x => x.GetRankInfo()).ReturnsAsync(
|
||||||
|
Ranks.Select(r => new RankInfoEntry
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
NecessaryPoint = r.Necessary,
|
||||||
|
AccumulatePoint = r.Accumulate,
|
||||||
|
LowerLimitPoint = r.Lower,
|
||||||
|
AccumulateMasterPoint = r.AccMp,
|
||||||
|
}).ToList());
|
||||||
|
return mock.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Viewer NewViewer(Format format, int point, int masterPoint)
|
||||||
|
{
|
||||||
|
var v = new Viewer { Id = 1, DisplayName = "v" };
|
||||||
|
v.RankProgress.Add(new ViewerRankProgress
|
||||||
|
{
|
||||||
|
Format = format, Point = point, MasterPoint = masterPoint,
|
||||||
|
});
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RankProgressService NewService() =>
|
||||||
|
new(RankRepo(), NullLogger<RankProgressService>.Instance);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task First_win_from_scratch_awards_100_and_promotes_to_Beginner1()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = new Viewer { Id = 1, DisplayName = "v" };
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: true);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(2));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(100));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(100));
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.IsMasterRank, Is.False);
|
||||||
|
Assert.That(r.IsGrandMasterRank, Is.False);
|
||||||
|
var row = v.RankProgress.Single();
|
||||||
|
Assert.That(row.Format, Is.EqualTo(Format.Rotation));
|
||||||
|
Assert.That(row.Point, Is.EqualTo(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_at_0_points_floors_at_0()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 0, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(1));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_within_beginner_can_drop_between_subranks()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 100, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(1));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(50));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(-50));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_at_D_tier_floor_stays_at_D()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 1200, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(5));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(1200));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_within_D_drops_from_D1_toward_D0_and_stops_at_floor()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 1230, masterPoint: 0);
|
||||||
|
|
||||||
|
var r1 = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
Assert.That(r1.AfterBattlePoint, Is.EqualTo(1200));
|
||||||
|
Assert.That(r1.BattlePoint, Is.EqualTo(-30));
|
||||||
|
Assert.That(r1.Rank, Is.EqualTo(5));
|
||||||
|
|
||||||
|
var r2 = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
Assert.That(r2.AfterBattlePoint, Is.EqualTo(1200));
|
||||||
|
Assert.That(r2.BattlePoint, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Win_crossing_50000_lands_at_Master_with_no_MP_change()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 49950, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: true);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(25));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(50050));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(100));
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.MasterPoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.IsMasterRank, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Win_at_Master_awards_MasterPoint_not_Point()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 50000, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: true);
|
||||||
|
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.MasterPoint, Is.EqualTo(100));
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(100));
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task MP_crossing_5000_promotes_to_GrandMaster_0()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 50000, masterPoint: 4900);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: true);
|
||||||
|
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(5000));
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(26));
|
||||||
|
Assert.That(r.IsGrandMasterRank, Is.True);
|
||||||
|
Assert.That(r.IsMasterRank, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_at_GM0_floor_stays_at_5000_and_does_not_demote_to_Master()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 50000, masterPoint: 5000);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(5000));
|
||||||
|
Assert.That(r.MasterPoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(26));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Loss_at_Master_with_MP_zero_leaves_Point_at_50000()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = NewViewer(Format.Rotation, point: 50000, masterPoint: 0);
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Rotation, isWin: false);
|
||||||
|
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(50000));
|
||||||
|
Assert.That(r.BattlePoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Rotation_and_Unlimited_progressions_are_separate()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = new Viewer { Id = 1, DisplayName = "v" };
|
||||||
|
v.RankProgress.Add(new ViewerRankProgress { Format = Format.Rotation, Point = 500 });
|
||||||
|
|
||||||
|
var r = await svc.GrantAsync(v, Format.Unlimited, isWin: true);
|
||||||
|
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(100));
|
||||||
|
Assert.That(v.RankProgress.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(v.RankProgress.Single(x => x.Format == Format.Rotation).Point, Is.EqualTo(500));
|
||||||
|
Assert.That(v.RankProgress.Single(x => x.Format == Format.Unlimited).Point, Is.EqualTo(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Crossover_format_throws()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = new Viewer { Id = 1, DisplayName = "v" };
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
|
||||||
|
await svc.GrantAsync(v, Format.Crossover, isWin: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetAsync_returns_zero_when_no_row_exists()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var v = new Viewer { Id = 1, DisplayName = "v" };
|
||||||
|
|
||||||
|
var r = await svc.GetAsync(v, Format.Rotation);
|
||||||
|
|
||||||
|
Assert.That(r.Rank, Is.EqualTo(1));
|
||||||
|
Assert.That(r.AfterBattlePoint, Is.EqualTo(0));
|
||||||
|
Assert.That(r.AfterMasterPoint, Is.EqualTo(0));
|
||||||
|
Assert.That(v.RankProgress, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user