feat(tk2): weighted-group reward picking
Replaces the all-rows-granted reward model with per-group weighted pick. Each ArenaTwoPickReward row now belongs to a RewardGroup with a Weight; finish/retire groups the WinCount's rows by RewardGroup and picks exactly one row per group, weighted by Weight (excluding Weight==0). A RewardNum==0 outcome skips both the grant and the rewards[] emission. Empty WinCount catalogs emit empty arrays. Existing seed entries preserve deterministic behavior by living in single-option groups (each with weight 1). Future seasons can expand groups to multi-option for true randomized rewards (e.g. 200-280 rupies). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -310,27 +310,54 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
.ToDictionaryAsync(i => i.Id, i => i.Type);
|
||||
|
||||
var deltas = new List<TwoPickRewardReceivedDto>();
|
||||
foreach (var r in rewardRows)
|
||||
var picks = new List<SVSim.Database.Models.ArenaTwoPickReward>();
|
||||
|
||||
// Group by RewardGroup, weighted-pick one row per group (Weight==0 excluded).
|
||||
foreach (var group in rewardRows.GroupBy(r => r.RewardGroup))
|
||||
{
|
||||
var goodsType = (SVSim.Database.Enums.UserGoodsType)r.RewardType;
|
||||
await _grants.ApplyAsync(viewer, goodsType, r.RewardId, r.RewardNum);
|
||||
var pickable = group.Where(r => r.Weight > 0).ToList();
|
||||
if (pickable.Count == 0) continue;
|
||||
var pick = WeightedPick(pickable, _rng);
|
||||
picks.Add(pick);
|
||||
|
||||
// Skip when the rolled outcome is "nothing" (RewardNum == 0).
|
||||
if (pick.RewardNum <= 0) continue;
|
||||
|
||||
var goodsType = (SVSim.Database.Enums.UserGoodsType)pick.RewardType;
|
||||
await _grants.ApplyAsync(viewer, goodsType, pick.RewardId, pick.RewardNum);
|
||||
deltas.Add(new TwoPickRewardReceivedDto
|
||||
{
|
||||
RewardType = r.RewardType,
|
||||
RewardDetailId = r.RewardId,
|
||||
RewardCount = r.RewardNum,
|
||||
ItemType = itemTypeById.TryGetValue((int)r.RewardId, out var t) ? t : 0,
|
||||
IsUsable = true,
|
||||
RewardType = pick.RewardType,
|
||||
RewardDetailId = pick.RewardId,
|
||||
RewardCount = pick.RewardNum,
|
||||
ItemType = itemTypeById.TryGetValue((int)pick.RewardId, out var t) ? t : 0,
|
||||
IsUsable = true,
|
||||
});
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var postStates = ComputePostStateRewardList(rewardRows, viewer);
|
||||
// ComputePostStateRewardList reads from the picked rows only — same set the
|
||||
// grants were applied for — so the post-state list mirrors the deltas exactly.
|
||||
var postStates = ComputePostStateRewardList(picks.Where(p => p.RewardNum > 0).ToList(), viewer);
|
||||
|
||||
await _runs.DeleteAsync(viewerId);
|
||||
return new FinishResponseDto { Rewards = deltas, RewardList = postStates };
|
||||
}
|
||||
|
||||
private static SVSim.Database.Models.ArenaTwoPickReward WeightedPick(
|
||||
List<SVSim.Database.Models.ArenaTwoPickReward> rows, IRandom rng)
|
||||
{
|
||||
long total = rows.Sum(r => (long)r.Weight);
|
||||
long roll = rng.Next((int)Math.Min(total, int.MaxValue));
|
||||
long cum = 0;
|
||||
foreach (var r in rows)
|
||||
{
|
||||
cum += r.Weight;
|
||||
if (roll < cum) return r;
|
||||
}
|
||||
return rows[^1];
|
||||
}
|
||||
|
||||
private static List<RewardEntryDto> ComputePostStateRewardList(
|
||||
IReadOnlyList<SVSim.Database.Models.ArenaTwoPickReward> rows, SVSim.Database.Models.Viewer viewer)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user