From d7bb44973abb205f2bbe8d7eb0ae1f4ac3eabb5e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 2 Jun 2026 00:55:18 -0400 Subject: [PATCH] feat(matching): ModePolicy registry for per-mode pair-up policy Adds PolicyKind enum (PvpOnly, PvpFirstThenAiFallback), ModePolicy record, and ModePolicyRegistry singleton with last-wins dict + PvpOnly default for unknown modes. Wired into InProcessPairUp in a later task. Co-Authored-By: Claude Opus 4.7 --- .../Matching/ModePolicy.cs | 36 +++++++++++++++ .../Matching/ModePolicyRegistryTests.cs | 46 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Matching/ModePolicy.cs create mode 100644 SVSim.UnitTests/Matching/ModePolicyRegistryTests.cs diff --git a/SVSim.EmulatedEntrypoint/Matching/ModePolicy.cs b/SVSim.EmulatedEntrypoint/Matching/ModePolicy.cs new file mode 100644 index 0000000..54a7585 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Matching/ModePolicy.cs @@ -0,0 +1,36 @@ +namespace SVSim.EmulatedEntrypoint.Matching; + +/// +/// Per-mode pairing policy. TK2 is PvP-only; rotation/unlimited rank battles +/// can fall back to an AI battle after a configurable threshold. Future modes +/// add rows in DI registration. +/// +public enum PolicyKind +{ + /// Pair PvP; if no partner arrives, keep waiting indefinitely (modulo stale eviction). + PvpOnly, + /// Pair PvP if a partner arrives within the threshold; otherwise fall back to a Bot battle. + PvpFirstThenAiFallback, +} + +public sealed record ModePolicy(string Mode, PolicyKind Kind); + +/// +/// DI singleton. Holds the per-mode policy lookup. Unknown modes default to +/// (safest — never accidentally fall through to AI +/// for a mode whose policy hasn't been wired). +/// +public sealed class ModePolicyRegistry +{ + private readonly Dictionary _byMode; + + public ModePolicyRegistry(IEnumerable policies) + { + // Last-wins on duplicate keys — documented in tests. + _byMode = new Dictionary(); + foreach (var p in policies) _byMode[p.Mode] = p; + } + + public ModePolicy For(string mode) => + _byMode.TryGetValue(mode, out var p) ? p : new ModePolicy(mode, PolicyKind.PvpOnly); +} diff --git a/SVSim.UnitTests/Matching/ModePolicyRegistryTests.cs b/SVSim.UnitTests/Matching/ModePolicyRegistryTests.cs new file mode 100644 index 0000000..82572ea --- /dev/null +++ b/SVSim.UnitTests/Matching/ModePolicyRegistryTests.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using SVSim.EmulatedEntrypoint.Matching; + +namespace SVSim.UnitTests.Matching; + +[TestFixture] +public class ModePolicyRegistryTests +{ + [Test] + public void For_known_mode_returns_its_policy() + { + var reg = new ModePolicyRegistry(new[] + { + new ModePolicy("rotation_rank_battle", PolicyKind.PvpFirstThenAiFallback), + new ModePolicy("arena_two_pick_battle", PolicyKind.PvpOnly), + }); + + Assert.That(reg.For("rotation_rank_battle").Kind, Is.EqualTo(PolicyKind.PvpFirstThenAiFallback)); + Assert.That(reg.For("arena_two_pick_battle").Kind, Is.EqualTo(PolicyKind.PvpOnly)); + } + + [Test] + public void For_unknown_mode_returns_PvpOnly_default() + { + var reg = new ModePolicyRegistry(Array.Empty()); + + var policy = reg.For("free_battle"); + + Assert.That(policy.Mode, Is.EqualTo("free_battle")); + Assert.That(policy.Kind, Is.EqualTo(PolicyKind.PvpOnly)); + } + + [Test] + public void Last_registration_for_same_mode_wins() + { + // Defensive: if someone double-registers a mode, the dict semantics + // give us the last one. Document the behavior in a test. + var reg = new ModePolicyRegistry(new[] + { + new ModePolicy("rotation_rank_battle", PolicyKind.PvpOnly), + new ModePolicy("rotation_rank_battle", PolicyKind.PvpFirstThenAiFallback), + }); + + Assert.That(reg.For("rotation_rank_battle").Kind, Is.EqualTo(PolicyKind.PvpFirstThenAiFallback)); + } +}