feat(rank): wire RankProgressService into RankBattleController.Finish

Replaces the phase-3 zero stub. Real progression math now lands in the
response body (rank / after_battle_point / after_master_point /
battle_point / master_point). Format is authored by URL (not derived from
BattleContext) so /finish still works when hit without a prior
/do_matching — mirrors the client's URL-per-format Wizard/RankBattleFinishTask
dispatch (decompile lines 12-35).

3 existing tests updated with real progression assertions; 3 new tests
cover D-tier floor loss, format separation, and AI-variant persistence.
Full solution suite: 1583/1583 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 10:21:12 -04:00
parent f18cfb58b8
commit 7ff8a7bd92
3 changed files with 122 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ using SVSim.BattleNode.Bridge;
using SVSim.BattleNode.Sessions;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.EmulatedEntrypoint.Services;
using SVSim.UnitTests.Infrastructure;
@@ -246,8 +247,12 @@ public class RankBattleControllerTests
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var data = doc.RootElement;
Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1));
Assert.That(data.GetProperty("rank").GetInt32(), Is.EqualTo(0));
Assert.That(data.GetProperty("after_battle_point").GetInt32(), Is.EqualTo(0));
// First-ever win: +100 → Beginner 1 (rank_id=2), Point=100.
Assert.That(data.GetProperty("rank").GetInt32(), Is.EqualTo(2));
Assert.That(data.GetProperty("after_battle_point").GetInt32(), Is.EqualTo(100));
Assert.That(data.GetProperty("battle_point").GetInt32(), Is.EqualTo(100));
Assert.That(data.GetProperty("after_master_point").GetInt32(), Is.EqualTo(0));
Assert.That(data.GetProperty("master_point").GetInt32(), Is.EqualTo(0));
// BattleXpConfig.XpPerWin=200; classexp.csv L1=50, L2=150 → 200 XP crosses both
// thresholds: land at L3 with 0.
Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(200));
@@ -266,6 +271,11 @@ public class RankBattleControllerTests
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
Assert.That(doc.RootElement.GetProperty("battle_result").GetInt32(), Is.EqualTo(2));
// battle_result==2 is loss-shaped for rank too: fresh viewer at 0 pts stays at
// Beginner 0 (tier floor 0). No point change; +/-0 emitted.
Assert.That(doc.RootElement.GetProperty("rank").GetInt32(), Is.EqualTo(1));
Assert.That(doc.RootElement.GetProperty("after_battle_point").GetInt32(), Is.EqualTo(0));
Assert.That(doc.RootElement.GetProperty("battle_point").GetInt32(), Is.EqualTo(0));
// battle_result==2 is loss-shaped: XpPerLoss=50 exactly meets L1 threshold → L2, Exp=0.
Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(50));
Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0));
@@ -285,9 +295,76 @@ public class RankBattleControllerTests
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.Classes).ThenInclude(c => c.Class)
.Include(x => x.RankProgress)
.FirstAsync(x => x.Id == viewerId);
var cls3 = v.Classes.Single(c => c.Class.Id == 3);
Assert.That(cls3.Level, Is.EqualTo(3));
Assert.That(cls3.Exp, Is.EqualTo(0));
// Rank progress persisted under Unlimited with +100.
var rp = v.RankProgress.Single(r => r.Format == Format.Unlimited);
Assert.That(rp.Point, Is.EqualTo(100));
Assert.That(rp.MasterPoint, Is.EqualTo(0));
}
[Test]
public async Task Finish_loss_at_D_tier_stays_at_D_floor()
{
await using var factory = new SVSimTestFactory();
var viewerId = await factory.SeedViewerAsync();
// Preload viewer with Point=1200 (D0 entry) in Rotation.
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.RankProgress).FirstAsync(x => x.Id == viewerId);
v.RankProgress.Add(new ViewerRankProgress
{
Format = Format.Rotation, Point = 1200, MasterPoint = 0,
});
await db.SaveChangesAsync();
}
var client = factory.CreateAuthenticatedClient(viewerId);
var resp = await client.PostAsJsonAsync(
"/rotation_rank_battle/finish", FinishBody(battleResult: 0, classId: 1));
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var data = doc.RootElement;
Assert.That(data.GetProperty("rank").GetInt32(), Is.EqualTo(5)); // still D0
Assert.That(data.GetProperty("after_battle_point").GetInt32(), Is.EqualTo(1200)); // floored
Assert.That(data.GetProperty("battle_point").GetInt32(), Is.EqualTo(0)); // no drop
}
[Test]
public async Task Finish_rotation_and_unlimited_progress_independently()
{
await using var factory = new SVSimTestFactory();
var viewerId = await factory.SeedViewerAsync();
var client = factory.CreateAuthenticatedClient(viewerId);
await client.PostAsJsonAsync("/rotation_rank_battle/finish", FinishBody(battleResult: 1, classId: 1));
await client.PostAsJsonAsync("/rotation_rank_battle/finish", FinishBody(battleResult: 1, classId: 1));
await client.PostAsJsonAsync("/unlimited_rank_battle/finish", FinishBody(battleResult: 1, classId: 1));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.RankProgress).FirstAsync(x => x.Id == viewerId);
Assert.That(v.RankProgress.Single(r => r.Format == Format.Rotation).Point, Is.EqualTo(200));
Assert.That(v.RankProgress.Single(r => r.Format == Format.Unlimited).Point, Is.EqualTo(100));
}
[Test]
public async Task Finish_ai_variant_persists_progress_same_as_human_variant()
{
await using var factory = new SVSimTestFactory();
var viewerId = await factory.SeedViewerAsync();
var client = factory.CreateAuthenticatedClient(viewerId);
await client.PostAsJsonAsync("/ai_unlimited_rank_battle/finish", FinishBody(battleResult: 1, classId: 1));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.RankProgress).FirstAsync(x => x.Id == viewerId);
Assert.That(v.RankProgress.Single(r => r.Format == Format.Unlimited).Point, Is.EqualTo(100));
}
}