diff --git a/SVSim.Bootstrap/Data/seeds/arena-two-pick-rewards.json b/SVSim.Bootstrap/Data/seeds/arena-two-pick-rewards.json index 3cc9417..6885713 100644 --- a/SVSim.Bootstrap/Data/seeds/arena-two-pick-rewards.json +++ b/SVSim.Bootstrap/Data/seeds/arena-two-pick-rewards.json @@ -10,9 +10,5 @@ { "win_count": 4, "reward_type": 4, "reward_id": 80001, "reward_num": 1 }, { "win_count": 4, "reward_type": 9, "reward_id": 0, "reward_num": 850 }, { "win_count": 5, "reward_type": 4, "reward_id": 80001, "reward_num": 1 }, - { "win_count": 5, "reward_type": 9, "reward_id": 0, "reward_num": 1000 }, - { "win_count": 6, "reward_type": 4, "reward_id": 80001, "reward_num": 1 }, - { "win_count": 6, "reward_type": 9, "reward_id": 0, "reward_num": 1250 }, - { "win_count": 7, "reward_type": 4, "reward_id": 80001, "reward_num": 2 }, - { "win_count": 7, "reward_type": 9, "reward_id": 0, "reward_num": 1500 } + { "win_count": 5, "reward_type": 9, "reward_id": 0, "reward_num": 1000 } ] diff --git a/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs b/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs index 98c6f1a..79d865d 100644 --- a/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs +++ b/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs @@ -10,7 +10,6 @@ public class ArenaTwoPickConfig { public int RewardScheduleId { get; set; } = 1; public int ChallengeId { get; set; } = 1; - public int MaxLosses { get; set; } = 2; public int ClassXpPerBattle { get; set; } = 100; public int SpotPointsPerBattle { get; set; } = 10; diff --git a/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs b/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs index 38d2e23..32d6581 100644 --- a/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs +++ b/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs @@ -171,8 +171,8 @@ public class ArenaTwoPickService : IArenaTwoPickService var rawMaxWins = await _rewards.GetMaxWinCountAsync(); if (rawMaxWins == 0) { - Console.Error.WriteLine("[ArenaTwoPickService] ArenaTwoPickRewards catalog empty; defaulting MaxBattleCount=7. Run SVSim.Bootstrap to seed."); - return 7; + Console.Error.WriteLine("[ArenaTwoPickService] ArenaTwoPickRewards catalog empty; defaulting MaxBattleCount=5. Run SVSim.Bootstrap to seed."); + return 5; } return rawMaxWins; } @@ -285,9 +285,12 @@ public class ArenaTwoPickService : IArenaTwoPickService var run = await _runs.GetByViewerIdAsync(viewerId) ?? throw new ArenaTwoPickException("arena_two_pick_no_active_run"); - var maxWins = await _rewards.GetMaxWinCountAsync(); - var aCfg = _config.Get(); - bool runOver = run.WinCount >= maxWins || run.LossCount >= aCfg.MaxLosses; + // Classic SV Take Two: run ends after MaxBattles total games played, regardless of the + // win/loss split. No separate loss cap (Worlds Beyond's 2-loss rule does not apply here). + // MaxBattles is derived from MAX(reward.WinCount), which is 5 for the live TK2 catalog. + var maxBattles = await ResolveMaxBattleCountAsync(); + int battlesPlayed = run.WinCount + run.LossCount; + bool runOver = battlesPlayed >= maxBattles; if (requireComplete && !runOver) throw new ArenaTwoPickException("arena_two_pick_run_not_complete"); @@ -339,11 +342,6 @@ public class ArenaTwoPickService : IArenaTwoPickService results.Add(isWin); run.ResultListJson = JsonSerializer.Serialize(results); if (isWin) run.WinCount += 1; else run.LossCount += 1; - - // Mark run complete if max losses reached (win-cap is handled by Finish/Retire). - if (run.LossCount >= aCfg.MaxLosses) - run.IsSelectCompleted = true; - await _runs.UpsertAsync(run); var viewer = await LoadViewerForGrantsAsync(viewerId); diff --git a/SVSim.UnitTests/Importers/ArenaTwoPickRewardImporterTests.cs b/SVSim.UnitTests/Importers/ArenaTwoPickRewardImporterTests.cs index 21500ac..3ab36b9 100644 --- a/SVSim.UnitTests/Importers/ArenaTwoPickRewardImporterTests.cs +++ b/SVSim.UnitTests/Importers/ArenaTwoPickRewardImporterTests.cs @@ -18,25 +18,26 @@ public class ArenaTwoPickRewardImporterTests } [Test] - public async Task Import_loads_all_16_rows_from_seed_file() + public async Task Import_loads_all_12_rows_from_seed_file() { await using var db = await CreateContextAsync(); var importer = new ArenaTwoPickRewardImporter(); await importer.ImportAsync(db, FindSeedDir()); + // 6 WinCount tiers (0..5) × 2 reward rows each = 12. Classic SV TK2 caps at 5 battles. var rows = await db.ArenaTwoPickRewards.OrderBy(r => r.WinCount).ThenBy(r => r.RewardType).ToListAsync(); - Assert.That(rows.Count, Is.EqualTo(16)); - Assert.That(rows.Max(r => r.WinCount), Is.EqualTo(7)); + Assert.That(rows.Count, Is.EqualTo(12)); + Assert.That(rows.Max(r => r.WinCount), Is.EqualTo(5)); var w0 = rows.Where(r => r.WinCount == 0).ToList(); Assert.That(w0.Count, Is.EqualTo(2)); Assert.That(w0.Single(r => r.RewardType == 4).RewardNum, Is.EqualTo(1)); Assert.That(w0.Single(r => r.RewardType == 9).RewardNum, Is.EqualTo(100)); - var w7 = rows.Where(r => r.WinCount == 7).ToList(); - Assert.That(w7.Single(r => r.RewardType == 4).RewardNum, Is.EqualTo(2)); - Assert.That(w7.Single(r => r.RewardType == 9).RewardNum, Is.EqualTo(1500)); + var w5 = rows.Where(r => r.WinCount == 5).ToList(); + Assert.That(w5.Single(r => r.RewardType == 4).RewardNum, Is.EqualTo(1)); + Assert.That(w5.Single(r => r.RewardType == 9).RewardNum, Is.EqualTo(1000)); } [Test] @@ -49,7 +50,7 @@ public class ArenaTwoPickRewardImporterTests await importer.ImportAsync(db, FindSeedDir()); var count = await db.ArenaTwoPickRewards.CountAsync(); - Assert.That(count, Is.EqualTo(16), "second import should upsert, not duplicate"); + Assert.That(count, Is.EqualTo(12), "second import should upsert, not duplicate"); } private static string FindSeedDir() diff --git a/SVSim.UnitTests/Repositories/ArenaTwoPickRewardRepositoryTests.cs b/SVSim.UnitTests/Repositories/ArenaTwoPickRewardRepositoryTests.cs index 74d64c0..393182d 100644 --- a/SVSim.UnitTests/Repositories/ArenaTwoPickRewardRepositoryTests.cs +++ b/SVSim.UnitTests/Repositories/ArenaTwoPickRewardRepositoryTests.cs @@ -26,7 +26,7 @@ public class ArenaTwoPickRewardRepositoryTests await using var db = await SeededContextAsync(); var repo = new ArenaTwoPickRewardRepository(db); - for (int w = 0; w <= 7; w++) + for (int w = 0; w <= 5; w++) { var rows = await repo.GetRewardsByWinCountAsync(w); Assert.That(rows.Count, Is.EqualTo(2), $"WinCount={w} should have 2 reward rows"); @@ -34,12 +34,12 @@ public class ArenaTwoPickRewardRepositoryTests } [Test] - public async Task GetMaxWinCount_returns_7() + public async Task GetMaxWinCount_returns_5() { await using var db = await SeededContextAsync(); var repo = new ArenaTwoPickRewardRepository(db); var max = await repo.GetMaxWinCountAsync(); - Assert.That(max, Is.EqualTo(7)); + Assert.That(max, Is.EqualTo(5)); } [Test] diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs index 04bc775..dba71fc 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs @@ -87,7 +87,7 @@ public class ArenaTwoPickServiceEntryTests var dto = await svc.EntryAsync(viewerId, consumeItemType: 3); Assert.That(dto.EntryInfo.Id, Is.GreaterThan(0)); - Assert.That(dto.EntryInfo.MaxBattleCount, Is.EqualTo(7)); + Assert.That(dto.EntryInfo.MaxBattleCount, Is.EqualTo(5)); Assert.That(dto.CandidateClassIds.Count, Is.EqualTo(3)); Assert.That(dto.RewardList.Count, Is.EqualTo(1)); Assert.That(dto.RewardList[0].RewardType, Is.EqualTo(4)); @@ -96,7 +96,7 @@ public class ArenaTwoPickServiceEntryTests var run = await db.ViewerArenaTwoPickRuns.FirstAsync(r => r.ViewerId == viewerId); Assert.That(run.ClassId, Is.EqualTo(0)); - Assert.That(run.MaxBattleCount, Is.EqualTo(7)); + Assert.That(run.MaxBattleCount, Is.EqualTo(5)); // Re-read viewer to verify ticket was debited. var updated = await db.Viewers.Include(v => v.Items).ThenInclude(i => i.Item).FirstAsync(v => v.Id == viewerId); diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs index 7c66301..c2bb8fc 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs @@ -128,9 +128,11 @@ public class ArenaTwoPickServiceFinishTests } [Test] - public async Task FinishAsync_at_2_losses_grants_loss_rewards_and_deletes_run() + public async Task FinishAsync_at_5_total_battles_with_0_wins_grants_loss_rewards_and_deletes_run() { - var (db, svc, vid) = await SetupWithRunAsync(winCount: 0, lossCount: 2); + // Classic Take Two: run ends after 5 total battles played, regardless of W/L split. + // 0W 5L = floor-tier reward (1 ticket + 100 rupies). + var (db, svc, vid) = await SetupWithRunAsync(winCount: 0, lossCount: 5); await using var _ = db; var dto = await svc.FinishAsync(vid); @@ -155,14 +157,17 @@ public class ArenaTwoPickServiceFinishTests } [Test] - public async Task RecordBattleResultAsync_2nd_loss_terminates_run() + public async Task RecordBattleResultAsync_increments_loss_without_terminating() { + // No 2-loss cap (that's a Worlds Beyond rule). Run termination is purely battles-played + // based and handled at Finish/Retire time, not in RecordBattleResult. var (db, svc, vid) = await SetupWithRunAsync(winCount: 0, lossCount: 1); await using var _ = db; await svc.RecordBattleResultAsync(vid, isWin: false); var run = await db.ViewerArenaTwoPickRuns.FirstAsync(); Assert.That(run.LossCount, Is.EqualTo(2)); - Assert.That(run.IsSelectCompleted, Is.True); + // Run still alive — IsSelectCompleted only flips when the 30-card draft finishes. + Assert.That(run.IsSelectCompleted, Is.True, "the test setup pre-sets isSelectCompleted=true"); } }