VanguardReMaster/server/tests/Vanguard.Server.Tests/MatchmakingServiceTests.cs

279 lines
11 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Vanguard.Rules.Cards;
using Vanguard.Server.Configuration;
using Vanguard.Server.Matches;
using Vanguard.Server.Matchmaking;
using Vanguard.Server.Profiles;
using Vanguard.Server.Storage;
using Vanguard.Server.Tickets;
namespace Vanguard.Server.Tests;
public class MatchmakingServiceTests
{
[Fact]
public async Task Two_Players_Are_Matched_And_Get_One_Time_Tickets()
{
var harness = NewHarness();
var first = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var second = await harness.Matchmaking.EnqueueAsync(
"p2", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
await harness.Matchmaking.MatchmakeAsync(harness.Time.GetUtcNow());
var firstStatus = await harness.Matchmaking.PollAsync(first.QueueId, "p1");
var secondStatus = await harness.Matchmaking.PollAsync(second.QueueId, "p2");
Assert.NotNull(firstStatus);
Assert.NotNull(secondStatus);
Assert.Equal("matched", firstStatus!.Status);
Assert.Equal("matched", secondStatus!.Status);
Assert.Equal(firstStatus.MatchId, secondStatus.MatchId);
Assert.Equal(0, firstStatus.Seat);
Assert.Equal(1, secondStatus.Seat);
Assert.Equal("p2", firstStatus.Opponent);
Assert.Equal("p1", secondStatus.Opponent);
Assert.NotNull(firstStatus.Ticket);
Assert.NotNull(secondStatus.Ticket);
var firstTicket = await harness.Tickets.RedeemAsync(firstStatus.Ticket);
var secondTicket = await harness.Tickets.RedeemAsync(secondStatus.Ticket);
Assert.Equal(0, firstTicket!.Seat);
Assert.Equal(1, secondTicket!.Seat);
Assert.Equal(firstStatus.MatchId, firstTicket.MatchId);
Assert.Equal(1, harness.Registry.Count);
Assert.Equal(1, harness.Registry.ActiveCount);
Assert.Equal(0, harness.Matchmaking.QueuedCount);
}
[Fact]
public async Task Ranked_And_Casual_Queues_Are_Separate()
{
var harness = NewHarness();
await harness.Matchmaking.EnqueueAsync("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var casual = await harness.Matchmaking.EnqueueAsync(
"p2", ServerTestData.BuildDeck(), GameMode.ClassicCasual);
await harness.Matchmaking.MatchmakeAsync(harness.Time.GetUtcNow());
Assert.Equal("queued", (await harness.Matchmaking.PollAsync(casual.QueueId, "p2"))!.Status);
Assert.Equal(0, harness.Registry.Count);
Assert.Equal(2, harness.Matchmaking.QueuedCount);
}
[Fact]
public async Task Window_Expands_While_Waiting()
{
var harness = NewHarness();
var ticket = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var initial = await harness.Matchmaking.PollAsync(ticket.QueueId, "p1");
Assert.Equal(100, initial!.Window);
harness.Time.Advance(TimeSpan.FromSeconds(20));
var later = await harness.Matchmaking.PollAsync(ticket.QueueId, "p1");
Assert.Equal(300, later!.Window); // 100 + 4 * 50
harness.Time.Advance(TimeSpan.FromSeconds(200));
var capped = await harness.Matchmaking.PollAsync(ticket.QueueId, "p1");
Assert.Equal(2000, capped!.Window);
}
[Fact]
public async Task Invalid_Deck_Is_Rejected_At_Enqueue()
{
var harness = NewHarness();
var deck = ServerTestData.BuildDeck();
deck.RemoveAt(0);
var exception = await Assert.ThrowsAsync<DeckRejectedException>(
() => harness.Matchmaking.EnqueueAsync("p1", deck, GameMode.ClassicRanked));
Assert.Contains(exception.Violations, violation => violation.Code == "deck.size");
}
[Fact]
public async Task Deck_Without_Starting_Vanguard_Is_Rejected()
{
var harness = NewHarness();
var deck = new List<string>();
foreach (var printing in ServerTestData.Catalog.Printings
.Where(printing => !printing.IsParallel)
.OrderBy(printing => printing.PrintingId, StringComparer.Ordinal))
{
if (deck.Count >= 50)
break;
var card = ServerTestData.Catalog.CardOf(printing.PrintingId);
if (card.Grade == 0 || card.IsSentinel || card.Trigger == TriggerType.Heal)
continue;
for (var i = 0; i < 4 && deck.Count < 50; i++)
deck.Add(printing.PrintingId);
}
Assert.Equal(50, deck.Count);
var exception = await Assert.ThrowsAsync<DeckRejectedException>(
() => harness.Matchmaking.EnqueueAsync("p1", deck, GameMode.ClassicRanked));
Assert.Contains(exception.Violations,
violation => violation.Code == "deck.starting-vanguard");
}
[Fact]
public async Task Cancel_Removes_From_Queue()
{
var harness = NewHarness();
var ticket = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
Assert.True(await harness.Matchmaking.CancelAsync(ticket.QueueId, "p1"));
var status = await harness.Matchmaking.PollAsync(ticket.QueueId, "p1");
Assert.Equal("cancelled", status!.Status);
Assert.Equal(0, harness.Matchmaking.QueuedCount);
Assert.False(await harness.Matchmaking.CancelAsync(ticket.QueueId, "p1"));
}
[Fact]
public async Task Second_Queue_Request_Replaces_The_First()
{
var harness = NewHarness();
var old = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var current = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
Assert.Equal("cancelled", (await harness.Matchmaking.PollAsync(old.QueueId, "p1"))!.Status);
Assert.Equal("queued", (await harness.Matchmaking.PollAsync(current.QueueId, "p1"))!.Status);
Assert.Equal(1, harness.Matchmaking.QueuedCount);
}
[Fact]
public async Task Queue_Expires_After_Timeout()
{
var harness = NewHarness();
var ticket = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
harness.Time.Advance(TimeSpan.FromSeconds(harness.Options.QueueExpirySeconds + 1));
await harness.Matchmaking.MatchmakeAsync(harness.Time.GetUtcNow());
Assert.Equal("expired", (await harness.Matchmaking.PollAsync(ticket.QueueId, "p1"))!.Status);
Assert.Equal(0, harness.Matchmaking.QueuedCount);
}
[Fact]
public async Task Ranked_Result_Changes_Hidden_Mmr_Casual_Does_Not()
{
var profiles = new PlayerProfileStore();
await profiles.RecordAsync(new MatchResult(
"m1", GameMode.ClassicRanked, "winner", "loser", 0));
Assert.True(profiles.GetMmr("winner") > 1000);
Assert.True(profiles.GetMmr("loser") < 1000);
var winnerAfterRanked = profiles.GetMmr("winner");
var loserAfterRanked = profiles.GetMmr("loser");
await profiles.RecordAsync(new MatchResult(
"m2", GameMode.ClassicCasual, "winner", "loser", 0));
Assert.Equal(winnerAfterRanked, profiles.GetMmr("winner"));
Assert.Equal(loserAfterRanked, profiles.GetMmr("loser"));
}
[Fact]
public async Task Queue_Survives_Reload_From_Store()
{
var harness = NewHarness();
var ticket = await harness.Matchmaking.EnqueueAsync(
"p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
// новый процесс: очередь пуста, но записи лежат в store
var services = new ServiceCollection();
services.AddSingleton(ServerTestData.Catalog);
services.AddSingleton(harness.Options);
services.AddSingleton<TimeProvider>(harness.Time);
services.AddSingleton<IMatchResultSink>(harness.Profiles);
services.AddSingleton<IMatchStore>(harness.MatchStore);
services.AddSingleton<IQueueStore>(harness.QueueStore);
services.AddLogging();
await using var provider = services.BuildServiceProvider();
var registry = new MatchGrainRegistry(
provider, harness.Options, harness.MatchStore, NullLogger<MatchGrainRegistry>.Instance);
var matchmaking = new MatchmakingService(
ServerTestData.Catalog,
registry,
harness.Tickets,
harness.Profiles,
harness.Options,
harness.QueueStore,
harness.Time,
NullLogger<MatchmakingService>.Instance);
await matchmaking.LoadQueuedAsync();
var status = await matchmaking.PollAsync(ticket.QueueId, "p1");
Assert.NotNull(status);
Assert.Equal("queued", status!.Status);
Assert.Equal(1, matchmaking.QueuedCount);
}
private static Harness NewHarness()
{
var time = new FakeTimeProvider(new DateTimeOffset(2026, 10, 21, 12, 0, 0, TimeSpan.Zero));
var options = ServerTestData.SmallOptions();
var profiles = new PlayerProfileStore();
var ticketStore = new InMemoryTicketStore(time);
var tickets = new GameTicketService(ticketStore, options, time);
var store = new NullMatchStore();
var queueStore = new InMemoryQueueStore();
var services = new ServiceCollection();
services.AddSingleton(ServerTestData.Catalog);
services.AddSingleton(options);
services.AddSingleton<TimeProvider>(time);
services.AddSingleton<IMatchResultSink>(profiles);
services.AddSingleton<IMatchStore>(store);
services.AddSingleton<IQueueStore>(queueStore);
services.AddLogging();
var provider = services.BuildServiceProvider();
var registry = new MatchGrainRegistry(
provider, options, store, NullLogger<MatchGrainRegistry>.Instance);
var matchmaking = new MatchmakingService(
ServerTestData.Catalog,
registry,
tickets,
profiles,
options,
queueStore,
time,
NullLogger<MatchmakingService>.Instance);
return new Harness(time, options, profiles, tickets, store, queueStore, registry, matchmaking);
}
private sealed record Harness(
FakeTimeProvider Time,
GameOptions Options,
PlayerProfileStore Profiles,
GameTicketService Tickets,
IMatchStore MatchStore,
IQueueStore QueueStore,
MatchGrainRegistry Registry,
MatchmakingService Matchmaking);
}