3 Commits

Author SHA1 Message Date
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
gamer147
24f9b2240e feat(matching): move BotRoster from hardcoded fixture to DB-backed seed
Phase 3 shipped the AI rank battle bot pool as a hardcoded 8-entry list
inlined in SVSim.EmulatedEntrypoint/Matching/BotRoster.cs — editing meant
recompiling. Per PLAN.md 2026-06-02 item (d), move it to a Bootstrap
importer so the roster lives in seeds/bot-roster.json and the DB.

Shape mirrors PracticeOpponent end-to-end:
- BotRosterEntry (SVSim.Database/Models) — PK = AiId via the Id passthrough
  pattern. DbSet<BotRosterEntry> BotRoster on SVSimDbContext.
- AddBotRoster migration (DDL only, per migrations-are-DDL-only rule).
- seeds/bot-roster.json — 8 rows preserving the current prod-verified
  cosmetic ids (sleeve 704141010 / emblem 400001100 / degree 120027 /
  field 5) and series-1 ai_ids from rm_ai_setting.csv (1111..1181).
- BotRosterSeed POCO + BotRosterImporter (idempotent upsert keyed by AiId,
  leaves seed-missing rows intact). Wired into SVSim.Bootstrap/Program.cs
  next to PracticeOpponentImporter.
- IGlobalsRepository.GetBotRoster() + impl.

IBotRoster.Pick → PickAsync because BotRoster now depends on the transient
IGlobalsRepository. RankBattleController awaits the new signature. The
deterministic hash-on-ctx invariant (same ctx → same bot, so /ai_<fmt>/start
retries pick the same opponent) is preserved.

DI: AddSingleton<IBotRoster> → AddTransient (matches IGlobalsRepository's
lifetime). Test fixture's SeedGlobalsAsync also runs the importer so
RankBattleControllerTests + the rewritten BotRosterTests both see seeded
rows.

Tests: 931 → 936 passing. Existing 3 BotRosterTests reshaped for the DB
backing + 1 new "throws on empty roster" guard; 4 new
BotRosterImporterTests mirror PracticeOpponentImporterTests
(round-trip / idempotent / seed-missing-row-intact / ai_id=0 skip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:58:19 -04:00
gamer147
a55187e10e feat(matching): IBotRoster + hardcoded BotRoster fixture (8 bots, one per class)
AIBotProfile carries the cosmetic metadata the AI rank-battle start
endpoint composes into oppo_info. BotRoster.Pick is deterministic per
MatchContext so mid-flight retries get the same opponent. ai_id values
4001..4008 are placeholders per the existing ai-start.md TODO — we have
no live capture of the prod catalog.

Future improvement: migrate Roster to a bot-roster.json seed under
SVSim.Bootstrap/Data/seeds/ for editability without rebuilds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:15:41 -04:00