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>
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>
The "Waiting for opponent" hang traced to BattleStartControl.IsReady never
flipping true. That's gated by SBattleLoad.LoadOpponentAssets which calls
ResourcesManager.LoadAssetGroupSync with the bot's
{rank, emblemId, degreeId, countryCode} — and our placeholder ids (1/1/1/"NONE")
don't resolve to any asset in the client's resource bundle, so the callback
never fires.
Replaced with the Scripted bot's known-good prod values:
- SleeveId: 704141010
- EmblemId: 400001100
- DegreeId: 120027
- FieldId: 5
- CountryCode: "JPN"
- IsOfficial: 0
These are the same ids ScriptedBotParticipant.Context uses, which we know
load fine because the TK2 Scripted flow has been working end-to-end since
Phase 2.
Reference for the load chain (decompiled client):
BattleUI.WaitForSetUp → m_SBattleLoad.WaitCallBack
→ BattleStartControl.SetUp → CheckAbleToInitialize
→ SBattleLoad.LoadOpponentAssets (SBattleLoad.cs:933)
→ ResourcesManager.LoadAssetGroupSync — hangs on missing assets
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 shipped placeholder ai_id values 4001..4008, which the client's
RankMatchAISettingList.GetSettingData() couldn't resolve — the lookup
is .First() against the rm_ai_setting.csv master table and throws
InvalidOperationException ("Sequence contains no matching element")
when the id isn't present. Surfaced on live smoke as a Unity error
during battle load:
Wizard.RankMatchAISettingDataSet.GetSettingData (System.Int32 enemyAiId)
BattleUI+<WaitForSetUp>d__9.MoveNext ()
Replaced with the series-1 enemy_ai_id per class from
data_dumps/client-assets/rm_ai_setting.csv:
1111=Forest, 1121=Sword, 1131=Rune, 1141=Dragon,
1151=Shadow, 1161=Blood, 1171=Haven, 1181=Portal
Practice mode's AI catalog (practice_ai_setting.csv) uses a different
schema keyed by (class_id, difficulty) with no enemy_ai_id field, so
practice ids aren't reusable here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>