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

222 lines
8.2 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Vanguard.Server.Configuration;
using Vanguard.Server.Matches;
using Vanguard.Server.Matchmaking;
using Vanguard.Server.Profiles;
using Vanguard.Server.Tickets;
namespace Vanguard.Server.Tests;
public class MatchmakingServiceTests
{
[Fact]
public void Two_Players_Are_Matched_And_Get_One_Time_Tickets()
{
var harness = NewHarness();
var first = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var second = harness.Matchmaking.Enqueue("p2", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
harness.Matchmaking.Matchmake(harness.Time.GetUtcNow());
var firstStatus = harness.Matchmaking.Poll(first.QueueId, "p1");
var secondStatus = harness.Matchmaking.Poll(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 = harness.Tickets.Redeem(firstStatus.Ticket);
var secondTicket = harness.Tickets.Redeem(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 void Ranked_And_Casual_Queues_Are_Separate()
{
var harness = NewHarness();
harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var casual = harness.Matchmaking.Enqueue("p2", ServerTestData.BuildDeck(), GameMode.ClassicCasual);
harness.Matchmaking.Matchmake(harness.Time.GetUtcNow());
Assert.Equal("queued", harness.Matchmaking.Poll(casual.QueueId, "p2")!.Status);
Assert.Equal(0, harness.Registry.Count);
Assert.Equal(2, harness.Matchmaking.QueuedCount);
}
[Fact]
public void Window_Expands_While_Waiting()
{
var harness = NewHarness();
var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var initial = harness.Matchmaking.Poll(ticket.QueueId, "p1")!;
Assert.Equal(100, initial.Window);
harness.Time.Advance(TimeSpan.FromSeconds(20));
var later = harness.Matchmaking.Poll(ticket.QueueId, "p1")!;
Assert.Equal(300, later.Window); // 100 + 4 * 50
harness.Time.Advance(TimeSpan.FromSeconds(200));
var capped = harness.Matchmaking.Poll(ticket.QueueId, "p1")!;
Assert.Equal(2000, capped.Window);
}
[Fact]
public void Invalid_Deck_Is_Rejected_At_Enqueue()
{
var harness = NewHarness();
var deck = ServerTestData.BuildDeck();
deck.RemoveAt(0);
var exception = Assert.Throws<DeckRejectedException>(
() => harness.Matchmaking.Enqueue("p1", deck, GameMode.ClassicRanked));
Assert.Contains(exception.Violations, violation => violation.Code == "deck.size");
}
[Fact]
public void 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 == Vanguard.Rules.Cards.TriggerType.Heal)
continue;
for (var i = 0; i < 4 && deck.Count < 50; i++)
deck.Add(printing.PrintingId);
}
Assert.Equal(50, deck.Count);
var exception = Assert.Throws<DeckRejectedException>(
() => harness.Matchmaking.Enqueue("p1", deck, GameMode.ClassicRanked));
Assert.Contains(exception.Violations,
violation => violation.Code == "deck.starting-vanguard");
}
[Fact]
public void Cancel_Removes_From_Queue()
{
var harness = NewHarness();
var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
Assert.True(harness.Matchmaking.Cancel(ticket.QueueId, "p1"));
var status = harness.Matchmaking.Poll(ticket.QueueId, "p1")!;
Assert.Equal("cancelled", status.Status);
Assert.Equal(0, harness.Matchmaking.QueuedCount);
Assert.False(harness.Matchmaking.Cancel(ticket.QueueId, "p1"));
}
[Fact]
public void Second_Queue_Request_Replaces_The_First()
{
var harness = NewHarness();
var old = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
var current = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
Assert.Equal("cancelled", harness.Matchmaking.Poll(old.QueueId, "p1")!.Status);
Assert.Equal("queued", harness.Matchmaking.Poll(current.QueueId, "p1")!.Status);
Assert.Equal(1, harness.Matchmaking.QueuedCount);
}
[Fact]
public void Queue_Expires_After_Timeout()
{
var harness = NewHarness();
var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked);
harness.Time.Advance(TimeSpan.FromSeconds(harness.Options.QueueExpirySeconds + 1));
harness.Matchmaking.Matchmake(harness.Time.GetUtcNow());
Assert.Equal("expired", harness.Matchmaking.Poll(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"));
}
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 tickets = new GameTicketService(options, time);
var services = new ServiceCollection();
services.AddSingleton(ServerTestData.Catalog);
services.AddSingleton(options);
services.AddSingleton<TimeProvider>(time);
services.AddSingleton<IMatchResultSink>(profiles);
services.AddLogging();
var provider = services.BuildServiceProvider();
var registry = new MatchGrainRegistry(provider, options, NullLogger<MatchGrainRegistry>.Instance);
var matchmaking = new MatchmakingService(
ServerTestData.Catalog,
registry,
tickets,
profiles,
options,
time,
NullLogger<MatchmakingService>.Instance);
return new Harness(time, options, profiles, tickets, registry, matchmaking);
}
private sealed record Harness(
FakeTimeProvider Time,
GameOptions Options,
PlayerProfileStore Profiles,
GameTicketService Tickets,
MatchGrainRegistry Registry,
MatchmakingService Matchmaking);
}