├── 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 | ![Logo](./assets/logo2.webp) 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 | ![System Design Diagram](./assets/rovio-task_v2.drawio.png) 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 | --------------------------------------------------------------------------------