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>
56 lines
3.3 KiB
C#
56 lines
3.3 KiB
C#
using SVSim.BattleNode.Bridge;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Matching;
|
|
|
|
/// <summary>
|
|
/// Phase 3 hardcoded fixture: one stub bot per class (1..8). <c>ai_id</c>
|
|
/// values 4001..4008 are placeholders per the existing
|
|
/// <c>docs/api-spec/endpoints/post-login/rank-battle/ai-start.md</c> TODO —
|
|
/// no live capture exists to validate the real prod catalog.
|
|
/// </summary>
|
|
public sealed class BotRoster : IBotRoster
|
|
{
|
|
// Cosmetic ids (sleeve / emblem / degree / field) intentionally use safe
|
|
// default values that match the master tables shipped in the project.
|
|
// The client-side AI catalog reads ai_id but renders cosmetics from the
|
|
// sleeveId/emblemId/etc returned here.
|
|
private static readonly IReadOnlyList<AIBotProfile> Roster = new[]
|
|
{
|
|
new AIBotProfile(AiId: 4001, CountryCode: "NONE", UserName: "Forestcraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 1, CharaId: 1, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4002, CountryCode: "NONE", UserName: "Swordcraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 2, CharaId: 2, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4003, CountryCode: "NONE", UserName: "Runecraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 3, CharaId: 3, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4004, CountryCode: "NONE", UserName: "Dragoncraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 4, CharaId: 4, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4005, CountryCode: "NONE", UserName: "Shadowcraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 5, CharaId: 5, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4006, CountryCode: "NONE", UserName: "Bloodcraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 6, CharaId: 6, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4007, CountryCode: "NONE", UserName: "Havencraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 7, CharaId: 7, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
new AIBotProfile(AiId: 4008, CountryCode: "NONE", UserName: "Portalcraft AI",
|
|
SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0,
|
|
ClassId: 8, CharaId: 8, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0),
|
|
};
|
|
|
|
public AIBotProfile Pick(MatchContext selfCtx)
|
|
{
|
|
// Deterministic: hash the ctx and pick from the roster. Same ctx →
|
|
// same bot so a mid-flight retry of /ai_<fmt>/start returns the same
|
|
// opponent (no fresh roster pick on each call).
|
|
var hash = StringComparer.Ordinal.GetHashCode(selfCtx.UserName)
|
|
^ StringComparer.Ordinal.GetHashCode(selfCtx.ClassId);
|
|
var index = (int)((uint)hash % Roster.Count);
|
|
return Roster[index];
|
|
}
|
|
}
|