Files
gamer147 e9af7af1b8 fix(ranked-ai): randomize bot selection and seed for AI fallback matches
Bot roster pick was hashing (UserName, ClassId) — same player always
faced the same bot class. Now hashes battleId so different matches get
different opponents while retries of the same pending battle stay
consistent.

AI start response hardcoded Seed=0 for both sides, so the client's
deck shuffle/mulligan/draw RNG was deterministic every match. The
BattleNode's per-battle MasterSeed (Random.Shared) was never sent to
bot-mode clients because InitBattleHandler skips the Matched frame.
Now populates Seed with Random.Shared.Next() on the HTTP response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 21:49:43 -04:00

54 lines
1.8 KiB
C#

using SVSim.BattleNode.Bridge;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Globals;
namespace SVSim.EmulatedEntrypoint.Matching;
/// <summary>
/// DB-backed bot roster. Reads <c>BotRoster</c> rows (seeded from
/// <c>seeds/bot-roster.json</c>) and picks one deterministically per
/// <see cref="MatchContext"/>. See <see cref="IBotRoster"/> for the contract.
/// </summary>
public sealed class BotRoster : IBotRoster
{
private readonly IGlobalsRepository _globals;
public BotRoster(IGlobalsRepository globals)
{
_globals = globals;
}
public async Task<AIBotProfile> PickAsync(MatchContext selfCtx, string battleId, CancellationToken ct = default)
{
var roster = await _globals.GetBotRoster();
if (roster.Count == 0)
{
throw new InvalidOperationException(
"BotRoster is empty. Run SVSim.Bootstrap to import seeds/bot-roster.json.");
}
// Deterministic per battle ID: same pending battle → same bot on retry,
// but different battles get different opponents.
var hash = StringComparer.Ordinal.GetHashCode(battleId);
var index = (int)((uint)hash % roster.Count);
var row = roster[index];
return ToProfile(row);
}
private static AIBotProfile ToProfile(BotRosterEntry row) => new(
AiId: row.AiId,
CountryCode: row.CountryCode,
UserName: row.UserName,
SleeveId: row.SleeveId,
EmblemId: row.EmblemId,
DegreeId: row.DegreeId,
FieldId: row.FieldId,
IsOfficial: row.IsOfficial,
ClassId: row.ClassId,
CharaId: row.CharaId,
Rank: row.Rank,
BattlePoint: row.BattlePoint,
IsMasterRank: row.IsMasterRank,
MasterPoint: row.MasterPoint);
}