├── Rovio.MatchMaking.Net
├── .gitignore
├── MatchMakingModule.cs
├── appsettings.Development.json
├── appsettings.json
├── Properties
│ └── launchSettings.json
├── Program.cs
├── Rovio.MatchMaking.Net.csproj
└── MatchMakingController.cs
├── Rovio.MatchMaking
├── .gitignore
├── JoinSessionRequest.cs
├── Player.cs
├── Rovio.MatchMaking.csproj
├── IQueuedPlayerRepository.cs
├── ISessionRepository.cs
├── Session.cs
├── SessionPlayer.cs
└── QueuedPlayer.cs
├── Rovio.MatchMaking.Console
├── .gitignore
├── Properties
│ └── AssemblyInfo.cs
├── appsettings.json
├── appsettings.Development.json
├── Rovio.MatchMaking.Console.csproj
├── Program.cs
└── Services
│ └── SessionMatchMaker.cs
├── Rovio.MatchMaking.Tests
├── .gitignore
└── Rovio.MatchMaking.Tests.csproj
├── Rovio.MatchMaking.Console.Tests
├── .gitignore
├── Rovio.MatchMaking.Console.Tests.csproj
└── SessionMatchMakerTests.cs
├── Rovio.MatchMaking.Net.Tests
├── .gitignore
├── Rovio.MatchMaking.Net.Tests.csproj
└── MatchMakingControllerTests.cs
├── Rovio.MatchMaking.Repositories
├── .gitignore
├── appsettings.json
├── appsettings.Development.json
├── Data
│ ├── Configurations
│ │ ├── QueuedPlayerConfiguration.cs
│ │ ├── SessionPlayerConfiguration.cs
│ │ └── SessionConfiguration.cs
│ └── AppDbContext.cs
├── AppDbContextFactory.cs
├── Rovio.MatchMaking.Repositories.csproj
├── QueuedPlayerRepository.cs
├── Migrations
│ ├── 20240924173219_InitialCreate.cs
│ ├── AppDbContextModelSnapshot.cs
│ └── 20240924173219_InitialCreate.Designer.cs
└── SessionRepository.cs
├── Rovio.Matchmaking.Repositories.Tests
├── .gitignore
├── Rovio.Matchmaking.Repositories.Tests.csproj
├── AppDbContextIntegrationTests.cs
├── QueuedPlayerRepositoryTests.cs
└── SessionRepositoryTests.cs
├── assets
├── logo.webp
├── logo2.webp
├── matchmaking-logo.png
└── rovio-task_v2.drawio.png
├── ServerEngineerTest.sln
└── README.md
/Rovio.MatchMaking.Net/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/.gitignore:
--------------------------------------------------------------------------------
1 | obj
2 | bin
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Tests/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console.Tests/.gitignore:
--------------------------------------------------------------------------------
1 | obj
2 | bin
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net.Tests/.gitignore:
--------------------------------------------------------------------------------
1 | obj
2 | bin
3 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/.gitignore:
--------------------------------------------------------------------------------
1 | obj
2 | bin
3 |
--------------------------------------------------------------------------------
/Rovio.Matchmaking.Repositories.Tests/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 |
--------------------------------------------------------------------------------
/assets/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadsf7293/group-matchmaking/HEAD/assets/logo.webp
--------------------------------------------------------------------------------
/assets/logo2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadsf7293/group-matchmaking/HEAD/assets/logo2.webp
--------------------------------------------------------------------------------
/assets/matchmaking-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadsf7293/group-matchmaking/HEAD/assets/matchmaking-logo.png
--------------------------------------------------------------------------------
/assets/rovio-task_v2.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadsf7293/group-matchmaking/HEAD/assets/rovio-task_v2.drawio.png
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Rovio.MatchMaking.Console.Tests")]
--------------------------------------------------------------------------------
/Rovio.MatchMaking/JoinSessionRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Rovio.MatchMaking;
2 |
3 | public class JoinSessionRequest
4 | {
5 | public Guid PlayerId { get; set; }
6 | public Guid SessionId { get; set; }
7 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking/Player.cs:
--------------------------------------------------------------------------------
1 | namespace Rovio.MatchMaking;
2 |
3 | public class Player
4 | {
5 | public Guid Id { get; set; }
6 | public string Name { get; set; }
7 | public int LatencyMilliseconds { get; set; }
8 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/MatchMakingModule.cs:
--------------------------------------------------------------------------------
1 | using Autofac;
2 |
3 | namespace Rovio.MatchMaking.Net;
4 |
5 | public class MatchMakingModule : Module
6 | {
7 | protected override void Load(ContainerBuilder builder)
8 | {
9 | // Register dependencies here
10 | }
11 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking/Rovio.MatchMaking.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "DefaultConnection": "Server=localhost;Port=2306;Database=MatchMakingDb;User=root;Password=strong_password;"
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking/IQueuedPlayerRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Rovio.MatchMaking;
2 |
3 | public interface IQueuedPlayerRepository
4 | {
5 | Task> GetAllQueuedPlayersAsync();
6 | Task GetQueuedPlayerByIdAsync(Guid id);
7 | Task GetQueuedPlayerByPlayerIdAsync(Guid playerId);
8 | Task CreateQueuedPlayerAsync(QueuedPlayer queuedPlayer);
9 | Task UpdateQueuedPlayerAsync(QueuedPlayer queuedPlayer);
10 | Task DeleteQueuedPlayerAsync(Guid id);
11 | }
12 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking/ISessionRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Rovio.MatchMaking;
2 |
3 | public interface ISessionRepository
4 | {
5 | Task> GetAllActiveSessionsAsync();
6 | Task CreateNewAsync(int latencyLevel, int joinedCount, int gameTimeInMinutes);
7 | Task GetSessionById(Guid sessionId);
8 | Task> GetSessionPlayersBySessionId(Guid sessionId);
9 | Task AddPlayerToSessionAsync(Guid sessionId, Guid playerId);
10 | Task RemovePlayerFromSessionAsync(Guid sessionId, Guid playerId);
11 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Data/Configurations/QueuedPlayerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
3 |
4 | namespace Rovio.MatchMaking.Repositories.Data.Configurations
5 | {
6 | class QueuedPlayerConfiguration : IEntityTypeConfiguration
7 | {
8 | public void Configure(EntityTypeBuilder entity)
9 | {
10 | entity.Property(e => e.Id)
11 | .HasColumnType("char(36)");
12 | entity.Property(e => e.PlayerId)
13 | .HasColumnType("char(36)");
14 | entity.HasKey(e => e.Id); //Primary key
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Data/AppDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Rovio.MatchMaking.Repositories.Data.Configurations;
3 |
4 | namespace Rovio.MatchMaking.Repositories.Data
5 | {
6 | public class AppDbContext : DbContext
7 | {
8 | public AppDbContext(DbContextOptions options) : base(options) { }
9 |
10 | public DbSet Sessions { get; set; }
11 | public DbSet QueuedPlayers { get; set; }
12 | public DbSet SessionPlayers { get; set; }
13 |
14 | protected override void OnModelCreating(ModelBuilder modelBuilder)
15 | {
16 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
17 | base.OnModelCreating(modelBuilder);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking/Session.cs:
--------------------------------------------------------------------------------
1 | public class Session
2 | {
3 | public Guid Id { get; set; } // Unique identifier for each session
4 |
5 | public int LatencyLevel { get; set; } // Latency level of the player (1 to 5)
6 |
7 | private int _joinedCount;
8 | public int JoinedCount
9 | {
10 | get => _joinedCount;
11 | set
12 | {
13 | if (value < 0 || value > 10)
14 | {
15 | throw new ArgumentOutOfRangeException(nameof(JoinedCount), "JoinedCount must be between 0 and 10.");
16 | }
17 | _joinedCount = value;
18 | }
19 | } // Number of players joined the session (max 10)
20 |
21 | public DateTime CreatedAt { get; set; }
22 |
23 | public DateTime StartsAt { get; set; } // Timestamp when the contest will start
24 |
25 | public DateTime EndsAt { get; set; } // Timestamp when the contest will end
26 | }
27 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking/SessionPlayer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Rovio.MatchMaking
4 | {
5 | public class SessionPlayer
6 | {
7 | public Guid Id { get; set; } // Unique identifier for each session-player relationship
8 |
9 | public Guid SessionId { get; set; } // ID of the session the player is attending
10 |
11 | public Guid PlayerId { get; set; } // ID of the player attending the session
12 |
13 | public string Status { get; set; } = "ATTENDED"; // Status of the player in the session (default: ATTENDED)
14 |
15 | public int Score { get; set; } = 0; // Score of the player in the contest (default: 0)
16 |
17 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // Timestamp when the record was created
18 |
19 | public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // Timestamp when the record was last updated
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Data/Configurations/SessionPlayerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
3 |
4 | namespace Rovio.MatchMaking.Repositories.Data.Configurations
5 | {
6 | class SessionPlayerConfiguration : IEntityTypeConfiguration
7 | {
8 | public void Configure(EntityTypeBuilder entity)
9 | {
10 | entity.HasKey(e => e.Id); // Primary Key
11 |
12 | entity.Property(e => e.SessionId)
13 | .IsRequired(); // Foreign key to Session
14 |
15 | entity.Property(e => e.PlayerId)
16 | .IsRequired(); // Foreign key to Player
17 |
18 | entity.Property(e => e.Status)
19 | .IsRequired()
20 | .HasDefaultValue("ATTENDED"); // Default Status
21 |
22 | entity.Property(e => e.Score)
23 | .IsRequired()
24 | .HasDefaultValue(0);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/Rovio.MatchMaking.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Exe
18 | net8.0
19 | enable
20 | enable
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Data/Configurations/SessionConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
3 |
4 | namespace Rovio.MatchMaking.Repositories.Data.Configurations
5 | {
6 | class SessionConfiguration : IEntityTypeConfiguration
7 | {
8 | public void Configure(EntityTypeBuilder entity)
9 | {
10 | entity.Property(e => e.Id).HasColumnType("char(36)");
11 | entity.Property(e => e.LatencyLevel)
12 | .IsRequired()
13 | .HasDefaultValue(1)
14 | .HasAnnotation("SqlServer:Check", "LatencyLevel >= 1");
15 |
16 | entity.Property(e => e.JoinedCount)
17 | .HasDefaultValue(0)
18 | .HasAnnotation("SqlServer:Check", "JoinedCount <= 10");
19 |
20 | entity.Property(e => e.StartsAt)
21 | .IsRequired();
22 |
23 | entity.Property(e => e.EndsAt)
24 | .IsRequired();
25 | entity.HasKey(e => e.Id); //Primary key
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/AppDbContextFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Design;
3 | using Microsoft.Extensions.Configuration;
4 | using System.IO;
5 |
6 | namespace Rovio.MatchMaking.Repositories.Data
7 | {
8 | public class AppDbContextFactory : IDesignTimeDbContextFactory
9 | {
10 | public AppDbContext CreateDbContext(string[] args)
11 | {
12 | // Build configuration
13 | IConfigurationRoot configuration = new ConfigurationBuilder()
14 | .SetBasePath(Directory.GetCurrentDirectory())
15 | .AddJsonFile("appsettings.json")
16 | .Build();
17 |
18 | // Set up the DbContext options using the connection string from appsettings.json
19 | var builder = new DbContextOptionsBuilder();
20 | var connectionString = configuration.GetConnectionString("DefaultConnection");
21 |
22 | // Use the connection string for MySQL
23 | builder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 21)));
24 |
25 | return new AppDbContext(builder.Options);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console.Tests/Rovio.MatchMaking.Console.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:6364",
8 | "sslPort": 44307
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "http://localhost:5156",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "applicationUrl": "https://localhost:7060;http://localhost:5156",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": true,
35 | "launchUrl": "swagger",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Rovio.MatchMaking.Repositories.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/Program.cs:
--------------------------------------------------------------------------------
1 | using Autofac;
2 | using Autofac.Extensions.DependencyInjection;
3 | using Microsoft.EntityFrameworkCore;
4 | using Rovio.MatchMaking;
5 | using Rovio.MatchMaking.Net;
6 | using Rovio.MatchMaking.Repositories;
7 | using Rovio.MatchMaking.Repositories.Data;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 |
11 | // Register the DbContext with DI
12 | builder.Services.AddDbContext(options =>
13 | options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"),
14 | new MySqlServerVersion(new Version(8, 0, 21))));
15 |
16 | builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
17 | .ConfigureContainer(b => { b.RegisterModule(); });
18 |
19 | builder.Services.AddEndpointsApiExplorer();
20 | builder.Services.AddSwaggerGen();
21 |
22 | builder.Services.AddControllers();
23 | builder.Services.AddSingleton();
24 | builder.Services.AddScoped();
25 |
26 |
27 | builder.Services.AddHttpContextAccessor();
28 |
29 | var app = builder.Build();
30 |
31 | if (app.Environment.IsDevelopment())
32 | {
33 | app.UseSwagger();
34 | app.UseSwaggerUI();
35 | }
36 |
37 | app.MapControllers();
38 | app.Run("http://localhost:5003");
--------------------------------------------------------------------------------
/Rovio.MatchMaking/QueuedPlayer.cs:
--------------------------------------------------------------------------------
1 | using Rovio.MatchMaking;
2 | public class QueuedPlayer
3 | {
4 | public Guid Id { get; set; } // Unique identifier for each queued request
5 | public Guid PlayerId { get; set; } // ID of the player requesting to join a session
6 | public int LatencyLevel { get; set; } // Latency level of the player (1 to 5)
7 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // Timestamp when the record was created
8 |
9 | public static QueuedPlayer CreateQueuedPlayerFromPlayer(Player player)
10 | {
11 | return new QueuedPlayer
12 | {
13 | Id = Guid.NewGuid(), // Assign a new unique ID
14 | PlayerId = player.Id, // Reference the player's ID
15 | LatencyLevel = ConvertLatencyToLevel(player.LatencyMilliseconds), // Convert latency to a level
16 | CreatedAt = DateTime.UtcNow
17 | };
18 | }
19 |
20 | // Utility method to convert latency to a level (1 to 5)
21 | private static int ConvertLatencyToLevel(int latencyMilliseconds)
22 | {
23 | if (latencyMilliseconds <= 100) return 1; // Best latency
24 | if (latencyMilliseconds <= 200) return 2;
25 | if (latencyMilliseconds <= 300) return 3;
26 | if (latencyMilliseconds <= 400) return 4;
27 | return 5; // Worst latency
28 | }
29 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Tests/Rovio.MatchMaking.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 | all
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/Rovio.MatchMaking.Net.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Rovio.Matchmaking.Repositories.Tests/Rovio.Matchmaking.Repositories.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 | all
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net.Tests/Rovio.MatchMaking.Net.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 | all
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/QueuedPlayerRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Rovio.MatchMaking;
3 | using Rovio.MatchMaking.Repositories.Data;
4 |
5 | public class QueuedPlayerRepository : IQueuedPlayerRepository
6 | {
7 | private readonly AppDbContext _context;
8 |
9 | public QueuedPlayerRepository(AppDbContext context)
10 | {
11 | _context = context;
12 | }
13 |
14 | public async Task> GetAllQueuedPlayersAsync()
15 | {
16 | return await _context.QueuedPlayers.ToListAsync();
17 | }
18 |
19 | public async Task GetQueuedPlayerByIdAsync(Guid id)
20 | {
21 | return await _context.QueuedPlayers.FindAsync(id);
22 | }
23 |
24 | public async Task GetQueuedPlayerByPlayerIdAsync(Guid playerId)
25 | {
26 | return await _context.QueuedPlayers.FirstOrDefaultAsync(qp => qp.PlayerId == playerId);
27 | }
28 |
29 | public async Task CreateQueuedPlayerAsync(QueuedPlayer queuedPlayer)
30 | {
31 | queuedPlayer.Id = Guid.NewGuid(); // Assign a new unique ID
32 | _context.QueuedPlayers.Add(queuedPlayer);
33 | await _context.SaveChangesAsync();
34 | return queuedPlayer;
35 | }
36 |
37 | public async Task UpdateQueuedPlayerAsync(QueuedPlayer queuedPlayer)
38 | {
39 | _context.QueuedPlayers.Update(queuedPlayer);
40 | await _context.SaveChangesAsync();
41 | }
42 |
43 | public async Task DeleteQueuedPlayerAsync(Guid id)
44 | {
45 | var queuedPlayer = await _context.QueuedPlayers.FindAsync(id);
46 | if (queuedPlayer != null)
47 | {
48 | _context.QueuedPlayers.Remove(queuedPlayer);
49 | await _context.SaveChangesAsync();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Hosting;
8 | using Rovio.MatchMaking.Console.Services;
9 | using Rovio.MatchMaking.Repositories;
10 | using Rovio.MatchMaking.Repositories.Data;
11 |
12 | namespace Rovio.MatchMaking.Console
13 | {
14 | class Program
15 | {
16 | static async Task Main(string[] args)
17 | {
18 | // Build configuration
19 | IConfigurationRoot configuration = new ConfigurationBuilder()
20 | .SetBasePath(Directory.GetCurrentDirectory())
21 | .AddJsonFile("appsettings.json")
22 | .Build();
23 |
24 | // Create a Host to manage dependency injection and configuration
25 | var host = CreateHostBuilder(args, configuration).Build(); // Pass configuration as parameter
26 |
27 | // Use the host's service provider to get the SessionMatchMaker service and run it
28 | var matchmaker = host.Services.GetRequiredService();
29 |
30 | try
31 | {
32 | // Execute the matchmaking process
33 | await matchmaker.RunAsync();
34 | }
35 | catch (Exception ex)
36 | {
37 | // Handle any unhandled exceptions
38 | System.Console.WriteLine($"An unexpected error occurred: {ex.Message}");
39 | Environment.Exit(1); // Exit with error code 1
40 | }
41 |
42 | System.Console.WriteLine("Matchmaking process completed successfully.");
43 | }
44 |
45 | // Configure the host builder and services
46 | static IHostBuilder CreateHostBuilder(string[] args, IConfiguration configuration) =>
47 | Host.CreateDefaultBuilder(args)
48 | .ConfigureServices((context, services) =>
49 | {
50 | // Configure DbContext with connection string
51 | var connectionString = configuration.GetConnectionString("DefaultConnection");
52 | services.AddDbContext(options =>
53 | options.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 21))));
54 |
55 | // Register repositories and services
56 | services.AddScoped();
57 | services.AddScoped();
58 | services.AddScoped();
59 |
60 | // Add additional services or configurations if needed
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/ServerEngineerTest.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.MatchMaking", "Rovio.MatchMaking\Rovio.MatchMaking.csproj", "{867764AC-A86C-4071-A0A7-DBE3A838C3D7}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.MatchMaking.Net", "Rovio.MatchMaking.Net\Rovio.MatchMaking.Net.csproj", "{7B577BFF-124C-4661-9804-BD5E595FE808}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.MatchMaking.Tests", "Rovio.MatchMaking.Tests\Rovio.MatchMaking.Tests.csproj", "{AC9C32AD-5FAC-4215-BC33-BAFB10346946}"
8 | EndProject
9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.MatchMaking.Repositories", "Rovio.MatchMaking.Repositories\Rovio.MatchMaking.Repositories.csproj", "{05307819-C957-4F18-9B27-0D61297D2F82}"
10 | EndProject
11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.Matchmaking.Repositories.Tests", "Rovio.Matchmaking.Repositories.Tests\Rovio.Matchmaking.Repositories.Tests.csproj", "{79E3CBF4-DB48-42D6-8470-A1D1A32FD6ED}"
12 | EndProject
13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rovio.MatchMaking.Net.Tests", "Rovio.MatchMaking.Net.Tests\Rovio.MatchMaking.Net.Tests.csproj", "{32AC942D-2E1D-4024-A7DD-68872E8F8042}"
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Release|Any CPU = Release|Any CPU
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {867764AC-A86C-4071-A0A7-DBE3A838C3D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {867764AC-A86C-4071-A0A7-DBE3A838C3D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {867764AC-A86C-4071-A0A7-DBE3A838C3D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {867764AC-A86C-4071-A0A7-DBE3A838C3D7}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {7B577BFF-124C-4661-9804-BD5E595FE808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {7B577BFF-124C-4661-9804-BD5E595FE808}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {7B577BFF-124C-4661-9804-BD5E595FE808}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {7B577BFF-124C-4661-9804-BD5E595FE808}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {AC9C32AD-5FAC-4215-BC33-BAFB10346946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {AC9C32AD-5FAC-4215-BC33-BAFB10346946}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {AC9C32AD-5FAC-4215-BC33-BAFB10346946}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {AC9C32AD-5FAC-4215-BC33-BAFB10346946}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {05307819-C957-4F18-9B27-0D61297D2F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {05307819-C957-4F18-9B27-0D61297D2F82}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {05307819-C957-4F18-9B27-0D61297D2F82}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {05307819-C957-4F18-9B27-0D61297D2F82}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {79E3CBF4-DB48-42D6-8470-A1D1A32FD6ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {79E3CBF4-DB48-42D6-8470-A1D1A32FD6ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {79E3CBF4-DB48-42D6-8470-A1D1A32FD6ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {79E3CBF4-DB48-42D6-8470-A1D1A32FD6ED}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {32AC942D-2E1D-4024-A7DD-68872E8F8042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {32AC942D-2E1D-4024-A7DD-68872E8F8042}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {32AC942D-2E1D-4024-A7DD-68872E8F8042}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {32AC942D-2E1D-4024-A7DD-68872E8F8042}.Release|Any CPU.Build.0 = Release|Any CPU
45 | EndGlobalSection
46 | EndGlobal
47 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Migrations/20240924173219_InitialCreate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace Rovio.MatchMaking.Repositories.Migrations
7 | {
8 | ///
9 | public partial class InitialCreate : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.AlterDatabase()
15 | .Annotation("MySql:CharSet", "utf8mb4");
16 |
17 | migrationBuilder.CreateTable(
18 | name: "QueuedPlayers",
19 | columns: table => new
20 | {
21 | Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
22 | PlayerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
23 | LatencyLevel = table.Column(type: "int", nullable: false),
24 | CreatedAt = table.Column(type: "datetime(6)", nullable: false)
25 | },
26 | constraints: table =>
27 | {
28 | table.PrimaryKey("PK_QueuedPlayers", x => x.Id);
29 | })
30 | .Annotation("MySql:CharSet", "utf8mb4");
31 |
32 | migrationBuilder.CreateTable(
33 | name: "SessionPlayers",
34 | columns: table => new
35 | {
36 | Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
37 | SessionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
38 | PlayerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
39 | Status = table.Column(type: "longtext", nullable: false, defaultValue: "ATTENDED")
40 | .Annotation("MySql:CharSet", "utf8mb4"),
41 | Score = table.Column(type: "int", nullable: false, defaultValue: 0),
42 | CreatedAt = table.Column(type: "datetime(6)", nullable: false),
43 | UpdatedAt = table.Column(type: "datetime(6)", nullable: false)
44 | },
45 | constraints: table =>
46 | {
47 | table.PrimaryKey("PK_SessionPlayers", x => x.Id);
48 | })
49 | .Annotation("MySql:CharSet", "utf8mb4");
50 |
51 | migrationBuilder.CreateTable(
52 | name: "Sessions",
53 | columns: table => new
54 | {
55 | Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
56 | LatencyLevel = table.Column(type: "int", nullable: false, defaultValue: 1),
57 | JoinedCount = table.Column(type: "int", nullable: false, defaultValue: 0),
58 | CreatedAt = table.Column(type: "datetime(6)", nullable: false),
59 | StartsAt = table.Column(type: "datetime(6)", nullable: false),
60 | EndsAt = table.Column(type: "datetime(6)", nullable: false)
61 | },
62 | constraints: table =>
63 | {
64 | table.PrimaryKey("PK_Sessions", x => x.Id);
65 | })
66 | .Annotation("MySql:CharSet", "utf8mb4");
67 | }
68 |
69 | ///
70 | protected override void Down(MigrationBuilder migrationBuilder)
71 | {
72 | migrationBuilder.DropTable(
73 | name: "QueuedPlayers");
74 |
75 | migrationBuilder.DropTable(
76 | name: "SessionPlayers");
77 |
78 | migrationBuilder.DropTable(
79 | name: "Sessions");
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net/MatchMakingController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Rovio.MatchMaking;
5 | namespace Rovio.MatchMaking.Net;
6 |
7 | [Route("match")]
8 | public class MatchMakingController : Controller
9 | {
10 | // Assuming there's already an injected service/repository to handle queuing
11 | private readonly IQueuedPlayerRepository _queuedPlayerRepository;
12 | private readonly ISessionRepository _sessionRepository;
13 | private IQueuedPlayerRepository @object;
14 |
15 | public MatchMakingController(IQueuedPlayerRepository queuedPlayerRepository, ISessionRepository sessionRepository)
16 | {
17 | _queuedPlayerRepository = queuedPlayerRepository;
18 | _sessionRepository = sessionRepository;
19 | }
20 |
21 | [HttpPost("queue")]
22 | public async Task QueuePlayerAsync([FromBody] Player player)
23 | {
24 | // throw new NotImplementedException();
25 | if (player == null)
26 | {
27 | return BadRequest(new { error = "Player cannot be null" });
28 | }
29 |
30 | // Check if the player is already queued
31 | var existingQueuedPlayer = await _queuedPlayerRepository.GetQueuedPlayerByPlayerIdAsync(player.Id);
32 | if (existingQueuedPlayer != null)
33 | {
34 | return BadRequest(new { error = "Player is already queued" });
35 | }
36 |
37 | // Log Player properties to the console
38 | Console.WriteLine($"Player Queued: ID = {player.Id}, Name = {player.Name}, Latency = {player.LatencyMilliseconds}");
39 |
40 | var queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
41 | var createdQueuedPlayer = await _queuedPlayerRepository.CreateQueuedPlayerAsync(queuedPlayer);
42 | // Assuming the sessionFactory handles the logic for queuing a player
43 | // await _sessionFactory.QueuePlayerAsync(player);
44 | // await _sessionFactory.Create();
45 |
46 | return Ok(new { message = "Player queued successfully" });
47 | }
48 |
49 | [HttpPost("dequeue")]
50 | public async Task DequeuePlayerAsync([FromBody] Player player)
51 | {
52 | if (player == null)
53 | {
54 | return BadRequest(new { error = "Player cannot be null" });
55 | }
56 |
57 | // Check if the player is already queued
58 | var queuedPlayer = await _queuedPlayerRepository.GetQueuedPlayerByPlayerIdAsync(player.Id);
59 | if (queuedPlayer == null)
60 | {
61 | return BadRequest(new { error = "Player is not queued" });
62 | }
63 |
64 | await _queuedPlayerRepository.DeleteQueuedPlayerAsync(queuedPlayer.Id);
65 |
66 | return Ok(new { message = "Player dequeued successfully" });
67 | }
68 |
69 | [HttpPost("join")]
70 | public async Task JoinSession([FromBody] JoinSessionRequest requestBody)
71 | {
72 | if (requestBody == null)
73 | {
74 | return BadRequest(new { error = "Request body cannot be null" });
75 | }
76 |
77 | var session = await _sessionRepository.GetSessionById(requestBody.SessionId);
78 | if (session == null) {
79 | return BadRequest(new { error = "Invalid Session" });
80 | }
81 |
82 | // if (session.StartsAt > DateTime.UtcNow) {
83 | // return BadRequest(new { error = "Session hasn't been started yet!" });
84 | // }
85 |
86 | var sessionPlayers = await _sessionRepository.GetSessionPlayersBySessionId(requestBody.SessionId);
87 | List playerIdsList = new List();
88 | foreach (var sp in sessionPlayers) {
89 | playerIdsList.Add(sp.PlayerId);
90 | }
91 | if (!playerIdsList.Contains(requestBody.PlayerId)) {
92 | return BadRequest(new { error = "You don't have permission to join this session" });
93 | }
94 |
95 | return Ok(new { session = session, sessionPlayers = sessionPlayers });
96 | }
97 | }
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Migrations/AppDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Rovio.MatchMaking.Repositories.Data;
8 |
9 | #nullable disable
10 |
11 | namespace Rovio.MatchMaking.Repositories.Migrations
12 | {
13 | [DbContext(typeof(AppDbContext))]
14 | partial class AppDbContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "8.0.8")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 64);
22 |
23 | MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("QueuedPlayer", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnType("char(36)");
30 |
31 | b.Property("CreatedAt")
32 | .HasColumnType("datetime(6)");
33 |
34 | b.Property("LatencyLevel")
35 | .HasColumnType("int");
36 |
37 | b.Property("PlayerId")
38 | .HasColumnType("char(36)");
39 |
40 | b.HasKey("Id");
41 |
42 | b.ToTable("QueuedPlayers");
43 | });
44 |
45 | modelBuilder.Entity("Rovio.MatchMaking.SessionPlayer", b =>
46 | {
47 | b.Property("Id")
48 | .ValueGeneratedOnAdd()
49 | .HasColumnType("char(36)");
50 |
51 | b.Property("CreatedAt")
52 | .HasColumnType("datetime(6)");
53 |
54 | b.Property("PlayerId")
55 | .HasColumnType("char(36)");
56 |
57 | b.Property("Score")
58 | .ValueGeneratedOnAdd()
59 | .HasColumnType("int")
60 | .HasDefaultValue(0);
61 |
62 | b.Property("SessionId")
63 | .HasColumnType("char(36)");
64 |
65 | b.Property("Status")
66 | .IsRequired()
67 | .ValueGeneratedOnAdd()
68 | .HasColumnType("longtext")
69 | .HasDefaultValue("ATTENDED");
70 |
71 | b.Property("UpdatedAt")
72 | .HasColumnType("datetime(6)");
73 |
74 | b.HasKey("Id");
75 |
76 | b.ToTable("SessionPlayers");
77 | });
78 |
79 | modelBuilder.Entity("Session", b =>
80 | {
81 | b.Property("Id")
82 | .ValueGeneratedOnAdd()
83 | .HasColumnType("char(36)");
84 |
85 | b.Property("CreatedAt")
86 | .HasColumnType("datetime(6)");
87 |
88 | b.Property("EndsAt")
89 | .HasColumnType("datetime(6)");
90 |
91 | b.Property("JoinedCount")
92 | .ValueGeneratedOnAdd()
93 | .HasColumnType("int")
94 | .HasDefaultValue(0)
95 | .HasAnnotation("SqlServer:Check", "JoinedCount <= 10");
96 |
97 | b.Property("LatencyLevel")
98 | .ValueGeneratedOnAdd()
99 | .HasColumnType("int")
100 | .HasDefaultValue(1)
101 | .HasAnnotation("SqlServer:Check", "LatencyLevel >= 1");
102 |
103 | b.Property("StartsAt")
104 | .HasColumnType("datetime(6)");
105 |
106 | b.HasKey("Id");
107 |
108 | b.ToTable("Sessions");
109 | });
110 | #pragma warning restore 612, 618
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/Migrations/20240924173219_InitialCreate.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Metadata;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Rovio.MatchMaking.Repositories.Data;
9 |
10 | #nullable disable
11 |
12 | namespace Rovio.MatchMaking.Repositories.Migrations
13 | {
14 | [DbContext(typeof(AppDbContext))]
15 | [Migration("20240924173219_InitialCreate")]
16 | partial class InitialCreate
17 | {
18 | ///
19 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
20 | {
21 | #pragma warning disable 612, 618
22 | modelBuilder
23 | .HasAnnotation("ProductVersion", "8.0.8")
24 | .HasAnnotation("Relational:MaxIdentifierLength", 64);
25 |
26 | MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
27 |
28 | modelBuilder.Entity("QueuedPlayer", b =>
29 | {
30 | b.Property("Id")
31 | .ValueGeneratedOnAdd()
32 | .HasColumnType("char(36)");
33 |
34 | b.Property("CreatedAt")
35 | .HasColumnType("datetime(6)");
36 |
37 | b.Property("LatencyLevel")
38 | .HasColumnType("int");
39 |
40 | b.Property("PlayerId")
41 | .HasColumnType("char(36)");
42 |
43 | b.HasKey("Id");
44 |
45 | b.ToTable("QueuedPlayers");
46 | });
47 |
48 | modelBuilder.Entity("Rovio.MatchMaking.SessionPlayer", b =>
49 | {
50 | b.Property("Id")
51 | .ValueGeneratedOnAdd()
52 | .HasColumnType("char(36)");
53 |
54 | b.Property("CreatedAt")
55 | .HasColumnType("datetime(6)");
56 |
57 | b.Property("PlayerId")
58 | .HasColumnType("char(36)");
59 |
60 | b.Property("Score")
61 | .ValueGeneratedOnAdd()
62 | .HasColumnType("int")
63 | .HasDefaultValue(0);
64 |
65 | b.Property("SessionId")
66 | .HasColumnType("char(36)");
67 |
68 | b.Property("Status")
69 | .IsRequired()
70 | .ValueGeneratedOnAdd()
71 | .HasColumnType("longtext")
72 | .HasDefaultValue("ATTENDED");
73 |
74 | b.Property("UpdatedAt")
75 | .HasColumnType("datetime(6)");
76 |
77 | b.HasKey("Id");
78 |
79 | b.ToTable("SessionPlayers");
80 | });
81 |
82 | modelBuilder.Entity("Session", b =>
83 | {
84 | b.Property("Id")
85 | .ValueGeneratedOnAdd()
86 | .HasColumnType("char(36)");
87 |
88 | b.Property("CreatedAt")
89 | .HasColumnType("datetime(6)");
90 |
91 | b.Property("EndsAt")
92 | .HasColumnType("datetime(6)");
93 |
94 | b.Property("JoinedCount")
95 | .ValueGeneratedOnAdd()
96 | .HasColumnType("int")
97 | .HasDefaultValue(0)
98 | .HasAnnotation("SqlServer:Check", "JoinedCount <= 10");
99 |
100 | b.Property("LatencyLevel")
101 | .ValueGeneratedOnAdd()
102 | .HasColumnType("int")
103 | .HasDefaultValue(1)
104 | .HasAnnotation("SqlServer:Check", "LatencyLevel >= 1");
105 |
106 | b.Property("StartsAt")
107 | .HasColumnType("datetime(6)");
108 |
109 | b.HasKey("Id");
110 |
111 | b.ToTable("Sessions");
112 | });
113 | #pragma warning restore 612, 618
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Rovio.Matchmaking.Repositories.Tests/AppDbContextIntegrationTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Data.Sqlite;
2 | using Microsoft.EntityFrameworkCore;
3 | using Rovio.MatchMaking.Repositories.Data;
4 | using Xunit;
5 | using Xunit.Abstractions;
6 |
7 | namespace Rovio.Matchmaking.Repositories.Tests
8 | {
9 | public class AppDbContextIntegrationTests
10 | {
11 | [Fact]
12 | public void Test_AppDbContext_Should_Contain_QueuedPlayers_Table()
13 | {
14 | using var connection = new SqliteConnection("DataSource=:memory:");
15 | connection.Open();
16 |
17 | var options = new DbContextOptionsBuilder()
18 | .UseSqlite(connection)
19 | .Options;
20 |
21 | using var context = new AppDbContext(options);
22 | context.Database.EnsureCreated();
23 |
24 | // Check if 'QueuedPlayers' table exists
25 | Assert.True(TableExistsHelper(connection, "QueuedPlayers"), "'QueuedPlayers' table does not exist.");
26 |
27 | // Check if 'QueuedPlayers' table has primary key 'Id'
28 | Assert.True(CheckTableForPrimaryKeyHelper(connection, "QueuedPlayers"), "Table 'QueuedPlayers' does not have a primary key column named 'Id'.");
29 | }
30 |
31 | [Fact]
32 | public void Test_AppDbContext_Should_Contain_SessionPlayers_Table()
33 | {
34 | using var connection = new SqliteConnection("DataSource=:memory:");
35 | connection.Open();
36 |
37 | var options = new DbContextOptionsBuilder()
38 | .UseSqlite(connection)
39 | .Options;
40 |
41 | using var context = new AppDbContext(options);
42 | context.Database.EnsureCreated();
43 |
44 | // Check if 'SessionPlayers' table exists
45 | Assert.True(TableExistsHelper(connection, "SessionPlayers"), "'SessionPlayers' table does not exist.");
46 |
47 | // Check if 'SessionPlayers' table has primary key 'Id'
48 | Assert.True(CheckTableForPrimaryKeyHelper(connection, "SessionPlayers"), "Table 'SessionPlayers' does not have a primary key column named 'Id'.");
49 | }
50 |
51 | [Fact]
52 | public void Test_AppDbContext_Should_Contain_Sessions_Table()
53 | {
54 | using var connection = new SqliteConnection("DataSource=:memory:");
55 | connection.Open();
56 |
57 | var options = new DbContextOptionsBuilder()
58 | .UseSqlite(connection)
59 | .Options;
60 |
61 | using var context = new AppDbContext(options);
62 | context.Database.EnsureCreated();
63 |
64 | // Check if 'Sessions' table exists
65 | Assert.True(TableExistsHelper(connection, "Sessions"), "'Sessions' table does not exist.");
66 |
67 | // Check if 'Sessions' table has primary key 'Id'
68 | Assert.True(CheckTableForPrimaryKeyHelper(connection, "Sessions"), "Table 'Sessions' does not have a primary key column named 'Id'.");
69 | }
70 |
71 | // Helper function to check if a table exists in the SQLite database
72 | private bool TableExistsHelper(SqliteConnection connection, string tableName)
73 | {
74 | using var tableListCommand = connection.CreateCommand();
75 | tableListCommand.CommandText = "SELECT name FROM sqlite_master WHERE type='table';";
76 | using var tableReader = tableListCommand.ExecuteReader();
77 |
78 | while (tableReader.Read())
79 | {
80 | var tableNameInDb = tableReader.GetString(0);
81 | if (tableNameInDb == tableName)
82 | {
83 | return true;
84 | }
85 | }
86 |
87 | return false;
88 | }
89 |
90 | private bool CheckTableForPrimaryKeyHelper(SqliteConnection connection, string tableName)
91 | {
92 | using var command = connection.CreateCommand();
93 | command.CommandText = $"PRAGMA table_info('{tableName}');";
94 |
95 | using var reader = command.ExecuteReader();
96 |
97 | while (reader.Read())
98 | {
99 | var columnName = reader.GetString(1);
100 | var isPrimaryKey = reader.GetInt32(5);
101 |
102 | if (columnName == "Id" && isPrimaryKey == 1)
103 | {
104 | return true;
105 | }
106 | }
107 |
108 | return false;
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Rovio.Matchmaking.Repositories.Tests/QueuedPlayerRepositoryTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Rovio.MatchMaking.Repositories.Data;
3 | using Xunit;
4 |
5 | namespace Rovio.MatchMaking.Tests
6 | {
7 | public class QueuedPlayerRepositoryTests
8 | {
9 | private AppDbContext _context;
10 | private IQueuedPlayerRepository _repository;
11 |
12 | public QueuedPlayerRepositoryTests()
13 | {
14 | ProvideCleanDatabase();
15 | }
16 |
17 | // This method is called to provide a fresh DB context ensuring every test case is stateless
18 | protected void ProvideCleanDatabase()
19 | {
20 | // Setting up a clean in-memory database
21 | var options = new DbContextOptionsBuilder()
22 | .UseInMemoryDatabase(Guid.NewGuid().ToString()) // Unique name
23 | .Options;
24 |
25 | _context = new AppDbContext(options);
26 | _repository = new QueuedPlayerRepository(_context);
27 | }
28 |
29 | [Fact]
30 | public async Task AddQueuedPlayerAsync_ShouldAddPlayer()
31 | {
32 | ProvideCleanDatabase();
33 |
34 | // Arrange
35 | var playerId = Guid.NewGuid();
36 | var queuedPlayer = new QueuedPlayer { PlayerId = playerId, LatencyLevel = 1, CreatedAt = DateTime.UtcNow };
37 |
38 | // Act
39 | var insertedQueuedPlayer = await _repository.CreateQueuedPlayerAsync(queuedPlayer);
40 | await _context.SaveChangesAsync();
41 | var result = await _context.QueuedPlayers.FindAsync(insertedQueuedPlayer.Id);
42 |
43 | // Assert
44 | Assert.NotNull(result);
45 | Assert.Equal(playerId, result.PlayerId);
46 | }
47 |
48 | [Fact]
49 | public async Task DeleteQueuedPlayerAsync_ShouldRemovePlayer()
50 | {
51 | ProvideCleanDatabase();
52 |
53 | // Arrange
54 | var playerId = Guid.NewGuid();
55 | var queuedPlayer = new QueuedPlayer { PlayerId = playerId, LatencyLevel = 1, CreatedAt = DateTime.UtcNow };
56 | await _context.QueuedPlayers.AddAsync(queuedPlayer);
57 | await _context.SaveChangesAsync();
58 |
59 | // Act
60 | await _repository.DeleteQueuedPlayerAsync(playerId);
61 | await _context.SaveChangesAsync();
62 | var result = await _context.QueuedPlayers.FindAsync(playerId);
63 |
64 | // Assert
65 | Assert.Null(result);
66 | }
67 |
68 | [Fact]
69 | public async Task UpdateQueuedPlayerAsync_ShouldUpdatePlayer()
70 | {
71 | ProvideCleanDatabase();
72 |
73 | // Arrange
74 | var playerId = Guid.NewGuid();
75 | var queuedPlayer = new QueuedPlayer { PlayerId = playerId, LatencyLevel = 1, CreatedAt = DateTime.UtcNow };
76 |
77 | // Act
78 | var insertedQueuedPlayer = await _repository.CreateQueuedPlayerAsync(queuedPlayer);
79 | await _context.SaveChangesAsync();
80 | insertedQueuedPlayer.LatencyLevel = 2;
81 | _context.QueuedPlayers.Update(insertedQueuedPlayer);
82 | await _context.SaveChangesAsync();
83 | var result = await _context.QueuedPlayers.FindAsync(insertedQueuedPlayer.Id);
84 |
85 | // Assert
86 | Assert.NotNull(result);
87 | Assert.Equal(2, result.LatencyLevel);
88 | }
89 |
90 | [Fact]
91 | public async Task GetQueuedPlayerByIdAsync_ShouldGetPlayer()
92 | {
93 | ProvideCleanDatabase();
94 |
95 | // Arrange
96 | var playerId = Guid.NewGuid();
97 | var queuedPlayer = new QueuedPlayer { PlayerId = playerId, LatencyLevel = 1, CreatedAt = DateTime.UtcNow };
98 |
99 | // Act
100 | var insertedQueuedPlayer = await _repository.CreateQueuedPlayerAsync(queuedPlayer);
101 | await _context.SaveChangesAsync();
102 |
103 | var result = await _repository.GetQueuedPlayerByIdAsync(insertedQueuedPlayer.Id);
104 |
105 | // Assert
106 | Assert.NotNull(result);
107 | }
108 |
109 | [Fact]
110 | public async Task GetQueuedPlayerByPlayerIdAsync_ShouldGetPlayer()
111 | {
112 | ProvideCleanDatabase();
113 |
114 | // Arrange
115 | var playerId = Guid.NewGuid();
116 | var queuedPlayer = new QueuedPlayer { PlayerId = playerId, LatencyLevel = 1, CreatedAt = DateTime.UtcNow };
117 |
118 | // Act
119 | var insertedQueuedPlayer = await _repository.CreateQueuedPlayerAsync(queuedPlayer);
120 | await _context.SaveChangesAsync();
121 |
122 | var result = await _repository.GetQueuedPlayerByPlayerIdAsync(insertedQueuedPlayer.PlayerId);
123 |
124 | // Assert
125 | Assert.NotNull(result);
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Repositories/SessionRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Rovio.MatchMaking.Repositories.Data;
3 | namespace Rovio.MatchMaking.Repositories;
4 |
5 | public class SessionRepository : ISessionRepository
6 | {
7 | private readonly AppDbContext _context;
8 | public SessionRepository(AppDbContext context)
9 | {
10 | _context = context;
11 | }
12 |
13 | public async Task> GetAllActiveSessionsAsync()
14 | {
15 | var activeSessions = _context.Sessions
16 | .Where(s => s.JoinedCount < 10) // Sessions with less than 10 players
17 | .Where(s => s.EndsAt > DateTime.UtcNow) // Sessions with EndsAt > now are active
18 | .OrderBy(s => s.CreatedAt); // Order by creation time;
19 |
20 | return await activeSessions.ToListAsync();
21 | }
22 |
23 | public async Task CreateNewAsync(int latencyLevel, int joinedCount, int gameTimeInMinutes)
24 | {
25 | // Validation checks for the parameters
26 | //TODO: move these checks to the logic layer??
27 | if (latencyLevel < 1 || latencyLevel > 5)
28 | {
29 | throw new ArgumentOutOfRangeException(nameof(latencyLevel), "Latency level must be between 1 and 5.");
30 | }
31 |
32 | if (joinedCount < 0 || joinedCount > 10)
33 | {
34 | throw new ArgumentOutOfRangeException(nameof(joinedCount), "Joined count must be between 0 and 10.");
35 | }
36 |
37 | if (gameTimeInMinutes <= 0)
38 | {
39 | throw new ArgumentOutOfRangeException(nameof(gameTimeInMinutes), "Game time must be a positive number.");
40 | }
41 |
42 | var session = new Session
43 | {
44 | Id = Guid.NewGuid(),
45 | LatencyLevel = latencyLevel,
46 | JoinedCount = joinedCount,
47 | CreatedAt = DateTime.UtcNow,
48 | StartsAt = DateTime.UtcNow,
49 | EndsAt = DateTime.UtcNow.AddMinutes(gameTimeInMinutes)
50 | };
51 |
52 | await _context.Sessions.AddAsync(session);
53 | await _context.SaveChangesAsync();
54 |
55 | return session;
56 | }
57 |
58 | public async Task GetSessionById(Guid sessionId)
59 | {
60 | var session = await _context.Sessions.FindAsync(sessionId);
61 | return session;
62 | }
63 |
64 | public async Task> GetSessionPlayersBySessionId(Guid sessionId)
65 | {
66 | var sessionPlayers = _context.SessionPlayers
67 | .Where(sp => sp.SessionId == sessionId);
68 |
69 | return await sessionPlayers.ToListAsync();
70 | }
71 |
72 | public async Task AddPlayerToSessionAsync(Guid sessionId, Guid playerId)
73 | {
74 | // Check if the session exists
75 | var session = await _context.Sessions.FindAsync(sessionId);
76 | if (session == null)
77 | {
78 | throw new ArgumentException("Session not found.");
79 | }
80 |
81 | // Check if the session has space for more players
82 | if (session.JoinedCount >= 10)
83 | {
84 | throw new InvalidOperationException("Session is full.");
85 | }
86 |
87 | // Check if the player is already in the session
88 | var existingSessionPlayer = await _context.SessionPlayers
89 | .FirstOrDefaultAsync(sp => sp.SessionId == sessionId && sp.PlayerId == playerId);
90 | if (existingSessionPlayer != null)
91 | {
92 | throw new InvalidOperationException("Player is already in the session.");
93 | }
94 |
95 | // Add the player to the session
96 | var sessionPlayer = new SessionPlayer
97 | {
98 | Id = Guid.NewGuid(),
99 | SessionId = sessionId,
100 | PlayerId = playerId,
101 | Status = "ATTENDED",
102 | Score = 0,
103 | CreatedAt = DateTime.UtcNow,
104 | UpdatedAt = DateTime.UtcNow
105 | };
106 |
107 | session.JoinedCount++; // Increase the player count in the session
108 |
109 | await _context.SessionPlayers.AddAsync(sessionPlayer);
110 | _context.Sessions.Update(session); // Update the session with the new JoinedCount
111 | await _context.SaveChangesAsync();
112 |
113 | return sessionPlayer;
114 | }
115 |
116 | public async Task RemovePlayerFromSessionAsync(Guid sessionId, Guid playerId)
117 | {
118 | // Find the session player entry
119 | var sessionPlayer = await _context.SessionPlayers
120 | .FirstOrDefaultAsync(sp => sp.SessionId == sessionId && sp.PlayerId == playerId);
121 | if (sessionPlayer == null)
122 | {
123 | throw new ArgumentException("Player is not in the session.");
124 | }
125 |
126 | // Update the status to 'LEFT'
127 | sessionPlayer.Status = "LEFT";
128 | sessionPlayer.UpdatedAt = DateTime.UtcNow;
129 |
130 | // Update the session player entry
131 | _context.SessionPlayers.Update(sessionPlayer);
132 |
133 | // Find the session and decrease the JoinedCount if necessary
134 | var session = await _context.Sessions.FindAsync(sessionId);
135 | if (session != null && session.JoinedCount > 0)
136 | {
137 | session.JoinedCount--;
138 | _context.Sessions.Update(session);
139 | }
140 |
141 | await _context.SaveChangesAsync();
142 |
143 | return sessionPlayer;
144 | }
145 | }
--------------------------------------------------------------------------------
/Rovio.Matchmaking.Repositories.Tests/SessionRepositoryTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Rovio.MatchMaking.Repositories;
3 | using Rovio.MatchMaking.Repositories.Data;
4 | using Xunit;
5 |
6 | namespace Rovio.MatchMaking.Tests
7 | {
8 | public class SessionRepositoryTests
9 | {
10 | private AppDbContext _context;
11 | private ISessionRepository _repository;
12 |
13 | public SessionRepositoryTests()
14 | {
15 | ProvideCleanDatabase();
16 | }
17 |
18 | // This method is called to provide a fresh DB context ensuring every test case is stateless
19 | protected void ProvideCleanDatabase()
20 | {
21 | // Setting up a clean in-memory database
22 | var options = new DbContextOptionsBuilder()
23 | .UseInMemoryDatabase(Guid.NewGuid().ToString()) // Unique name
24 | .Options;
25 |
26 | _context = new AppDbContext(options);
27 | _repository = new SessionRepository(_context);
28 | }
29 |
30 | [Fact]
31 | public async Task GetAllActiveSessionsAsync_ShouldListActiveSessions()
32 | {
33 | ProvideCleanDatabase();
34 |
35 | // Arrange
36 | var sessionAtive1 = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow, EndsAt = DateTime.UtcNow.AddMinutes(30) };
37 | var sessionAtive2 = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow, EndsAt = DateTime.UtcNow.AddMinutes(30) };
38 | var sessionInactive = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow.AddMinutes(-50), EndsAt = DateTime.UtcNow.AddMinutes(-20) };
39 |
40 | // Act
41 | _context.Sessions.Add(sessionAtive1);
42 | _context.Sessions.Add(sessionAtive2);
43 | _context.Sessions.Add(sessionInactive);
44 | await _context.SaveChangesAsync();
45 | var fetchedSessions = await _repository.GetAllActiveSessionsAsync();
46 |
47 | // Assert
48 | Assert.NotNull(fetchedSessions);
49 | Assert.Equal(2, fetchedSessions.Count());
50 | }
51 |
52 | [Fact]
53 | public async Task GetSessionById_ShouldWorkOK()
54 | {
55 | ProvideCleanDatabase();
56 |
57 | // Arrange
58 | var session = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow, EndsAt = DateTime.UtcNow.AddMinutes(30) };
59 |
60 | // Act
61 | var insertedSession = _context.Sessions.Add(session);
62 | await _context.SaveChangesAsync();
63 | var fetchedSession = await _repository.GetSessionById(insertedSession.Entity.Id);
64 |
65 | // Assert
66 | Assert.NotNull(fetchedSession);
67 | }
68 |
69 | [Fact]
70 | public async Task GetSessionPlayersBySessionId_ShouldWorkOK()
71 | {
72 | ProvideCleanDatabase();
73 |
74 | // Arrange
75 | var session = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow, EndsAt = DateTime.UtcNow.AddMinutes(30) };
76 |
77 | // Act
78 | var insertedSession = _context.Sessions.Add(session);
79 | var sessionPlayer1 = new SessionPlayer{ SessionId = insertedSession.Entity.Id, PlayerId = new Guid() };
80 | var sessionPlayer2 = new SessionPlayer{ SessionId = insertedSession.Entity.Id, PlayerId = new Guid() };
81 | _context.SessionPlayers.Add(sessionPlayer1);
82 | _context.SessionPlayers.Add(sessionPlayer2);
83 | await _context.SaveChangesAsync();
84 |
85 | var fetchedSessionPlayers = await _repository.GetSessionPlayersBySessionId(session.Id);
86 |
87 | // Assert
88 | Assert.NotNull(fetchedSessionPlayers);
89 | Assert.Equal(2, fetchedSessionPlayers.Count());
90 | }
91 |
92 | [Fact]
93 | public async Task CreateNewAsync_ShouldCreateSession()
94 | {
95 | ProvideCleanDatabase();
96 |
97 | // Arrange
98 | var session = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow };
99 |
100 | // Act
101 | var insertedSession = await _repository.CreateNewAsync(session.LatencyLevel, session.JoinedCount, 10);
102 | await _context.SaveChangesAsync();
103 | var fetchedSession = await _context.Sessions.FindAsync(insertedSession.Id);
104 |
105 | // Assert
106 | Assert.NotNull(fetchedSession);
107 | Assert.Equal(session.JoinedCount, fetchedSession.JoinedCount);
108 | Assert.Equal(session.LatencyLevel, fetchedSession.LatencyLevel);
109 | }
110 |
111 | [Fact]
112 | public async Task AddPlayerToSessionAsync_ShouldAddPlayer()
113 | {
114 | ProvideCleanDatabase();
115 |
116 | // Arrange
117 | var playerId = Guid.NewGuid();
118 | var session = new Session { LatencyLevel = 1, JoinedCount = 0, CreatedAt = DateTime.UtcNow };
119 |
120 | // Act
121 | var insertedSession = await _repository.CreateNewAsync(session.LatencyLevel, session.JoinedCount, 10);
122 | await _context.SaveChangesAsync();
123 | var insertedSessionPlayer = await _repository.AddPlayerToSessionAsync(insertedSession.Id, playerId);
124 | await _context.SaveChangesAsync();
125 | var fetchedSessionPlayer = await _context.SessionPlayers.FindAsync(insertedSessionPlayer.Id);
126 | var fetchedSession = await _context.Sessions.FindAsync(insertedSession.Id);
127 |
128 | // Assert
129 | Assert.NotNull(fetchedSessionPlayer);
130 | Assert.NotNull(fetchedSession);
131 | Assert.Equal(playerId, fetchedSessionPlayer.PlayerId);
132 | Assert.Equal(insertedSession.Id, fetchedSessionPlayer.SessionId);
133 | Assert.Equal(1, fetchedSession.JoinedCount);
134 | }
135 |
136 | [Fact]
137 | public async Task RemovePlayerFromSessionAsync_ShouldAddPlayer()
138 | {
139 | ProvideCleanDatabase();
140 |
141 | // Arrange
142 | var playerId = Guid.NewGuid();
143 | var session = new Session { LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow };
144 | var insertedSession = _context.Sessions.Add(session);
145 |
146 | var sessionPlayer = new SessionPlayer{ SessionId = insertedSession.Entity.Id, PlayerId = playerId };
147 | var insertedSessionPlayer = _context.SessionPlayers.Add(sessionPlayer);
148 | await _context.SaveChangesAsync();
149 | var fetchedSession = await _context.Sessions.FindAsync(insertedSession.Entity.Id);
150 | Assert.NotNull(fetchedSession);
151 | var fetchedSessionPlayer = await _context.SessionPlayers.FindAsync(insertedSessionPlayer.Entity.Id);
152 | Assert.NotNull(fetchedSessionPlayer);
153 | Assert.Equal(playerId, fetchedSessionPlayer.PlayerId);
154 | Assert.Equal("ATTENDED", fetchedSessionPlayer.Status);
155 | Assert.Equal(insertedSession.Entity.Id, fetchedSessionPlayer.SessionId);
156 | Assert.Equal(1, fetchedSession.JoinedCount);
157 |
158 | // Act
159 | await _repository.RemovePlayerFromSessionAsync(insertedSession.Entity.Id, insertedSessionPlayer.Entity.PlayerId);
160 | await _context.SaveChangesAsync();
161 | fetchedSessionPlayer = await _context.SessionPlayers.FindAsync(insertedSessionPlayer.Entity.Id);
162 | Assert.NotNull(fetchedSessionPlayer);
163 | Assert.Equal(playerId, fetchedSessionPlayer.PlayerId);
164 | Assert.Equal("LEFT", fetchedSessionPlayer.Status);
165 | Assert.Equal(insertedSession.Entity.Id, fetchedSessionPlayer.SessionId);
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console/Services/SessionMatchMaker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.EntityFrameworkCore.Storage;
7 | using Microsoft.EntityFrameworkCore.InMemory;
8 | using Rovio.MatchMaking.Repositories;
9 | using Rovio.MatchMaking.Repositories.Data;
10 |
11 | namespace Rovio.MatchMaking.Console.Services;
12 | public class SessionMatchMaker
13 | {
14 | private readonly AppDbContext _context;
15 | private readonly ISessionRepository _sessionRepository;
16 | private readonly IQueuedPlayerRepository _queuedPlayerRepository;
17 |
18 | public SessionMatchMaker(AppDbContext context, IQueuedPlayerRepository queuedPlayerRepository, ISessionRepository sessionRepository)
19 | {
20 | _context = context;
21 | _queuedPlayerRepository = queuedPlayerRepository;
22 | _sessionRepository = sessionRepository;
23 | }
24 |
25 | public async Task RunAsync()
26 | {
27 | try {
28 | // Step 1: Read Players from Queues and Create a HashMap
29 | var queuedPlayersMap = await CreateQueuedPlayersMapAsync();
30 |
31 | // Step 2: Read Active Sessions and Create a HashMap
32 | var activeSessionsMap = await CreateActiveSessionsMapAsync();
33 |
34 | var joinedSessionsPlayerIds = await AddQueuedPlayersToActiveSessions(queuedPlayersMap, activeSessionsMap);
35 | //TODO: send notification and matched event for joinedSessionsPlayerIds users
36 |
37 | queuedPlayersMap = await RemoveAttendedPlayerIdsFromMap(queuedPlayersMap, joinedSessionsPlayerIds);
38 |
39 | // Step 4: Create New Sessions if Necessary
40 | joinedSessionsPlayerIds = await CreateSessionForRemainedPlayers(queuedPlayersMap);
41 | } catch (Exception ex) {
42 | System.Console.WriteLine("Error while doing the matching operation!");
43 | }
44 |
45 | System.Console.WriteLine("Matchmaking process completed successfully!");
46 | }
47 |
48 | internal async Task> AddQueuedPlayersToActiveSessions(Dictionary> queuedPlayersMap, Dictionary> activeSessionsMap)
49 | {
50 | // Preventing to use db transactions for test environment, to be able to mock db methods in this case
51 | var isTestEnvironment = _context.Database.IsInMemory();
52 |
53 | List addedPlayerIds = new List();
54 |
55 | foreach (var latencyLevel in queuedPlayersMap.Keys)
56 | {
57 | if (activeSessionsMap.ContainsKey(latencyLevel))
58 | {
59 | foreach (var queuedPlayer in queuedPlayersMap[latencyLevel])
60 | {
61 | var sessions = activeSessionsMap[latencyLevel];
62 | foreach (var session in sessions)
63 | {
64 | if (session.JoinedCount < 10)
65 | {
66 | IDbContextTransaction transaction = null;
67 | if (!isTestEnvironment) {
68 | transaction = await _context.Database.BeginTransactionAsync();
69 | }
70 | // using var transaction = await _context.Database.BeginTransactionAsync();
71 | try {
72 | await _sessionRepository.AddPlayerToSessionAsync(session.Id, queuedPlayer.PlayerId);
73 | session.JoinedCount++;
74 | if (!isTestEnvironment) {
75 | _context.Sessions.Update(session);
76 | }
77 | await _queuedPlayerRepository.DeleteQueuedPlayerAsync(queuedPlayer.Id);
78 | if (!isTestEnvironment) {
79 | await _context.SaveChangesAsync();
80 | await transaction.CommitAsync();
81 | }
82 |
83 | addedPlayerIds.Add(queuedPlayer.PlayerId);
84 | } catch (Exception ex)
85 | {
86 | // Rollback the transaction if any operation fails
87 | if (!isTestEnvironment) {
88 | await transaction.RollbackAsync();
89 | }
90 | throw ex;
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | return addedPlayerIds;
99 | }
100 |
101 | internal async Task> CreateSessionForRemainedPlayers(Dictionary> queuedPlayersMap)
102 | {
103 | // Preventing to use db transactions for test environment, to be able to mock db methods in this case
104 | var isTestEnvironment = _context.Database.IsInMemory();
105 |
106 | List addedPlayerIds = new List();
107 |
108 | foreach (var latencyLevel in queuedPlayersMap.Keys)
109 | {
110 | var remainingQueuedPlayers = queuedPlayersMap[latencyLevel];
111 | while (remainingQueuedPlayers.Count >= 2)
112 | {
113 | var sessionSize = remainingQueuedPlayers.Count >= 10 ? 10 : remainingQueuedPlayers.Count;
114 |
115 | IDbContextTransaction transaction = null;
116 | if (!isTestEnvironment) {
117 | transaction = await _context.Database.BeginTransactionAsync();
118 | }
119 | try {
120 | var newSession = await _sessionRepository.CreateNewAsync(latencyLevel, 0, 30);
121 |
122 | for (int i = 0; i < sessionSize; i++)
123 | {
124 | var queuedPlayer = remainingQueuedPlayers[i];
125 | // I have considered that the whole matching module is called by another service, so the existence of player and validity of playerId had been checked in other modules before
126 | await _sessionRepository.AddPlayerToSessionAsync(newSession.Id, queuedPlayer.PlayerId);
127 | await _queuedPlayerRepository.DeleteQueuedPlayerAsync(queuedPlayer.Id);
128 | }
129 |
130 | if (!isTestEnvironment) {
131 | await _context.SaveChangesAsync();
132 | await transaction.CommitAsync();
133 | }
134 |
135 | for (int i = 0; i < sessionSize; i++)
136 | {
137 | var queuedPlayer = remainingQueuedPlayers[i];
138 | addedPlayerIds.Add(queuedPlayer.PlayerId);
139 | }
140 | remainingQueuedPlayers.RemoveRange(0, sessionSize);
141 |
142 | } catch (Exception ex) {
143 | // Rollback the transaction if any operation fails
144 | if (!isTestEnvironment) {
145 | await transaction.RollbackAsync();
146 | }
147 |
148 | System.Console.WriteLine($"An error occurred during creating new session: PlayerIDs={string.Join(", ", remainingQueuedPlayers.GetRange(0, sessionSize))}, LatencyLevel={latencyLevel}, Error={ex.Message}");
149 | throw ex;
150 | }
151 | }
152 | }
153 |
154 | return addedPlayerIds;
155 | }
156 |
157 | internal async Task>> RemoveAttendedPlayerIdsFromMap(Dictionary> queuedPlayersMap, List attendedPlayerIds)
158 | {
159 | var attendedPlayerIdsMap = new Dictionary();
160 | foreach (var playerId in attendedPlayerIds) {
161 | attendedPlayerIdsMap[playerId] = true;
162 | }
163 |
164 | var newQueuedPlayersMap = new Dictionary>();
165 | foreach (var latencyLevel in queuedPlayersMap.Keys) {
166 | var newQueuedPlayersList = new List();
167 | foreach (var queuedPlayer in queuedPlayersMap[latencyLevel]) {
168 | if (!attendedPlayerIdsMap.ContainsKey(queuedPlayer.PlayerId)) {
169 | newQueuedPlayersList.Add(queuedPlayer);
170 | }
171 | }
172 | if (newQueuedPlayersList.Count > 0) {
173 | newQueuedPlayersMap[latencyLevel] = newQueuedPlayersList;
174 | }
175 | }
176 |
177 | return newQueuedPlayersMap;
178 | }
179 |
180 | internal async Task>> CreateQueuedPlayersMapAsync()
181 | {
182 | //TODO: move this part to repoe
183 | // var playersQueue = await _context.QueuedPlayers
184 | // .OrderBy(qp => qp.CreatedAt) // Order by queueing time
185 | // .ToListAsync();
186 | var playersQueue = await _queuedPlayerRepository.GetAllQueuedPlayersAsync();
187 |
188 | var queudPlayersMap = new Dictionary>();
189 |
190 | foreach (var queuedPlayer in playersQueue)
191 | {
192 | if (!queudPlayersMap.ContainsKey(queuedPlayer.LatencyLevel))
193 | {
194 | queudPlayersMap[queuedPlayer.LatencyLevel] = new List();
195 | }
196 | queudPlayersMap[queuedPlayer.LatencyLevel].Add(queuedPlayer);
197 | }
198 |
199 | return queudPlayersMap;
200 | }
201 |
202 | internal async Task>> CreateActiveSessionsMapAsync()
203 | {
204 | //TODO: move this part to repoe
205 | var activeSessions = await _sessionRepository.GetAllActiveSessionsAsync();
206 |
207 | var activeSessionsMap = new Dictionary>();
208 |
209 | foreach (var session in activeSessions)
210 | {
211 | if (!activeSessionsMap.ContainsKey(session.LatencyLevel))
212 | {
213 | activeSessionsMap[session.LatencyLevel] = new List();
214 | }
215 | activeSessionsMap[session.LatencyLevel].Add(session);
216 | }
217 |
218 | return activeSessionsMap;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Assumptions
4 |
5 | I have made the following assumptions:
6 |
7 | - I have designed this service to be invoked internally by other services, making it safe to call methods like QueuePlayer using the PlayerId provided in the REST API. However, if the service is exposed to external users, we should implement authentication mechanisms and avoid accepting PlayerId directly from the request body to ensure security.
8 | - I defined something named QueuedPlayer as another entity in table, because I have considered that Player is something from another microservice and since I wanted to add some new fields to it, I considered a QueuedPlayer entity which can be casted to player and vice versa.
9 | - **Session Definition:** A session is a competitive environment where 2 to 10 players can participate. A player can only be part of one session at a time until it concludes.
10 | - **Session Joining:** Players must either leave their current contest or wait for it to finish before joining a new session.
11 | - **Session Timing:** A session begins at a specified timestamp if at least two players are available and ends after a pre-determined duration from the start time.
12 | - **Error Handling:** The `QueuePlayerAsync` method may occasionally return errors. Therefore, I have modified the interface response to `Task` to handle these errors effectively.
13 | - **Process Flow:**
14 | - A player requests to join a contest session.
15 | - If the player is not currently in an active session, their request is placed in a queue.
16 | - A worker process periodically checks the queue, matches players, creates new sessions, and notifies players about the start of their contest.
17 | - **Optional Implementation:** If there is an active session that meets the player's criteria, they can join it. Otherwise, their request will be queued (this feature is not implemented).
18 |
19 | ## Why Use Asynchronous Matching?
20 |
21 | If a session were created for every request and returned in the response, the system would need to check all database records to find a suitable match, which also could lead to unnecessary session creation if we want to speed up the response time by passing some condition checkings in database. This approach requires handling `mutual exclusion` for every request. As processing each request takes time, this architecture can result in poor response times and potential deadlocks if not implemented carefully.
22 |
23 | To address these issues, I've considered:
24 |
25 | - **Session Creation:** Sessions are not created via the API but only through a scheduled console command (cron job).
26 | - **Cron Job Execution:** A matching cron job can be scheduled to run every minute. Since only one instance of this job executes at a time, mutual exclusion and concurrency management are unnecessary as long as this condition is upheld. Alternatively, we can implement a worker that continuously performs the matching operation in a `while(true)` loop, if the one-minute interval is not optimal for user experience.
27 | - **Efficient Matching:** This approach ensures efficient matching and prevents the creation of unnecessary sessions through a well-implemented matching command.
28 |
29 | # Database Schema
30 |
31 | ### Sessions Table
32 |
33 | | Column Name | Data Type | Constraints | Description |
34 | |---------------|--------------|-----------------------|---------------------------------|
35 | | `Id` | `UUID` | `PRIMARY KEY` | Unique identifier for each session |
36 | | `LatencyLevel` | `INT` | `CHECK(LatencyLevel >= 1)` | Latency level of the player (1 to 5) |
37 | | `JoinedCount` | `INT` | `DEFAULT 0 CHECK(JoinedCount <= 10)` | Number of players joined the session |
38 | | `CreatedAt` | `DATETIME` | `DEFAULT CURRENT_TIMESTAMP` | Timestamp when the record was created |
39 | | `StartsAt` | `DATETIME` | `` | Timestamp when the contest will start |
40 | | `EndsAt` | `DATETIME` | `` | Timestamp when the contest will end |
41 |
42 | ### Sessions_Players Table
43 |
44 | Relation of Players and Sessions
45 |
46 | | Column Name | Data Type | Constraints | Description |
47 | |--------------|-------------------------------|---------------------------------------------|-----------------------------------------------|
48 | | `Id` | `UUID` | `PRIMARY KEY` | Unique identifier for each session |
49 | | `Session_id` | `UUID` | `NOT NULL` | ID of the session the player is attending |
50 | | `Player_id` | `UUID` | `NOT NULL` | ID of the player attending the session |
51 | | `Status` | `ENUM('ATTENDED','PLAYED','LEFT')` | `DEFAULT 'ATTENDED'` | Status of the player in the session |
52 | | `Score` | `INT` | `DEFAULT 0` | Score of the player in the contest |
53 | | `CreatedAt` | `DATETIME` | `DEFAULT CURRENT_TIMESTAMP` | Timestamp when the record was created |
54 | | `UpdatedAt` | `DATETIME` | `DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | Timestamp when the record was last updated |
55 |
56 | ## Queued_Players Table
57 |
58 | This table contains players' requests to join a session.
59 |
60 | | Column Name | Data Type | Constraints | Description |
61 | |---------------|------------|-----------------------------------------|---------------------------------------------------------|
62 | | `Id` | `UUID` | `PRIMARY KEY` | Unique identifier for each request |
63 | | `Player_id` | `UUID` | `UNIQUE` | ID of the player requesting to join a session |
64 | | `LatencyLevel`| `INT` | `CHECK(LatencyLevel >= 1 AND LatencyLevel <= 5)` | Latency level of the player (1 to 5, where 1 is best) |
65 | | `CreatedAt` | `DATETIME` | `DEFAULT CURRENT_TIMESTAMP` | Timestamp when the request was created |
66 |
67 | # Code Design and Architecture
68 |
69 | ## APIs
70 |
71 | 1. **Queue**: Allows players to request entry into the matchmaking queue, provided they haven't already joined.
72 | 2. **Dequeue**: Enables players to cancel their matchmaking request.
73 | 3. **Join Session**: Once a player has been matched and receives their `sessionId` via push notification or WebSocket, they can use this API to join their contest session.
74 |
75 | ## System Workflow
76 |
77 | 1. Players initiate a matchmaking request through the `queue` API.
78 | 2. Players can cancel their request by calling the `dequeue` API.
79 | 3. A worker process, running in the `Rovio.MatchMaking.Console` command-line tool, is responsible for matching players.
80 |
81 | - The worker first retrieves active sessions with fewer than 10 players.
82 | - It then attempts to assign players to these sessions based on their latency level, ensuring they join sessions within acceptable latency thresholds.
83 |
84 | ## Latency Levels
85 |
86 | Latency is categorized as follows:
87 |
88 | - **Level 1**: ≤ 100ms
89 | - **Level 2**: > 100ms and ≤ 200ms
90 | - **Level 3**: > 200ms and ≤ 300ms
91 | - **Level 4**: > 300ms and ≤ 400ms
92 | - **Level 5**: > 400ms
93 |
94 |
95 | # System Design Diagram of Matchmaking service
96 |
97 | 
98 |
99 | # How to run project
100 |
101 | ## Initial Setup
102 |
103 | ### 1. Database Configuration
104 | - Ensure that your database connections are configured correctly:
105 | 1. Install a fresh MySQL database and obtain its credentials. You can use a Dockerized version of MySQL.
106 | 2. Update the database configurations in the following files:
107 | - `Rovio.MatchMaking.Repositories/appsettings.json`
108 | - `Rovio.MatchMaking.Repositories/appsettings.Development.json`
109 | - `Rovio.MatchMaking.Net/appsettings.json`
110 | - `Rovio.MatchMaking.Net/appsettings.Development.json`
111 |
112 | ### 2. Installing Infrastructure
113 | - Navigate to the `Rovio.MatchMaking.Repositories` directory and run the following command to set up the infrastructure:
114 |
115 | ```bash
116 | dotnet ef database update --context AppDbContext
117 | ```
118 |
119 | ### 3. Installing Dependencies
120 | - Install the necessary dependencies by running:
121 |
122 | ```bash
123 | dotnet restore
124 | ```
125 |
126 | ### 4. Running the Project
127 | - Build and run the project using the following commands:
128 |
129 | ```bash
130 | dotnet build
131 | dotnet run
132 | ```
133 |
134 | - Start the `Rovio.MatchMaking.Net` module by executing the `dotnet run` command at the root of the project.
135 | - Once running, you can access the `Swagger Panel` and use the `queue` API multiple times to queue different users with varying latencies.
136 |
137 | ### 5. Running the Matchmaking Worker
138 | - To match users, navigate to the `Rovio.MatchMaking.Console` project and run the following command:
139 |
140 | ```bash
141 | dotnet run
142 | ```
143 |
144 | - This will initiate the matchmaking worker and execute the matchmaking operations. After execution, verify the following in the database:
145 | - Players queued for matching should be removed from the `QueuedPlayers` table.
146 | - New sessions should be created in the `Sessions` table.
147 | - The `SessionPlayers` table should reflect the players assigned to sessions based on their latencies.
148 |
149 | ### 6. Client Notification and Session Joining
150 | - After matching, clients (users) will be notified of their session assignment via FCM or WebSocket, receiving their `session_id`.
151 | - They can then call the `match/join` API to obtain session data and view their teammates (competitors list).
152 |
153 | # How to deploy on production and how to scale
154 | ## Production Deployment Strategy
155 |
156 | To deploy the system into production, it is recommended to Dockerize the `Rovio.MatchMaking.Net` and `Rovio.MatchMaking.Console` modules. Using a container orchestration tool like Kubernetes, you can manage deployments efficiently on platforms such as Google Kubernetes Engine (GKE), Amazon EKS, or Azure Kubernetes Service (AKS).
157 |
158 | ### Deployment Plan
159 |
160 | 1. **Deploying `Rovio.MatchMaking.Net`:**
161 | - Set up `Rovio.MatchMaking.Net` as a Kubernetes deployment.
162 | - Configure it to scale based on demand, either manually or using an autoscaler to dynamically adjust resources according to the load.
163 |
164 | 2. **Deploying the Matchmaking Worker (`Rovio.MatchMaking.Console`):**
165 | - Deploy the `Rovio.MatchMaking.Console` module as a single-pod deployment.
166 | - Integrate health checks and monitoring tools to ensure the worker restarts or scales as needed.
167 | - Consider using a message queuing system like RabbitMQ or Redis to manage matchmaking tasks. This will allow multiple instances of the matchmaking worker to operate concurrently without race conditions, overcoming the limitations of the current single-instance model.
168 |
169 | 3. **Database Configuration:**
170 | - A single-node instance should be sufficient for the database, but performance can be enhanced with the following optimizations:
171 | - Implement table partitioning.
172 | - Define efficient indexing strategies.
173 | - Use sharding if necessary (although it is unlikely to be needed even under high loads).
174 | - Regularly archive and move old data to cold storage to maintain performance and manage database size.
175 |
176 | By following this strategy, the system can handle high loads efficiently, ensure resilience, and provide seamless scalability in a production environment.
177 |
178 | # Tests
179 | Tests are developed for the following modules:
180 | - `Rovio.MatchMaking.Net`
181 | - `Rovio.MatchMaking.Console`
182 | To run the tests, you can simply run the following cmd from root of the project:
183 | ```
184 | dotnet test
185 | ```
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Net.Tests/MatchMakingControllerTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.Extensions.Logging;
4 | using Moq;
5 | using Xunit;
6 |
7 | namespace Rovio.MatchMaking.Net.Tests
8 | {
9 | public class MatchMakingControllerTests
10 | {
11 | private readonly Mock _queuedPlayerRepositoryMock;
12 | private readonly Mock _sessionRepositoryMock;
13 | private readonly MatchMakingController _controller;
14 | private readonly Mock> _loggerMock;
15 |
16 | public MatchMakingControllerTests()
17 | {
18 | _queuedPlayerRepositoryMock = new Mock();
19 | _sessionRepositoryMock = new Mock();
20 | _loggerMock = new Mock>();
21 | _controller = new MatchMakingController(_queuedPlayerRepositoryMock.Object, _sessionRepositoryMock.Object);
22 | }
23 |
24 | [Fact]
25 | public async Task QueuePlayerAsync_ShouldReturnBadRequest_WhenPlayerIsNull()
26 | {
27 | // Act
28 | var result = await _controller.QueuePlayerAsync(null);
29 |
30 | // Assert
31 | var badRequestResult = Assert.IsType(result);
32 | Assert.NotNull(badRequestResult.Value);
33 | Assert.Contains("Player cannot be null", badRequestResult.Value.ToString());
34 | }
35 |
36 | [Fact]
37 | public async Task QueuePlayerAsync_ShouldReturnOk_WhenPlayerIsValid()
38 | {
39 | // Arrange
40 | var player = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
41 | var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
42 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.CreateQueuedPlayerAsync(_queuedPlayer)).ReturnsAsync(new QueuedPlayer());
43 |
44 | // Act
45 | var result = await _controller.QueuePlayerAsync(player);
46 |
47 | // Assert
48 | var okResult = Assert.IsType(result);
49 | Assert.Contains("Player queued successfully", okResult.Value.ToString());
50 | }
51 |
52 | [Fact]
53 | public async Task QueuePlayerAsync_ShouldReturnBadRequest_WhenPlayerHadBeenQueued()
54 | {
55 | // Arrange
56 | var player = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
57 | var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
58 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.GetQueuedPlayerByPlayerIdAsync(player.Id)).ReturnsAsync(new QueuedPlayer());
59 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.CreateQueuedPlayerAsync(_queuedPlayer)).ReturnsAsync(new QueuedPlayer());
60 |
61 | // Act
62 | var result = await _controller.QueuePlayerAsync(player);
63 |
64 | // Assert
65 | var badRequestResult = Assert.IsType(result);
66 | Assert.NotNull(badRequestResult.Value);
67 | Assert.Contains("Player is already queued", badRequestResult.Value.ToString());
68 | }
69 |
70 | [Fact]
71 | public async Task DequeuePlayerAsync_ShouldReturnBadRequest_WhenPlayerIsNull()
72 | {
73 | // Act
74 | var result = await _controller.DequeuePlayerAsync(null);
75 |
76 | // Assert
77 | var badRequestResult = Assert.IsType(result);
78 | Assert.NotNull(badRequestResult.Value);
79 | Assert.Contains("Player cannot be null", badRequestResult.Value.ToString());
80 | }
81 |
82 | [Fact]
83 | public async Task DequeuePlayerAsync_ShouldReturnBadRequest_WhenPlayerHadNotBeenQueued()
84 | {
85 | // Arrange
86 | var player = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
87 |
88 | // Act
89 | var result = await _controller.DequeuePlayerAsync(player);
90 |
91 | // Assert
92 | var badRequestResult = Assert.IsType(result);
93 | Assert.NotNull(badRequestResult.Value);
94 | Assert.Contains("Player is not queued", badRequestResult.Value.ToString());
95 | }
96 |
97 | [Fact]
98 | public async Task DequeuePlayerAsync_ShouldReturnOk_WhenPlayerHadBeenQueued()
99 | {
100 | // Arrange
101 | var player = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
102 | var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
103 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.GetQueuedPlayerByPlayerIdAsync(player.Id)).ReturnsAsync(new QueuedPlayer());
104 |
105 | // Act
106 | var result = await _controller.QueuePlayerAsync(player);
107 |
108 | // Assert
109 | var badRequestResult = Assert.IsType(result);
110 | Assert.NotNull(badRequestResult.Value);
111 | Assert.Contains("Player is already queued", badRequestResult.Value.ToString());
112 | }
113 |
114 | [Fact]
115 | public async Task JoinSession_ShouldReturnBadRequest_WhenBodyIsNull()
116 | {
117 | // Arrange
118 | var player1 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
119 | var player2 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
120 | var player3 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
121 |
122 | var session = new Session {Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 2};
123 | var sessionPlayer1 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player1.Id};
124 | var sessionPlayer2 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player2.Id};
125 |
126 | var sessionPlayers = new List();
127 | sessionPlayers.Add(sessionPlayer1);
128 | sessionPlayers.Add(sessionPlayer2);
129 |
130 | // var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
131 | _sessionRepositoryMock.Setup(sr => sr.GetSessionById(session.Id)).ReturnsAsync(session);
132 | _sessionRepositoryMock.Setup(sr => sr.GetSessionPlayersBySessionId(session.Id)).ReturnsAsync(sessionPlayers);
133 |
134 | // Act
135 | var invalidSessionId = player1.Id;
136 | var result = await _controller.JoinSession(null);
137 |
138 | // Assert
139 | var badRequestResult = Assert.IsType(result);
140 | Assert.NotNull(badRequestResult.Value);
141 | Assert.Contains("Request body cannot be null", badRequestResult.Value.ToString());
142 | }
143 |
144 | [Fact]
145 | public async Task JoinSession_ShouldReturnOk_WhenPlayerHasASession()
146 | {
147 | // Arrange
148 | var player1 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
149 | var player2 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
150 |
151 | var session = new Session {Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 2};
152 | var sessionPlayer1 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player1.Id};
153 | var sessionPlayer2 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player2.Id};
154 |
155 | var sessionPlayers = new List();
156 | sessionPlayers.Add(sessionPlayer1);
157 | sessionPlayers.Add(sessionPlayer2);
158 |
159 | // var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
160 | _sessionRepositoryMock.Setup(sr => sr.GetSessionById(session.Id)).ReturnsAsync(session);
161 | _sessionRepositoryMock.Setup(sr => sr.GetSessionPlayersBySessionId(session.Id)).ReturnsAsync(sessionPlayers);
162 |
163 | // Act
164 | var result = await _controller.JoinSession(new JoinSessionRequest{PlayerId= player1.Id, SessionId= session.Id});
165 |
166 | // Assert
167 | var oKResult = Assert.IsType(result);
168 | Assert.NotNull(oKResult.Value);
169 | }
170 |
171 | [Fact]
172 | public async Task JoinSession_ShouldReturnBadRequest_WhenPlayerHasNotAccess()
173 | {
174 | // Arrange
175 | var player1 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
176 | var player2 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
177 | var player3 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
178 |
179 | var session = new Session {Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 2};
180 | var sessionPlayer1 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player1.Id};
181 | var sessionPlayer2 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player2.Id};
182 |
183 | var sessionPlayers = new List();
184 | sessionPlayers.Add(sessionPlayer1);
185 | sessionPlayers.Add(sessionPlayer2);
186 |
187 | // var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
188 | _sessionRepositoryMock.Setup(sr => sr.GetSessionById(session.Id)).ReturnsAsync(session);
189 | _sessionRepositoryMock.Setup(sr => sr.GetSessionPlayersBySessionId(session.Id)).ReturnsAsync(sessionPlayers);
190 |
191 | // Act
192 | var result = await _controller.JoinSession(new JoinSessionRequest{PlayerId= player3.Id, SessionId= session.Id});
193 |
194 | // Assert
195 | var badRequestResult = Assert.IsType(result);
196 | Assert.NotNull(badRequestResult.Value);
197 | Assert.Contains("You don't have permission to join this session", badRequestResult.Value.ToString());
198 | }
199 |
200 | [Fact]
201 | public async Task JoinSession_ShouldReturnBadRequest_WhenSessionIdIsInvalid()
202 | {
203 | // Arrange
204 | var player1 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
205 | var player2 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
206 | var player3 = new Player { Id = Guid.NewGuid(), Name = "TestPlayer", LatencyMilliseconds = 50 };
207 |
208 | var session = new Session {Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 2};
209 | var sessionPlayer1 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player1.Id};
210 | var sessionPlayer2 = new SessionPlayer {Id = Guid.NewGuid(), SessionId = session.Id, PlayerId = player2.Id};
211 |
212 | var sessionPlayers = new List();
213 | sessionPlayers.Add(sessionPlayer1);
214 | sessionPlayers.Add(sessionPlayer2);
215 |
216 | // var _queuedPlayer = QueuedPlayer.CreateQueuedPlayerFromPlayer(player);
217 | _sessionRepositoryMock.Setup(sr => sr.GetSessionById(session.Id)).ReturnsAsync(session);
218 | _sessionRepositoryMock.Setup(sr => sr.GetSessionPlayersBySessionId(session.Id)).ReturnsAsync(sessionPlayers);
219 |
220 | // Act
221 | var invalidSessionId = player1.Id;
222 | var result = await _controller.JoinSession(new JoinSessionRequest{PlayerId= player1.Id, SessionId= invalidSessionId});
223 |
224 | // Assert
225 | var badRequestResult = Assert.IsType(result);
226 | Assert.NotNull(badRequestResult.Value);
227 | Assert.Contains("Invalid Session", badRequestResult.Value.ToString());
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Rovio.MatchMaking.Console.Tests/SessionMatchMakerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Microsoft.EntityFrameworkCore;
5 | using Moq;
6 | using Rovio.MatchMaking.Console.Services;
7 | using Rovio.MatchMaking.Repositories;
8 | using Rovio.MatchMaking.Repositories.Data;
9 | using Xunit;
10 |
11 | namespace Rovio.MatchMaking.Console.Tests
12 | {
13 | public class SessionMatchMakerTests
14 | {
15 | private readonly Mock _queuedPlayerRepositoryMock;
16 | private readonly Mock _sessionRepositoryMock;
17 | private readonly AppDbContext _context;
18 | private readonly SessionMatchMaker _sessionMatchMaker;
19 |
20 | public SessionMatchMakerTests()
21 | {
22 | // Create an in-memory database for testing
23 | var options = new DbContextOptionsBuilder()
24 | .UseInMemoryDatabase(databaseName: "TestDatabase")
25 | .Options;
26 |
27 | //TODO: remove direct database related context and stuff
28 | _context = new AppDbContext(options);
29 | _queuedPlayerRepositoryMock = new Mock();
30 | _sessionRepositoryMock = new Mock();
31 |
32 | // Initialize the SessionMatchmaker with mocked dependencies
33 | _sessionMatchMaker = new SessionMatchMaker(
34 | _context,
35 | _queuedPlayerRepositoryMock.Object,
36 | _sessionRepositoryMock.Object
37 | );
38 | }
39 |
40 | [Fact]
41 | public async Task CreatePlayersMapAsync_ShouldReturnCorrectMap1()
42 | {
43 | // Arrange
44 | var players = new List
45 | {
46 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid(), LatencyLevel = 1, CreatedAt = DateTime.UtcNow },
47 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid(), LatencyLevel = 2, CreatedAt = DateTime.UtcNow }
48 | };
49 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.GetAllQueuedPlayersAsync()).ReturnsAsync(players);
50 |
51 | // Act
52 | var result = await _sessionMatchMaker.CreateQueuedPlayersMapAsync();
53 |
54 | // Assert
55 | Assert.NotNull(result);
56 | Assert.Equal(2, result.Count);
57 | Assert.Contains(1, result.Keys);
58 | Assert.Equal(1, result[1].Count);
59 | Assert.Equal(players[0].PlayerId, result[1][0].PlayerId);
60 | Assert.Contains(2, result.Keys);
61 | Assert.Equal(1, result[2].Count);
62 | Assert.Equal(players[1].PlayerId, result[2][0].PlayerId);
63 | }
64 |
65 | [Fact]
66 | public async Task CreatePlayersMapAsync_ShouldReturnCorrectMap2()
67 | {
68 | // Arrange
69 | var players = new List
70 | {
71 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1, CreatedAt = DateTime.UtcNow },
72 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2, CreatedAt = DateTime.UtcNow },
73 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2, CreatedAt = DateTime.UtcNow },
74 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2, CreatedAt = DateTime.UtcNow },
75 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 3, CreatedAt = DateTime.UtcNow },
76 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 3, CreatedAt = DateTime.UtcNow },
77 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 4, CreatedAt = DateTime.UtcNow },
78 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 4, CreatedAt = DateTime.UtcNow },
79 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 4, CreatedAt = DateTime.UtcNow },
80 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 4, CreatedAt = DateTime.UtcNow },
81 | new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 4, CreatedAt = DateTime.UtcNow },
82 | };
83 | _queuedPlayerRepositoryMock.Setup(qpr => qpr.GetAllQueuedPlayersAsync()).ReturnsAsync(players);
84 |
85 | // Act
86 | var result = await _sessionMatchMaker.CreateQueuedPlayersMapAsync();
87 |
88 | // Assert
89 | Assert.NotNull(result);
90 | Assert.Equal(4, result.Count);
91 | Assert.Contains(1, result.Keys);
92 | Assert.Equal(1, result[1].Count);
93 | Assert.Equal(players[0].PlayerId, result[1][0].PlayerId);
94 | Assert.Contains(2, result.Keys);
95 | Assert.Equal(3, result[2].Count);
96 | Assert.Equal(players[1].PlayerId, result[2][0].PlayerId);
97 | Assert.Equal(players[2].PlayerId, result[2][1].PlayerId);
98 | Assert.Equal(players[3].PlayerId, result[2][2].PlayerId);
99 | }
100 |
101 |
102 | [Fact]
103 | public async Task CreateActiveSessionsMapAsync_ShouldReturnCorrectMap1()
104 | {
105 | // Arrange
106 | var sessions = new List
107 | {
108 | new Session { Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
109 | new Session { Id = Guid.NewGuid(), LatencyLevel = 2, JoinedCount = 2, CreatedAt = DateTime.UtcNow },
110 | };
111 | _sessionRepositoryMock.Setup(sr => sr.GetAllActiveSessionsAsync()).ReturnsAsync(sessions);
112 |
113 | // Act
114 | var result = await _sessionMatchMaker.CreateActiveSessionsMapAsync();
115 |
116 | // Assert
117 | Assert.NotNull(result);
118 | Assert.Equal(2, result.Count);
119 | Assert.Contains(1, result.Keys);
120 | Assert.Equal(1, result[1].Count);
121 | Assert.Equal(sessions[0].Id, result[1][0].Id);
122 | Assert.Contains(2, result.Keys);
123 | Assert.Equal(1, result[2].Count);
124 | Assert.Equal(sessions[1].Id, result[2][0].Id);
125 | }
126 |
127 | [Fact]
128 | public async Task CreateActiveSessionsMapAsync_ShouldReturnCorrectMap2()
129 | {
130 | // Arrange
131 | var sessions = new List
132 | {
133 | new Session { Id = Guid.NewGuid(), LatencyLevel = 1, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
134 | new Session { Id = Guid.NewGuid(), LatencyLevel = 2, JoinedCount = 2, CreatedAt = DateTime.UtcNow },
135 | new Session { Id = Guid.NewGuid(), LatencyLevel = 2, JoinedCount = 2, CreatedAt = DateTime.UtcNow },
136 | new Session { Id = Guid.NewGuid(), LatencyLevel = 3, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
137 | new Session { Id = Guid.NewGuid(), LatencyLevel = 3, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
138 | new Session { Id = Guid.NewGuid(), LatencyLevel = 3, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
139 | new Session { Id = Guid.NewGuid(), LatencyLevel = 4, JoinedCount = 1, CreatedAt = DateTime.UtcNow },
140 | };
141 | _sessionRepositoryMock.Setup(sr => sr.GetAllActiveSessionsAsync()).ReturnsAsync(sessions);
142 |
143 | // Act
144 | var result = await _sessionMatchMaker.CreateActiveSessionsMapAsync();
145 |
146 | // Assert
147 | Assert.NotNull(result);
148 | Assert.Equal(4, result.Count);
149 | Assert.Contains(1, result.Keys);
150 | Assert.Equal(1, result[1].Count);
151 | Assert.Equal(sessions[0].Id, result[1][0].Id);
152 | //
153 | Assert.Contains(2, result.Keys);
154 | Assert.Equal(2, result[2].Count);
155 | Assert.Equal(sessions[1].Id, result[2][0].Id);
156 | Assert.Equal(sessions[2].Id, result[2][1].Id);
157 | //
158 | Assert.Contains(3, result.Keys);
159 | Assert.Equal(3, result[3].Count);
160 | Assert.Equal(sessions[3].Id, result[3][0].Id);
161 | Assert.Equal(sessions[4].Id, result[3][1].Id);
162 | Assert.Equal(sessions[5].Id, result[3][2].Id);
163 | //
164 | Assert.Contains(4, result.Keys);
165 | Assert.Equal(1, result[4].Count);
166 | Assert.Equal(sessions[6].Id, result[4][0].Id);
167 | }
168 |
169 | [Fact]
170 | public async Task RemoveAttendedPlayerIdsFromMap_ShouldRemoveAllAttendedPlayers()
171 | {
172 | // Arrange
173 | var player1 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
174 | var player2 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
175 | var player3 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2 };
176 | var queuedPlayersMap = new Dictionary>
177 | {
178 | { 1, new List { player1, player2 } },
179 | { 2, new List { player3 } }
180 | };
181 | var attendedPlayerIds = new List { player1.PlayerId, player3.PlayerId };
182 |
183 | // Act
184 | var result = await _sessionMatchMaker.RemoveAttendedPlayerIdsFromMap(queuedPlayersMap, attendedPlayerIds);
185 |
186 | // Assert
187 | Assert.Equal(1, result.Count);
188 | Assert.Contains(1, result.Keys);
189 | Assert.Equal(player2.PlayerId, result[1][0].PlayerId);
190 | }
191 |
192 | [Fact]
193 | public async Task RemoveAttendedPlayerIdsFromMap_ShouldNotRemoveNonAttendedPlayers()
194 | {
195 | // Arrange
196 | var player1 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
197 | var player2 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
198 | var player3 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2 };
199 | var queuedPlayersMap = new Dictionary>
200 | {
201 | { 1, new List { player1, player2 } },
202 | { 2, new List { player3 } }
203 | };
204 | var attendedPlayerIds = new List(); // No attended players
205 |
206 | // Act
207 | var result = await _sessionMatchMaker.RemoveAttendedPlayerIdsFromMap(queuedPlayersMap, attendedPlayerIds);
208 |
209 | // Assert
210 | Assert.Equal(2, result.Count);
211 | Assert.Contains(1, result.Keys);
212 | Assert.Contains(2, result.Keys);
213 | Assert.Equal(player1.PlayerId, result[1][0].PlayerId);
214 | Assert.Equal(player2.PlayerId, result[1][1].PlayerId);
215 | Assert.Equal(player3.PlayerId, result[2][0].PlayerId);
216 | }
217 |
218 | [Fact]
219 | public async Task RemoveAttendedPlayerIdsFromMap_ShouldHandleEmptyMap()
220 | {
221 | // Arrange
222 | var queuedPlayersMap = new Dictionary>();
223 | var attendedPlayerIds = new List { Guid.NewGuid(), Guid.NewGuid() }; // Some attended player IDs
224 |
225 | // Act
226 | var result = await _sessionMatchMaker.RemoveAttendedPlayerIdsFromMap(queuedPlayersMap, attendedPlayerIds);
227 |
228 | // Assert
229 | Assert.Empty(result); // The result should also be an empty map
230 | }
231 |
232 | [Fact]
233 | public async Task RemoveAttendedPlayerIdsFromMap_ShouldRemoveAttendedPlayersAcrossDifferentLatencyLevels()
234 | {
235 | // Arrange
236 | var player1 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
237 | var player2 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 1 };
238 | var player3 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 2 };
239 | var player4 = new QueuedPlayer { PlayerId = Guid.NewGuid(), LatencyLevel = 3 };
240 | var queuedPlayersMap = new Dictionary>
241 | {
242 | { 1, new List { player1, player2 } },
243 | { 2, new List { player3 } },
244 | { 3, new List { player4 } }
245 | };
246 | var attendedPlayerIds = new List { player1.PlayerId, player4.PlayerId };
247 |
248 | // Act
249 | var result = await _sessionMatchMaker.RemoveAttendedPlayerIdsFromMap(queuedPlayersMap, attendedPlayerIds);
250 |
251 | // Assert
252 | Assert.Equal(2, result.Count);
253 | Assert.Contains(1, result.Keys);
254 | Assert.Contains(2, result.Keys);
255 | Assert.Equal(1, result[1].Count);
256 | Assert.Equal(1, result[1].Count);
257 | Assert.Equal(player2.PlayerId, result[1][0].PlayerId);
258 | Assert.Equal(player3.PlayerId, result[2][0].PlayerId);
259 | }
260 |
261 | [Fact]
262 | public async Task AddQueuedPlayersToActiveSessions_ShouldAddPlayersSuccessfully()
263 | {
264 | // Arrange
265 | var playerId = Guid.NewGuid();
266 | var queuedPlayerId = Guid.NewGuid();
267 | var sessionId = Guid.NewGuid();
268 | var queuedPlayersMap = new Dictionary>
269 | {
270 | { 1, new List { new QueuedPlayer { PlayerId = playerId, Id = queuedPlayerId } } }
271 | };
272 |
273 | var activeSessionsMap = new Dictionary>
274 | {
275 | { 1, new List { new Session { Id = sessionId, JoinedCount = 0 } } }
276 | };
277 |
278 | _sessionRepositoryMock.Setup(repo => repo.AddPlayerToSessionAsync(It.IsAny(), It.IsAny()))
279 | .ReturnsAsync(new SessionPlayer{});
280 | _queuedPlayerRepositoryMock.Setup(repo => repo.DeleteQueuedPlayerAsync(It.IsAny()))
281 | .Returns(Task.CompletedTask);
282 |
283 | // Act
284 | var result = await _sessionMatchMaker.AddQueuedPlayersToActiveSessions(queuedPlayersMap, activeSessionsMap);
285 |
286 | // Assert
287 | Assert.Single(result); // Only one player should be added
288 | Assert.Equal(playerId, result[0]);
289 | Assert.Equal(1, activeSessionsMap[1][0].JoinedCount); // Ensure the joined count increased
290 | }
291 |
292 | [Fact]
293 | public async Task AddQueuedPlayersToActiveSessions_ShouldAddPlayersSuccessfully2()
294 | {
295 | // Arrange
296 | var queuedPlayersMap = new Dictionary>
297 | {
298 | { 1, new List {
299 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
300 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
301 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
302 | }
303 | },
304 | { 2, new List {
305 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
306 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
307 | }
308 | },
309 | { 3, new List {
310 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() }
311 | }
312 | }
313 | };
314 |
315 | var activeSessionsMap = new Dictionary>
316 | {
317 | { 1, new List { new Session { Id = Guid.NewGuid(), JoinedCount = 9 } } },
318 | { 2, new List { new Session { Id = Guid.NewGuid(), JoinedCount = 5 } } },
319 | { 3, new List { new Session { Id = Guid.NewGuid(), JoinedCount = 8 } } }
320 | };
321 |
322 | _sessionRepositoryMock.Setup(repo => repo.AddPlayerToSessionAsync(It.IsAny(), It.IsAny()))
323 | .ReturnsAsync(new SessionPlayer{});
324 | _queuedPlayerRepositoryMock.Setup(repo => repo.DeleteQueuedPlayerAsync(It.IsAny()))
325 | .Returns(Task.CompletedTask);
326 |
327 | // Act
328 | var result = await _sessionMatchMaker.AddQueuedPlayersToActiveSessions(queuedPlayersMap, activeSessionsMap);
329 |
330 | // Assert
331 | Assert.Equal(4, result.Count);
332 | Assert.Equal(queuedPlayersMap[1][0].PlayerId, result[0]); // The first player of latency level of 1
333 | Assert.Equal(queuedPlayersMap[2][0].PlayerId, result[1]); // The first player of latency level of 2
334 | Assert.Equal(queuedPlayersMap[2][1].PlayerId, result[2]); // The second player of latency level of 2
335 | Assert.Equal(queuedPlayersMap[3][0].PlayerId, result[3]); // The first player of latency level of 3
336 | }
337 |
338 | [Fact]
339 | public async Task CreateSessionForRemainedPlayers_ShouldAddPlayersSuccessfully()
340 | {
341 | // Arrange
342 | var queuedPlayersMap = new Dictionary>
343 | {
344 | { 1, new List {
345 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
346 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
347 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
348 | }
349 | },
350 | { 2, new List {
351 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
352 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() },
353 | }
354 | },
355 | { 3, new List {
356 | new QueuedPlayer { Id = Guid.NewGuid(), PlayerId = Guid.NewGuid() }
357 | }
358 | }
359 | };
360 |
361 | // Cloning the queuedPlayersMap, because It will be manipulated by the code
362 | Dictionary> queuedPlayersMapCp = new Dictionary>(queuedPlayersMap.Count, queuedPlayersMap.Comparer);
363 | foreach (var latencyLevel in queuedPlayersMap.Keys)
364 | {
365 | var list = new List();
366 | foreach (var qp in queuedPlayersMap[latencyLevel]) {
367 | list.Add(qp);
368 | }
369 | queuedPlayersMapCp[latencyLevel] = list;
370 | }
371 | //
372 |
373 | _sessionRepositoryMock.Setup(repo => repo.CreateNewAsync(1, 0, It.IsAny()))
374 | .ReturnsAsync(new Session{});
375 | _sessionRepositoryMock.Setup(repo => repo.CreateNewAsync(2, 0, It.IsAny()))
376 | .ReturnsAsync(new Session{});
377 | _sessionRepositoryMock.Setup(repo => repo.AddPlayerToSessionAsync(It.IsAny(), It.IsAny()))
378 | .ReturnsAsync(new SessionPlayer{});
379 | _queuedPlayerRepositoryMock.Setup(repo => repo.DeleteQueuedPlayerAsync(It.IsAny()))
380 | .Returns(Task.CompletedTask);
381 |
382 | // Act
383 | var result = await _sessionMatchMaker.CreateSessionForRemainedPlayers(queuedPlayersMap);
384 |
385 | // Assert
386 | Assert.Equal(5, result.Count);
387 | Assert.Equal(queuedPlayersMapCp[1][0].PlayerId, result[0]); // The first player of latency level of 1
388 | Assert.Equal(queuedPlayersMapCp[1][1].PlayerId, result[1]); // The second player of latency level of 1
389 | Assert.Equal(queuedPlayersMapCp[1][2].PlayerId, result[2]); // The third player of latency level of 1
390 | Assert.Equal(queuedPlayersMapCp[2][0].PlayerId, result[3]); // The first player of latency level of 2
391 | Assert.Equal(queuedPlayersMapCp[2][1].PlayerId, result[4]); // The second player of latency level of 2
392 | }
393 | }
394 | }
395 |
--------------------------------------------------------------------------------