diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 4e951fe..0132e36 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Setup .NET
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
- dotnet-version: 5.0.201
+ dotnet-version: '6.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
diff --git a/Client/Client - Backup.csproj b/Client/Client - Backup.csproj
deleted file mode 100644
index 9405ab1..0000000
--- a/Client/Client - Backup.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- Exe
- net5.0
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Client/Client.Account/Client.Account.csproj b/Client/Client.Account/Client.Account.csproj
new file mode 100644
index 0000000..d0005cc
--- /dev/null
+++ b/Client/Client.Account/Client.Account.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Client/Client.Account/Enums/MenuTypes.cs b/Client/Client.Account/Enums/MenuTypes.cs
new file mode 100644
index 0000000..614a7ff
--- /dev/null
+++ b/Client/Client.Account/Enums/MenuTypes.cs
@@ -0,0 +1,9 @@
+namespace Client.Account.Enums;
+
+internal enum MenuTypes
+{
+ Unknown = 0,
+ SignUp = 1,
+ Login = 2,
+ Back = 3,
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Extensions/EnumExtensions.cs b/Client/Client.Account/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..03c3339
--- /dev/null
+++ b/Client/Client.Account/Extensions/EnumExtensions.cs
@@ -0,0 +1,39 @@
+using Client.Account.Enums;
+
+namespace Client.Account.Extensions;
+
+internal static class EnumExtensions
+{
+ internal static string GetDisplayName(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Back => "Back",
+ MenuTypes.Login => "Login",
+ MenuTypes.SignUp => "Sign Up",
+ _ => "Unknown",
+ };
+ }
+
+ internal static MenuTypes TryGetMenuType(this string? stringInput)
+ {
+ return stringInput switch
+ {
+ "3" => MenuTypes.Back,
+ "2" => MenuTypes.Login,
+ "1" => MenuTypes.SignUp,
+ _ => MenuTypes.Unknown,
+ };
+ }
+
+ internal static int GetValue(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Back => 3,
+ MenuTypes.Login => 2,
+ MenuTypes.SignUp => 1,
+ _ => 0,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs b/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..476abe6
--- /dev/null
+++ b/Client/Client.Account/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,19 @@
+using Client.Account.Menus;
+using Client.Account.Services;
+using Client.Account.Services.Interfaces;
+using RockPaperScissors.Common.Client;
+
+namespace Client.Account.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IAccountService CreateAccountService(this IClient client)
+ {
+ return new AccountService(client);
+ }
+
+ public static IAccountMenu CreateAccountMenu(this IAccountService accountService)
+ {
+ return new AccountMenu(accountService);
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Menus/AccountMenu.cs b/Client/Client.Account/Menus/AccountMenu.cs
new file mode 100644
index 0000000..b467d1f
--- /dev/null
+++ b/Client/Client.Account/Menus/AccountMenu.cs
@@ -0,0 +1,160 @@
+using Client.Account.Enums;
+using Client.Account.Extensions;
+using Client.Account.Services;
+using Client.Account.Services.Interfaces;
+using RockPaperScissors.Common.Enums;
+using RockPaperScissors.Common.Extensions;
+
+namespace Client.Account.Menus;
+
+internal sealed class AccountMenu : IAccountMenu
+{
+ private readonly IAccountService _accountService;
+
+ public AccountMenu(IAccountService accountService)
+ {
+ _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService));
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ "Account Menu:".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.SignUp.GetValue()}.\t{MenuTypes.SignUp.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Login.GetValue()}.\t{MenuTypes.Login.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Back.GetValue()}.\t{MenuTypes.Back.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+
+ "\nPlease select an item from the list".Print(ConsoleColor.Green);
+
+ Console.Write("Select -> ");
+
+ var menuType = Console.ReadLine().TryGetMenuType();
+
+ if (menuType is MenuTypes.Unknown)
+ {
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+
+ continue;
+ }
+
+ switch (menuType)
+ {
+ case MenuTypes.SignUp:
+ var isRegistered = await RegisterAsync(cancellationToken);
+
+ if (!isRegistered)
+ {
+ "\nPress any key to back to the start up menu list!".Print(ConsoleColor.Cyan);
+ Console.ReadKey();
+ Console.Clear();
+
+ continue;
+ }
+
+ "Trying logging in...".Print(ConsoleColor.White);
+ Console.Clear();
+
+ return;
+
+ case MenuTypes.Login:
+ var isSuccess = await LoginAsync(cancellationToken);
+
+ if (isSuccess)
+ {
+ "\nPress any key to back to the start up menu list!".Print(ConsoleColor.Cyan);
+ Console.ReadKey();
+ Console.Clear();
+
+ return;
+ }
+
+ continue;
+
+ case MenuTypes.Back:
+ Console.Clear();
+
+ return;
+
+ default:
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+ continue;
+ }
+ }
+ }
+
+ public async Task LogoutAsync(CancellationToken cancellationToken)
+ {
+ if (!_accountService.IsAuthorized())
+ {
+ return true;
+ }
+
+ var user = _accountService.GetUser();
+
+ var response = await _accountService.LogoutAsync(user.SessionId, cancellationToken);
+
+ return response.Match(
+ _ =>
+ {
+ "Successfully signed out".Print(ConsoleColor.DarkGreen);
+
+ return true;
+ },
+ exception =>
+ {
+ exception.Message.Print(ConsoleColor.Red);
+
+ return false;
+ });
+ }
+
+ private async Task RegisterAsync(CancellationToken cancellationToken)
+ {
+ ("We are glad to welcome you in the registration form!\n" +
+ "Please enter the required details\n" +
+ "to register an account on the platform")
+ .Print(ConsoleColor.Magenta);
+
+ var login = "Login: ".BuildString(StringDestination.Login);
+ var password = "Password: ".BuildString(StringDestination.Password);
+
+ var response = await _accountService.SignUpAsync(login, password, cancellationToken);
+
+ return response.Match(
+ _ =>
+ {
+ "Successfully registered!".Print(ConsoleColor.Green);
+
+ return true;
+ },
+ exception =>
+ {
+ exception.Message.Print(ConsoleColor.Red);
+
+ return false;
+ });
+ }
+
+ private async Task LoginAsync(CancellationToken cancellationToken)
+ {
+ var login = "Login: ".BuildString(StringDestination.Login);
+ var password = "Password: ".BuildString(StringDestination.Password, isNeedConfirmation: true);
+
+ var response = await _accountService.LoginAsync(login, password, cancellationToken);
+
+ return response.Match(
+ _ =>
+ {
+ "Successfully signed in".Print(ConsoleColor.DarkGreen);
+
+ return true;
+ },
+ exception =>
+ {
+ exception.Message.Print(ConsoleColor.Red);
+
+ return false;
+ });
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Menus/IAccountMenu.cs b/Client/Client.Account/Menus/IAccountMenu.cs
new file mode 100644
index 0000000..49d6cab
--- /dev/null
+++ b/Client/Client.Account/Menus/IAccountMenu.cs
@@ -0,0 +1,8 @@
+namespace Client.Account.Menus;
+
+public interface IAccountMenu
+{
+ Task StartAsync(CancellationToken cancellationToken);
+
+ Task LogoutAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Services/AccountService.cs b/Client/Client.Account/Services/AccountService.cs
new file mode 100644
index 0000000..db08747
--- /dev/null
+++ b/Client/Client.Account/Services/AccountService.cs
@@ -0,0 +1,105 @@
+using Client.Account.Services.Interfaces;
+using OneOf;
+using RockPaperScissors.Common;
+using RockPaperScissors.Common.Client;
+using RockPaperScissors.Common.Extensions;
+using RockPaperScissors.Common.Requests;
+using RockPaperScissors.Common.Responses;
+
+namespace Client.Account.Services;
+
+internal sealed class AccountService : IAccountService
+{
+ private readonly IClient _client;
+ private IUser _user = null!;
+
+ public AccountService(IClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public Task> SignUpAsync(
+ string login, string password, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(login))
+ {
+ return Task.FromResult(OneOf.FromT1(new CustomException("Login must not be 'null' or '\"\"'")));
+ }
+
+ if (string.IsNullOrEmpty(password))
+ {
+ return Task.FromResult(OneOf.FromT1(new CustomException("Password must not be 'null' or '\"\"'")));
+ }
+
+ var request = new RegisterRequest
+ {
+ Login = login,
+ Password = password
+ };
+
+ var response =
+ _client.PostAsync("api/Account/register", request, cancellationToken);
+
+ return response;
+ }
+
+ public async Task> LoginAsync(
+ string login, string password, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(login))
+ {
+ return new CustomException("Login must not be 'null' or '\"\"'");
+ }
+
+ if (string.IsNullOrEmpty(password))
+ {
+ return new CustomException("Password must not be 'null' or '\"\"'");
+ }
+
+ var request = new LoginRequest
+ {
+ Login = login,
+ Password = password
+ };
+
+ var response =
+ await _client.PostAsync("api/Account/login", request, cancellationToken);
+
+ if (!response.IsT0)
+ {
+ return response;
+ }
+
+ var loginResponse = response.AsT0;
+ _user = new User(loginResponse.Token, loginResponse.Login);
+
+ return response;
+ }
+
+ public Task> LogoutAsync(
+ string? token, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ return Task.FromResult(OneOf.FromT1(new CustomException("Token must not be 'null' or '\"\"'")));
+ }
+
+ var queryUrl = new QueryBuilder(1)
+ {
+ { "sessionId", token }
+ };
+ var url = $"api/Account/logout{queryUrl}";
+
+ var response =
+ _client.GetAsync(url, ("Authorization", token), cancellationToken);
+
+ return response;
+ }
+
+ public IUser GetUser() => _user;
+
+ public bool IsAuthorized()
+ {
+ return _user is not null && _user.IsAuthorized;
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Services/Interfaces/IAccountService.cs b/Client/Client.Account/Services/Interfaces/IAccountService.cs
new file mode 100644
index 0000000..0354256
--- /dev/null
+++ b/Client/Client.Account/Services/Interfaces/IAccountService.cs
@@ -0,0 +1,25 @@
+using OneOf;
+using RockPaperScissors.Common;
+using RockPaperScissors.Common.Responses;
+
+namespace Client.Account.Services.Interfaces;
+
+public interface IAccountService
+{
+ Task> SignUpAsync(string login,
+ string password,
+ CancellationToken cancellationToken = default);
+
+ Task> LoginAsync(
+ string login,
+ string password,
+ CancellationToken cancellationToken = default);
+
+ Task> LogoutAsync(
+ string? token,
+ CancellationToken cancellationToken = default);
+
+ IUser GetUser();
+
+ bool IsAuthorized();
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Services/Interfaces/IUser.cs b/Client/Client.Account/Services/Interfaces/IUser.cs
new file mode 100644
index 0000000..998c032
--- /dev/null
+++ b/Client/Client.Account/Services/Interfaces/IUser.cs
@@ -0,0 +1,12 @@
+namespace Client.Account.Services.Interfaces;
+
+public interface IUser
+{
+ public string? SessionId { get; }
+
+ public string? Login { get; }
+
+ bool IsAuthorized => !string.IsNullOrEmpty(SessionId) && !string.IsNullOrEmpty(Login);
+
+ string GetBearerToken();
+}
\ No newline at end of file
diff --git a/Client/Client.Account/Services/User.cs b/Client/Client.Account/Services/User.cs
new file mode 100644
index 0000000..39f5692
--- /dev/null
+++ b/Client/Client.Account/Services/User.cs
@@ -0,0 +1,27 @@
+using Client.Account.Services.Interfaces;
+
+namespace Client.Account.Services;
+
+internal sealed class User : IUser
+{
+ public static readonly User Default = new(string.Empty, string.Empty);
+
+ internal User(string sessionId, string login)
+ {
+ SessionId = sessionId;
+ Login = login;
+ }
+
+ public string SessionId { get; }
+
+ public string Login { get; }
+
+
+ public bool IsAuthorized => !string.IsNullOrEmpty(SessionId) && !string.IsNullOrEmpty(Login);
+
+
+ public string GetBearerToken()
+ {
+ return $"Bearer {SessionId}";
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Host/Client.Host.csproj b/Client/Client.Host/Client.Host.csproj
new file mode 100644
index 0000000..5bfac12
--- /dev/null
+++ b/Client/Client.Host/Client.Host.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net6
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/ClientAppEmulator.cs b/Client/Client.Host/ClientAppEmulator.cs
similarity index 91%
rename from Client/ClientAppEmulator.cs
rename to Client/Client.Host/ClientAppEmulator.cs
index 1efd022..6adbb76 100644
--- a/Client/ClientAppEmulator.cs
+++ b/Client/Client.Host/ClientAppEmulator.cs
@@ -1,18 +1,18 @@
-using System;
+/*using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
-using Client.Models;
-using Client.Models.Interfaces;
-using Client.Services;
-using Client.Services.RequestProcessor;
-using Client.Services.RequestProcessor.RequestModels.Impl;
+using Client.Host.Models;
+using Client.Host.Models.Interfaces;
+using Client.Host.Services;
+using Client.Host.Services.RequestProcessor;
+using Client.Host.Services.RequestProcessor.RequestModels.Impl;
using Newtonsoft.Json;
using NLog;
-namespace Client
+namespace Client.Host
{
public class ClientAppEmulator
{
@@ -135,63 +135,7 @@ private async Task StartMenu(CancellationToken token)
}
}
- private async Task PlayerMenu()
- {
- while (true)
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor($"***\nHello, {_playerAccount.Login}\n" +
- "Please choose option", ConsoleColor.Cyan);
- ColorTextWriterService.PrintLineMessageWithSpecialColor("1.\tPlay with bot\n" +
- "2\tCreate room\n" +
- "3\tJoin Private room\n" +
- "4\tJoin Public room\n" +
- "5\tShow Statistics\n" +
- "6\tLog out", ConsoleColor.Yellow);
-
- ColorTextWriterService.PrintLineMessageWithSpecialColor("\nPlease select an item from the list", ConsoleColor.Green);
- Console.Write("Select -> ");
- var passed = int.TryParse(Console.ReadLine(), out int playersMenuInput);
- if (!passed)
- {
- logger.Trace($"Not passed argument to player Menu");
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Unsupported input", ConsoleColor.Red);
- continue;
- }
- switch (playersMenuInput)
- {
- case 1:
- Console.Clear();
- await JoinRoomWithBot();
- break;
- case 2:
- Console.Clear();
- await CreationRoom();
- break;
- case 3:
- Console.Clear();
- await JoinPrivateRoom();
- break;
- case 4:
- Console.Clear();
- await JoinPublicRoom();
- break;
- case 5:
- Console.Clear();
- var statistics = await PersonalStatistics(_sessionId);
- Console.WriteLine(statistics+"\n\nPress any key to go back.");
- Console.ReadKey();
- break;
- case 6:
- Console.Clear();
- await Logout();
- return;
- default:
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Unsupported input", ConsoleColor.Red);
- continue;
- }
- }
- }
private async Task JoinRoomWithBot()
{
logger.Trace("JoinRoomWithBot method");
@@ -280,7 +224,7 @@ private async Task JoinPrivateRoom()
logger.Info("Error occured. Probably there is either no room or it is already full");
}
}
- /**/
+ /*#1#
private async Task StartRoomMenu()
{
@@ -312,7 +256,7 @@ private async Task StartRoomMenu()
Console.ReadKey();
}
}
- /**/
+ /*#1#
private async Task MakeYourMove()
{
var move = 0;
@@ -710,6 +654,4 @@ private static void PrintStatistics(IEnumerable statisticsEnumera
}
}
}
-}
-
-
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/Client/Client.Host/Menus/IMainMenu.cs b/Client/Client.Host/Menus/IMainMenu.cs
new file mode 100644
index 0000000..40e613a
--- /dev/null
+++ b/Client/Client.Host/Menus/IMainMenu.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace Client.Host.Menus;
+
+public interface IMainMenu
+{
+ Task PlayerMenu();
+}
\ No newline at end of file
diff --git a/Client/Client.Host/Menus/MainMenu.cs b/Client/Client.Host/Menus/MainMenu.cs
new file mode 100644
index 0000000..99aae7f
--- /dev/null
+++ b/Client/Client.Host/Menus/MainMenu.cs
@@ -0,0 +1,87 @@
+/*using System;
+using System.Threading.Tasks;
+using Client.Host.Models;
+using Client.Host.Services;
+using Client.Host.Services.RequestProcessor;
+
+namespace Client.Host.Menus;
+
+public class MainMenu : IMainMenu
+{
+ private readonly TokenModel _playerAccount;
+ private readonly IRoomService _roomService;
+ private readonly IStatisticsService _statisticsService;
+
+ public MainMenu(TokenModel account,
+ IRequestPerformer requestPerformer,
+ IStatisticsService statisticsService)
+ {
+ _playerAccount = account;
+ _statisticsService = statisticsService;
+ _roomService = new RoomService(_playerAccount, requestPerformer);
+ }
+
+ public async Task PlayerMenu()
+ {
+ while (true)
+ {
+ TextWrite.Print(
+ $"***\nHello, {_playerAccount.Login}\n" +
+ "Please choose option", ConsoleColor.Cyan);
+ TextWrite.Print(
+ "1.\tPlay with bot\n" +
+ "2\tCreate room\n" +
+ "3\tJoin room\n" +
+ "4\tSearch open room\n" +
+ "5\tShow Statistics\n" +
+ "6\tLog out", ConsoleColor.Yellow);
+
+ TextWrite.Print("\nPlease select an item from the list", ConsoleColor.Green);
+
+ Console.Write("Select -> ");
+ var passed = int.TryParse(Console.ReadLine(), out var playersMenuInput);
+ if (!passed)
+ {
+ TextWrite.Print("Unsupported input", ConsoleColor.Red);
+ continue;
+ }
+ switch (playersMenuInput)
+ {
+ case 1:
+ Console.Clear();
+ //await JoinRoomWithBot();
+ var room = await _roomService.CreateRoom(true, true);
+ if (room is null)
+ return;
+ //todo: redirect somewhere
+ break;
+ case 2:
+ Console.Clear();
+ //await CreationRoom();
+ break;
+ case 3:
+ Console.Clear();
+ //await JoinPrivateRoom();
+ break;
+ case 4:
+ Console.Clear();
+ //await JoinPublicRoom();
+ break;
+ case 5:
+ Console.Clear();
+ var statistics = await _statisticsService
+ .GetPersonalStatistics(_playerAccount.BearerToken);
+ Console.WriteLine(statistics+"\n\nPress any key to go back.");
+ Console.ReadKey();
+ break;
+ case 6:
+ Console.Clear();
+ //await Logout();
+ return;
+ default:
+ TextWrite.Print("Unsupported input", ConsoleColor.Red);
+ continue;
+ }
+ }
+ }
+}*/
\ No newline at end of file
diff --git a/Client/Client.Host/Program.cs b/Client/Client.Host/Program.cs
new file mode 100644
index 0000000..0ddc2a5
--- /dev/null
+++ b/Client/Client.Host/Program.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Client.Account.Extensions;
+using Client.StartMenu.Extensions;
+using Client.Statistics.Extensions;
+
+namespace Client.Host;
+
+internal static class Program
+{
+ private static async Task Main()
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ try
+ {
+ var baseAddress = "http://localhost:5000";
+ var client = RockPaperScissors.Common.Client.Client.Create(baseAddress);
+ var accountService = client.CreateAccountService();
+ var statisticsService = client.CreateStatisticsService();
+ var healthCheckService = client.CreateHealthCheckService(cancellationTokenSource);
+
+ var accountMenu = accountService.CreateAccountMenu();
+ var statisticsMenu = statisticsService.CreateStatisticsMenu(accountService);
+ var startMenu = statisticsMenu.CreateStartMenu(accountMenu, healthCheckService);
+
+ await startMenu.PrintAsync(cancellationTokenSource.Token);
+
+ return 0;
+ }
+ catch (TaskCanceledException)
+ {
+ return -1;
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine(exception.Message);
+ Console.WriteLine("Unknown error occured. Closing.");
+
+ return -1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Host/Services/RoomService.cs b/Client/Client.Host/Services/RoomService.cs
new file mode 100644
index 0000000..86e5e6c
--- /dev/null
+++ b/Client/Client.Host/Services/RoomService.cs
@@ -0,0 +1,59 @@
+// using Client.Host.Extensions;
+// using Client.Host.Models;
+// using Client.Host.Services.RequestProcessor;
+// using Client.Host.Services.RequestProcessor.RequestModels;
+// using Client.Host.Services.RequestProcessor.RequestModels.Impl;
+// using Newtonsoft.Json;
+//
+// namespace Client.Host.Services;
+//
+// public class RoomService : IRoomService
+// {
+// private readonly TokenModel _tokenModel;
+// private readonly IRequestPerformer _requestPerformer;
+//
+// public RoomService(TokenModel tokenModel,
+// IRequestPerformer requestPerformer)
+// {
+// _tokenModel = tokenModel;
+// _requestPerformer = requestPerformer;
+// }
+// public async Task CreateRoom(bool isPrivate, bool isTraining)
+// {
+// Console.WriteLine("Trying to create a room.");
+//
+// var options = new RequestOptions
+// {
+// Address = $"room/create?isPrivate={isPrivate}",
+// IsValid = true,
+// Headers = new Dictionary
+// {
+// {
+// "Authorization", _tokenModel.BearerToken
+// },
+// {
+// "X-Training", isTraining.ToString()
+// }
+// },
+// Method = RequestMethod.Post,
+// Body = string.Empty,
+// Name = "Create Room"
+// };
+// var reachedResponse = await _requestPerformer
+// .PerformRequestAsync(options);
+//
+// if (reachedResponse.TryParseJson(out var errorModel))
+// {
+// TextWrite.Print(errorModel.Message, ConsoleColor.Red);
+// return null;
+// }
+//
+// var room = JsonConvert.DeserializeObject(reachedResponse.Content);
+// return room;
+// }
+// }
+//
+// public interface IRoomService
+// {
+// Task CreateRoom(bool isPrivate, bool isTraining);
+// }
\ No newline at end of file
diff --git a/Client/Client.Host/Services/StatisticsService.cs b/Client/Client.Host/Services/StatisticsService.cs
new file mode 100644
index 0000000..f8ce6e0
--- /dev/null
+++ b/Client/Client.Host/Services/StatisticsService.cs
@@ -0,0 +1,74 @@
+// using System;
+// using System.Collections.Generic;
+// using System.Threading.Tasks;
+// using Client.Host.Models;
+// using Client.Host.Models.Interfaces;
+// using Client.Host.Services.RequestProcessor;
+// using Client.Host.Services.RequestProcessor.RequestModels;
+// using Client.Host.Services.RequestProcessor.RequestModels.Impl;
+// using Mapster;
+// using Newtonsoft.Json;
+//
+// namespace Client.Host.Services;
+//
+// public class StatisticsService: IStatisticsService
+// {
+// private readonly IRequestPerformer _requestPerformer;
+//
+// public StatisticsService(IRequestPerformer requestPerformer)
+// {
+// _requestPerformer = requestPerformer;
+// }
+//
+// public async Task GetAllStatistics()
+// {
+// var options = new RequestOptions
+// {
+// ContentType = "none",
+// Address = "stats/all",
+// IsValid = true,
+// Method = RequestMethod.Get,
+// Name = "OverallStats"
+// };
+//
+// var response = await _requestPerformer.PerformRequestAsync(options);
+//
+// var toConvert = JsonConvert.DeserializeObject(response.Content);
+// return toConvert?.Adapt();
+// }
+//
+// public async Task GetPersonalStatistics(string token)
+// {
+// var options = new RequestOptions
+// {
+// Headers = new Dictionary{{"Authorization",token}},
+// ContentType = "none",
+// Address = "stats/personal",
+// IsValid = true,
+// Method = RequestMethod.Get,
+// Name = "PersonalStats"
+// };
+//
+// var response = await _requestPerformer.PerformRequestAsync(options);
+//
+// return response.Content != null ? JsonConvert.DeserializeObject(response.Content) : null;
+// }
+//
+// public Task PrintStatistics(IOverallStatistics[] statistics)
+// {
+// for(var i = 0; i < statistics.Length; i++)
+// {
+// TextWrite.Print($"{i+1}. User: {statistics[i].Account.Login}\n" +
+// $"Score: {statistics[i].Score}", ConsoleColor.White);
+// }
+// Console.Write('\n');
+// return Task.CompletedTask;
+// }
+// }
+//
+// public interface IStatisticsService
+// {
+// Task GetAllStatistics();
+// Task GetPersonalStatistics(string token);
+// Task PrintStatistics(IOverallStatistics[] statistics);
+// }
\ No newline at end of file
diff --git a/Client/Client.MainMenu/Client.MainMenu.csproj b/Client/Client.MainMenu/Client.MainMenu.csproj
new file mode 100644
index 0000000..eb2460e
--- /dev/null
+++ b/Client/Client.MainMenu/Client.MainMenu.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
diff --git a/Client/Client.MainMenu/Enums/MenuTypes.cs b/Client/Client.MainMenu/Enums/MenuTypes.cs
new file mode 100644
index 0000000..adf197d
--- /dev/null
+++ b/Client/Client.MainMenu/Enums/MenuTypes.cs
@@ -0,0 +1,10 @@
+namespace Client.MainMenu.Enums;
+
+internal enum MenuTypes
+{
+ Unknown = 0,
+ PersonalScoreboard = 1,
+ CreateRoom = 2,
+ Logout = 3,
+ Exit = 4,
+}
\ No newline at end of file
diff --git a/Client/Client.MainMenu/Extensions/EnumExtensions.cs b/Client/Client.MainMenu/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..0fafffd
--- /dev/null
+++ b/Client/Client.MainMenu/Extensions/EnumExtensions.cs
@@ -0,0 +1,42 @@
+using Client.MainMenu.Enums;
+
+namespace Client.MainMenu.Extensions;
+
+internal static class EnumExtensions
+{
+ internal static string GetDisplayName(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.PersonalScoreboard => "Personal Statistics",
+ MenuTypes.CreateRoom => "Create room",
+ MenuTypes.Logout => "Logout",
+ MenuTypes.Exit => "Exit",
+ _ => "Unknown",
+ };
+ }
+
+ internal static MenuTypes TryGetMenuType(this string? stringInput)
+ {
+ return stringInput switch
+ {
+ "4" => MenuTypes.Exit,
+ "3" => MenuTypes.Logout,
+ "2" => MenuTypes.CreateRoom,
+ "1" => MenuTypes.PersonalScoreboard,
+ _ => MenuTypes.Unknown,
+ };
+ }
+
+ internal static int GetValue(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Exit => 4,
+ MenuTypes.Logout => 3,
+ MenuTypes.CreateRoom => 2,
+ MenuTypes.PersonalScoreboard => 1,
+ _ => 0,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs b/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..6001356
--- /dev/null
+++ b/Client/Client.MainMenu/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,14 @@
+namespace Client.MainMenu.Extensions;
+
+// public static class ServiceCollectionExtensions
+// {
+// public static IAccountService CreateAccountService(this IClient client)
+// {
+// return new AccountService(client);
+// }
+//
+// public static IAccountMenu CreateAccountMenu(this IAccountService accountService)
+// {
+// return new AccountMenu(accountService);
+// }
+// }
\ No newline at end of file
diff --git a/Client/Client.MainMenu/Services/RoomService.cs b/Client/Client.MainMenu/Services/RoomService.cs
new file mode 100644
index 0000000..d0b0218
--- /dev/null
+++ b/Client/Client.MainMenu/Services/RoomService.cs
@@ -0,0 +1,11 @@
+namespace Client.MainMenu.Services;
+
+public interface IRoomService
+{
+ //Todo : implement
+}
+
+internal sealed class RoomService : IRoomService
+{
+ //Todo : implement
+}
\ No newline at end of file
diff --git a/Client/Client.MainMenu/Services/RoundService.cs b/Client/Client.MainMenu/Services/RoundService.cs
new file mode 100644
index 0000000..2dbfc6a
--- /dev/null
+++ b/Client/Client.MainMenu/Services/RoundService.cs
@@ -0,0 +1,11 @@
+namespace Client.MainMenu.Services;
+
+public interface IRoundService
+{
+ //Todo : implement
+}
+
+internal sealed class RoundService : IRoundService
+{
+ //Todo : implement
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Client.StartMenu.csproj b/Client/Client.StartMenu/Client.StartMenu.csproj
new file mode 100644
index 0000000..4d8587f
--- /dev/null
+++ b/Client/Client.StartMenu/Client.StartMenu.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/Client/Client.StartMenu/Enums/MenuTypes.cs b/Client/Client.StartMenu/Enums/MenuTypes.cs
new file mode 100644
index 0000000..c9524ea
--- /dev/null
+++ b/Client/Client.StartMenu/Enums/MenuTypes.cs
@@ -0,0 +1,9 @@
+namespace Client.StartMenu.Enums;
+
+internal enum MenuTypes
+{
+ Unknown = 0,
+ Account = 1,
+ Leaderboard = 2,
+ Exit = 3,
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Extensions/EnumExtensions.cs b/Client/Client.StartMenu/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..3f1898a
--- /dev/null
+++ b/Client/Client.StartMenu/Extensions/EnumExtensions.cs
@@ -0,0 +1,39 @@
+using Client.StartMenu.Enums;
+
+namespace Client.StartMenu.Extensions;
+
+internal static class EnumExtensions
+{
+ internal static string GetDisplayName(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Account => "Account",
+ MenuTypes.Leaderboard => "Leaderboard",
+ MenuTypes.Exit => "Exit",
+ _ => "Unknown",
+ };
+ }
+
+ internal static MenuTypes TryGetMenuType(this string? stringInput)
+ {
+ return stringInput switch
+ {
+ "3" => MenuTypes.Exit,
+ "2" => MenuTypes.Leaderboard,
+ "1" => MenuTypes.Account,
+ _ => MenuTypes.Unknown,
+ };
+ }
+
+ internal static int GetValue(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Exit => 3,
+ MenuTypes.Leaderboard => 2,
+ MenuTypes.Account => 1,
+ _ => 0,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs b/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..c8f39c7
--- /dev/null
+++ b/Client/Client.StartMenu/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+using Client.Account.Menus;
+using Client.StartMenu.Menus;
+using Client.StartMenu.Services;
+using Client.Statistics.Menus;
+using RockPaperScissors.Common.Client;
+
+namespace Client.StartMenu.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IStartMenu CreateStartMenu(
+ this IStatisticsMenu statisticsMenu,
+ IAccountMenu accountMenu,
+ IHealthCheckService healthCheckService)
+ {
+ ArgumentNullException.ThrowIfNull(accountMenu);
+ ArgumentNullException.ThrowIfNull(statisticsMenu);
+
+ return new Menus.StartMenu(accountMenu, statisticsMenu, healthCheckService);
+ }
+
+ public static IHealthCheckService CreateHealthCheckService(
+ this IClient client,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+ ArgumentNullException.ThrowIfNull(cancellationTokenSource);
+
+ return new HealthCheckService(client, cancellationTokenSource);
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Menus/IStartMenu.cs b/Client/Client.StartMenu/Menus/IStartMenu.cs
new file mode 100644
index 0000000..81967ba
--- /dev/null
+++ b/Client/Client.StartMenu/Menus/IStartMenu.cs
@@ -0,0 +1,6 @@
+namespace Client.StartMenu.Menus;
+
+public interface IStartMenu
+{
+ Task PrintAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Menus/StartMenu.cs b/Client/Client.StartMenu/Menus/StartMenu.cs
new file mode 100644
index 0000000..7053a10
--- /dev/null
+++ b/Client/Client.StartMenu/Menus/StartMenu.cs
@@ -0,0 +1,101 @@
+using Client.Account.Menus;
+using Client.StartMenu.Enums;
+using Client.StartMenu.Extensions;
+using Client.StartMenu.Services;
+using Client.Statistics.Menus;
+using RockPaperScissors.Common.Extensions;
+
+namespace Client.StartMenu.Menus;
+
+internal sealed class StartMenu: IStartMenu
+{
+ private readonly IAccountMenu _accountMenu;
+ private readonly IStatisticsMenu _statisticsMenu;
+ private readonly IHealthCheckService _healthCheckService;
+
+ public StartMenu(
+ IAccountMenu accountMenu,
+ IStatisticsMenu statisticsMenu,
+ IHealthCheckService healthCheckService)
+ {
+ _accountMenu = accountMenu ?? throw new ArgumentNullException(nameof(accountMenu));
+ _statisticsMenu = statisticsMenu ?? throw new ArgumentNullException(nameof(statisticsMenu));
+ _healthCheckService = healthCheckService ?? throw new ArgumentNullException(nameof(healthCheckService));
+ }
+
+ public async Task PrintAsync(CancellationToken cancellationToken)
+ {
+ PrintGreeting();
+
+ await _healthCheckService.ConnectAsync();
+ cancellationToken.ThrowIfCancellationRequested();
+ await _healthCheckService.PingAsync();
+
+ "\nPress any key to show start up menu list.".Print(ConsoleColor.Green);
+
+ Console.ReadKey();
+ Console.Clear();
+
+ await ShowStartAsync(cancellationToken);
+ }
+
+ private async Task ShowStartAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ "Start menu:".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Account.GetValue()}. \t{MenuTypes.Account.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Leaderboard.GetValue()}. \t{MenuTypes.Leaderboard.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Exit.GetValue()}. \t{MenuTypes.Exit.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+
+ "Please select an item from the list".Print(ConsoleColor.Green);
+
+ Console.Write("Select -> ");
+
+ var menuType = Console.ReadLine().TryGetMenuType();
+
+ if (menuType is MenuTypes.Unknown)
+ {
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+
+ continue;
+ }
+
+ switch (menuType)
+ {
+ case MenuTypes.Account:
+ await _accountMenu.StartAsync(cancellationToken);
+ Console.Clear();
+
+ break;
+
+ case MenuTypes.Leaderboard:
+ await _statisticsMenu.StartAsync(cancellationToken);
+ Console.Clear();
+
+ break;
+
+ case MenuTypes.Exit:
+ await _accountMenu.LogoutAsync(cancellationToken);
+ Console.Clear();
+
+ return;
+
+ default:
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+ continue;
+ }
+ }
+ }
+
+ private static void PrintGreeting()
+ {
+ ("VERSION 2.0\n" +
+ "Welcome to the world best game ----> Rock-Paper-Scissors!\n" +
+ "You are given the opportunity to compete with other users in this wonderful game,\n" +
+ "or if you don’t have anyone to play, don’t worry,\n" +
+ "you can find a random player or just try your skill with a bot.").Print(ConsoleColor.White);
+
+ "(c)Ihor Volokhovych & Michael Terekhov".Print(ConsoleColor.Cyan);
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Services/HealthCheckService.cs b/Client/Client.StartMenu/Services/HealthCheckService.cs
new file mode 100644
index 0000000..2a45c0f
--- /dev/null
+++ b/Client/Client.StartMenu/Services/HealthCheckService.cs
@@ -0,0 +1,54 @@
+using RockPaperScissors.Common.Client;
+using RockPaperScissors.Common.Extensions;
+
+namespace Client.StartMenu.Services;
+
+internal sealed class HealthCheckService : IHealthCheckService
+{
+ private readonly IClient _client;
+ private readonly CancellationTokenSource _cancellationTokenSource;
+
+ public HealthCheckService(IClient client, CancellationTokenSource cancellationTokenSource)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _cancellationTokenSource =
+ cancellationTokenSource ?? throw new ArgumentNullException(nameof(cancellationTokenSource));
+ }
+
+ public async Task ConnectAsync()
+ {
+ "\nTrying to connect to the server...".Print(ConsoleColor.White);
+ var result = await _client.GetAsync("/health", _cancellationTokenSource.Token);
+
+ if (result)
+ {
+ "Connected to the server".Print(ConsoleColor.Green);
+
+ return;
+ }
+
+ "Failed to connect. Closing... \nPress any key...".Print(ConsoleColor.Red);
+ _cancellationTokenSource.Cancel();
+ }
+
+ public async Task PingAsync()
+ {
+ await Task.Factory.StartNew(async () =>
+ {
+ "Starting health checks".Print(ConsoleColor.White);
+ while (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ var result = await _client.GetAsync("/health", _cancellationTokenSource.Token);
+
+ if (!result)
+ {
+ "\nConnection to the server lost. Stopping...\nPress any key...".Print(ConsoleColor.Red);
+
+ _cancellationTokenSource.Cancel();
+ }
+
+ await Task.Delay(1000);
+ }
+ }, TaskCreationOptions.LongRunning);
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.StartMenu/Services/IHealthCheckService.cs b/Client/Client.StartMenu/Services/IHealthCheckService.cs
new file mode 100644
index 0000000..a5243b3
--- /dev/null
+++ b/Client/Client.StartMenu/Services/IHealthCheckService.cs
@@ -0,0 +1,8 @@
+namespace Client.StartMenu.Services;
+
+public interface IHealthCheckService
+{
+ Task ConnectAsync();
+
+ Task PingAsync();
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Client.Statistics.csproj b/Client/Client.Statistics/Client.Statistics.csproj
new file mode 100644
index 0000000..bf5f454
--- /dev/null
+++ b/Client/Client.Statistics/Client.Statistics.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/Client/Client.Statistics/Enums/MenuTypes.cs b/Client/Client.Statistics/Enums/MenuTypes.cs
new file mode 100644
index 0000000..843803c
--- /dev/null
+++ b/Client/Client.Statistics/Enums/MenuTypes.cs
@@ -0,0 +1,9 @@
+namespace Client.Statistics.Enums;
+
+internal enum MenuTypes
+{
+ Unknown = 0,
+ Personal = 1,
+ All = 2,
+ Back = 3,
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Extensions/EnumExtensions.cs b/Client/Client.Statistics/Extensions/EnumExtensions.cs
new file mode 100644
index 0000000..e57576f
--- /dev/null
+++ b/Client/Client.Statistics/Extensions/EnumExtensions.cs
@@ -0,0 +1,39 @@
+using Client.Statistics.Enums;
+
+namespace Client.Statistics.EnumExtensions;
+
+internal static class EnumExtensions
+{
+ internal static string GetDisplayName(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Back => "Back",
+ MenuTypes.All => "Top 10 Users",
+ MenuTypes.Personal => "Personal statistics",
+ _ => "Unknown",
+ };
+ }
+
+ internal static MenuTypes TryGetMenuType(this string? stringInput)
+ {
+ return stringInput switch
+ {
+ "3" => MenuTypes.Back,
+ "2" => MenuTypes.All,
+ "1" => MenuTypes.Personal,
+ _ => MenuTypes.Unknown,
+ };
+ }
+
+ internal static int GetValue(this MenuTypes menuTypes)
+ {
+ return menuTypes switch
+ {
+ MenuTypes.Back => 3,
+ MenuTypes.All => 2,
+ MenuTypes.Personal => 1,
+ _ => 0,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs b/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a9b214c
--- /dev/null
+++ b/Client/Client.Statistics/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,20 @@
+using Client.Account.Services;
+using Client.Account.Services.Interfaces;
+using Client.Statistics.Menus;
+using Client.Statistics.Services;
+using RockPaperScissors.Common.Client;
+
+namespace Client.Statistics.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IStatisticsService CreateStatisticsService(this IClient client)
+ {
+ return new StatisticsService(client);
+ }
+
+ public static IStatisticsMenu CreateStatisticsMenu(this IStatisticsService statisticsService, IAccountService accountService)
+ {
+ return new StatisticsMenu(statisticsService, accountService);
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Menus/IStatisticsMenu.cs b/Client/Client.Statistics/Menus/IStatisticsMenu.cs
new file mode 100644
index 0000000..c7b7ebe
--- /dev/null
+++ b/Client/Client.Statistics/Menus/IStatisticsMenu.cs
@@ -0,0 +1,6 @@
+namespace Client.Statistics.Menus;
+
+public interface IStatisticsMenu
+{
+ Task StartAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Menus/StatisticsMenu.cs b/Client/Client.Statistics/Menus/StatisticsMenu.cs
new file mode 100644
index 0000000..f40225e
--- /dev/null
+++ b/Client/Client.Statistics/Menus/StatisticsMenu.cs
@@ -0,0 +1,163 @@
+using Client.Account.Services;
+using Client.Account.Services.Interfaces;
+using Client.Statistics.EnumExtensions;
+using Client.Statistics.Enums;
+using Client.Statistics.Services;
+using RockPaperScissors.Common.Extensions;
+using RockPaperScissors.Common.Responses;
+
+namespace Client.Statistics.Menus;
+
+internal sealed class StatisticsMenu: IStatisticsMenu
+{
+ private readonly IStatisticsService _statisticsService;
+ private readonly IAccountService _accountService;
+
+ public StatisticsMenu(IStatisticsService statisticsService, IAccountService accountService)
+ {
+ _statisticsService = statisticsService ?? throw new ArgumentNullException(nameof(statisticsService));
+ _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService));
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ "Statistics Menu:".Print(ConsoleColor.DarkYellow);
+
+ if (_accountService.IsAuthorized())
+ {
+ $"{MenuTypes.Personal.GetValue()}.\t{MenuTypes.Personal.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ }
+
+ $"{MenuTypes.All.GetValue()}.\t{MenuTypes.All.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+ $"{MenuTypes.Back.GetValue()}.\t{MenuTypes.Back.GetDisplayName()}".Print(ConsoleColor.DarkYellow);
+
+ "\nPlease select an item from the list".Print(ConsoleColor.Green);
+
+ Console.Write("Select -> ");
+
+ var menuType = Console.ReadLine().TryGetMenuType();
+
+ if (menuType is MenuTypes.Unknown)
+ {
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+
+ continue;
+ }
+
+ switch (menuType)
+ {
+ case MenuTypes.All:
+ Console.Clear();
+ await PrintAllStatisticsAsync(cancellationToken);
+
+ break;
+
+ case MenuTypes.Personal:
+ Console.Clear();
+ await PrintPersonalStatisticsAsync(cancellationToken);
+
+ break;
+
+ case MenuTypes.Back:
+ Console.Clear();
+
+ return;
+
+ default:
+ "Invalid input. Try again.".Print(ConsoleColor.Red);
+ continue;
+ }
+ }
+ }
+
+ private async Task PrintAllStatisticsAsync(CancellationToken cancellationToken)
+ {
+ var allStatistics = await _statisticsService.GetAllAsync(cancellationToken);
+
+ if (allStatistics.IsT0)
+ {
+ PrintAllStatistics(allStatistics.AsT0);
+
+ return;
+ }
+
+ allStatistics.AsT1.Message.Print(ConsoleColor.Red);
+ }
+
+ private async Task PrintPersonalStatisticsAsync(CancellationToken cancellationToken)
+ {
+ if (!_accountService.IsAuthorized())
+ {
+ "User in not logged in.".Print(ConsoleColor.Red);
+
+ return;
+ }
+
+ var user = _accountService.GetUser();
+ var personalStatistics = await _statisticsService.GetPersonalAsync(user.GetBearerToken(), cancellationToken);
+
+ if (personalStatistics.IsT0)
+ {
+ PrintStatistics(personalStatistics.AsT0, user.Login!);
+
+ return;
+ }
+
+ personalStatistics.AsT1.Message.Print(ConsoleColor.Red);
+ }
+
+ private static void PrintAllStatistics(AllStatisticsResponse[] allStatistics)
+ {
+ var statisticsSpan = allStatistics.AsSpan();
+
+ for (var index = 0; index < statisticsSpan.Length; index++)
+ {
+ var color = GetColor(index);
+ $"{index + 1}. User: {statisticsSpan[index].Login}; Score: {statisticsSpan[index].Score}".Print(color);
+ }
+ }
+
+ private static void PrintStatistics(PersonalStatisticsResponse allStatistics, string userName)
+ {
+ $"Here is your statistics, \"{userName}\" :".Print(ConsoleColor.White);
+
+ "\tMain statistics".Print(ConsoleColor.DarkYellow);
+
+ $"\t{nameof(allStatistics.Wins)}: {allStatistics.Wins}".Print(ConsoleColor.Green);
+ $"\t{nameof(allStatistics.Draws)}: {allStatistics.Draws}".Print(ConsoleColor.Yellow);
+ $"\t{nameof(allStatistics.Loss)}: {allStatistics.Loss}".Print(ConsoleColor.Red);
+ $"\t{nameof(allStatistics.Score)}: {allStatistics.Score}".Print(ConsoleColor.White);
+
+ "\tOther statistics".Print(ConsoleColor.DarkYellow);
+
+ $"\t{allStatistics.TimeSpent} spent time playing".Print(ConsoleColor.White);
+ $"\t{allStatistics.UsedPaper} times used 'Paper'".Print(ConsoleColor.White);
+ $"\t{allStatistics.UsedRock} times used 'Rock'".Print(ConsoleColor.White);
+ $"\t{allStatistics.UsedScissors} times used 'Scissors'".Print(ConsoleColor.White);
+ $"\t{allStatistics.WinLossRatio}% Win/Loss ratio".Print(GetColor(allStatistics.WinLossRatio));
+ }
+
+ private static ConsoleColor GetColor(int index)
+ {
+ return index switch
+ {
+ < 3 => ConsoleColor.Green,
+ < 6 => ConsoleColor.Yellow,
+ _ => ConsoleColor.White
+ };
+ }
+
+ private static ConsoleColor GetColor(double index)
+ {
+ return index switch
+ {
+ > 75d => ConsoleColor.DarkGreen,
+ > 50d => ConsoleColor.Green,
+ > 25d => ConsoleColor.Yellow,
+ > 10d => ConsoleColor.Red,
+ _ => ConsoleColor.DarkRed
+ };
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Services/IStatisticsService.cs b/Client/Client.Statistics/Services/IStatisticsService.cs
new file mode 100644
index 0000000..aa4fcbe
--- /dev/null
+++ b/Client/Client.Statistics/Services/IStatisticsService.cs
@@ -0,0 +1,14 @@
+using OneOf;
+using RockPaperScissors.Common;
+using RockPaperScissors.Common.Responses;
+
+namespace Client.Statistics.Services;
+
+public interface IStatisticsService
+{
+ Task> GetAllAsync(CancellationToken cancellationToken);
+
+ Task> GetPersonalAsync(
+ string? token,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Client/Client.Statistics/Services/StatisticsService.cs b/Client/Client.Statistics/Services/StatisticsService.cs
new file mode 100644
index 0000000..7780151
--- /dev/null
+++ b/Client/Client.Statistics/Services/StatisticsService.cs
@@ -0,0 +1,37 @@
+using OneOf;
+using RockPaperScissors.Common;
+using RockPaperScissors.Common.Client;
+using RockPaperScissors.Common.Responses;
+
+namespace Client.Statistics.Services;
+
+internal sealed class StatisticsService: IStatisticsService
+{
+ private readonly IClient _client;
+
+ public StatisticsService(IClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public Task> GetAllAsync(CancellationToken cancellationToken)
+ {
+ var response =
+ _client.GetAsync("api/Statistics/all", cancellationToken);
+
+ return response;
+ }
+
+ public Task> GetPersonalAsync(string? token, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ return Task.FromResult(OneOf.FromT1(new CustomException("Token must not be 'null' or '\"\"'")));
+ }
+
+ var response =
+ _client.GetAsync("api/Statistics/personal", ("Authorization", token), cancellationToken);
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/Client/Client.csproj b/Client/Client.csproj
deleted file mode 100644
index 00d2bb4..0000000
--- a/Client/Client.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net5.0
-
-
-
-
-
-
-
-
-
-
diff --git a/Client/Models/Account.cs b/Client/Models/Account.cs
deleted file mode 100644
index 5f9de0b..0000000
--- a/Client/Models/Account.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using Client.Models.Interfaces;
-using Newtonsoft.Json;
-
-namespace Client.Models
-{
- internal class Account : IAccount
- {
-
- [JsonProperty("SessionId")]
- public string SessionId { get; set; }
-
- [JsonProperty("Login")]
- public string Login { get; set; }
-
- [JsonProperty("Password")]
- public string Password { get; set; }
-
- [JsonProperty("LastRequest")]
- public DateTime LastRequest { get; set; }
- //soon wil be deleted
- public override string ToString()
- {
- return $"Login:\t{Login}\n" +
- $"Password:\t{Password}";
- }
- }
-}
diff --git a/Client/Models/Interfaces/IAccount.cs b/Client/Models/Interfaces/IAccount.cs
deleted file mode 100644
index 6babd23..0000000
--- a/Client/Models/Interfaces/IAccount.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-
-namespace Client.Models.Interfaces
-{
- internal interface IAccount
- {
- public string SessionId { get; set; }
- public string Login { get; set; }
- public string Password { get; set; }
- public DateTime LastRequest { get; set; }
- //soon wil be deleted
- }
-}
diff --git a/Client/Models/Interfaces/IOverallStatistics.cs b/Client/Models/Interfaces/IOverallStatistics.cs
deleted file mode 100644
index 54392be..0000000
--- a/Client/Models/Interfaces/IOverallStatistics.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Client.Models.Interfaces
-{
- internal interface IOverallStatistics
- {
- string Login { get; set; }
-
- int Score { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Interfaces/IRoom.cs b/Client/Models/Interfaces/IRoom.cs
deleted file mode 100644
index ae76a92..0000000
--- a/Client/Models/Interfaces/IRoom.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using Newtonsoft.Json;
-
-namespace Client.Models.Interfaces
-{
- internal interface IRoom
- {
- [JsonProperty("RoomId")]
- string RoomId { get; set; }
-
- [JsonProperty("Players")]
- ConcurrentDictionary Players { get; set; }
-
- [JsonProperty("CurrentRoundId")]
- string CurrentRoundId { get; set; }
-
- [JsonProperty("CreationTime")]
- DateTime CreationTime { get; set; }
- bool IsReady { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Interfaces/IRound.cs b/Client/Models/Interfaces/IRound.cs
deleted file mode 100644
index d2f2602..0000000
--- a/Client/Models/Interfaces/IRound.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using Newtonsoft.Json;
-
-namespace Client.Models.Interfaces
-{
- internal interface IRound
- {
- [JsonProperty("Id")]
- string Id { get; init; } //Not to store identical rounds
-
- [JsonProperty("Moves")]
- ConcurrentDictionary PlayerMoves { get; set; } //where string key is playerId
-
- [JsonProperty("IsFinished")]
- bool IsFinished { get; set; } //Probably not needed.
-
- [JsonProperty("TimeFinished")]
- DateTime TimeFinished { get; set; }
-
- [JsonProperty("WinnerId")]
- string WinnerId { get; set; }
-
- [JsonProperty("LoserId")]
- string LoserId { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Interfaces/IStatistics.cs b/Client/Models/Interfaces/IStatistics.cs
deleted file mode 100644
index e0d4f36..0000000
--- a/Client/Models/Interfaces/IStatistics.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Newtonsoft.Json;
-
-namespace Client.Models.Interfaces
-{
- internal interface IStatistics
- {
- [JsonProperty("Id")]
- string Id { get; set; }
-
- [JsonProperty("UserLogin")]
- string Login { get; set; }
-
- [JsonProperty("Wins")]
- int Wins { get; set; }
-
- [JsonProperty("Loss")]
- int Loss { get; set; }
-
- [JsonProperty("Draws")]
- int Draws { get; set; }
-
- [JsonProperty("WinToLossRatio")]
- double WinLossRatio { get; set; }
-
- [JsonProperty("TimeSpent")]
- string TimeSpent { get; set; }
-
- [JsonProperty("UsedRock")]
- int UsedRock { get; set; }
-
- [JsonProperty("UsedPaper")]
- int UsedPaper { get; set; }
-
- [JsonProperty("UsedScissors")]
- int UsedScissors { get; set; }
-
- [JsonProperty("Score")]
- int Score { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Interfaces/StatisticsDto.cs b/Client/Models/Interfaces/StatisticsDto.cs
deleted file mode 100644
index 8b6d390..0000000
--- a/Client/Models/Interfaces/StatisticsDto.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Newtonsoft.Json;
-
-namespace Client.Models.Interfaces
-{
- public class StatisticsDto
- {
- [JsonProperty("Login")]
- public string Login { get; set; }
-
- [JsonProperty("Score")]
- public int Score { get; set; }
-
- public override string ToString()
- {
- return $"Login: {Login} ; Score: {Score}\n";
- }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Room.cs b/Client/Models/Room.cs
deleted file mode 100644
index 07e93e8..0000000
--- a/Client/Models/Room.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Newtonsoft.Json;
-using System;
-using System.Collections.Concurrent;
-using Client.Models.Interfaces;
-
-namespace Client.Models
-{
- internal class Room : IRoom
- {
- [JsonProperty("RoomId")]
- public string RoomId { get; set; }
-
- [JsonProperty("Players")]
- public ConcurrentDictionary Players { get; set; }
-
- [JsonProperty("CurrentRoundId")]
- public string CurrentRoundId { get; set; }
-
- [JsonProperty("CreationTime")]
- public DateTime CreationTime { get; set; }
-
- public bool IsReady { get; set; }
- }
-}
diff --git a/Client/Models/Round.cs b/Client/Models/Round.cs
deleted file mode 100644
index 7bfefc7..0000000
--- a/Client/Models/Round.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Newtonsoft.Json;
-using System;
-using System.Collections.Concurrent;
-using Client.Models.Interfaces;
-
-namespace Client.Models
-{
- internal class Round : IRound
- {
- [JsonProperty("Id")]
- public string Id { get; init; } //Not to store identical rounds
-
- [JsonProperty("Moves")]
- public ConcurrentDictionary PlayerMoves { get; set; } //where string key is playerId
-
- [JsonProperty("IsFinished")]
- public bool IsFinished { get; set; } //Probably not needed.
-
- [JsonProperty("TimeFinished")]
- public DateTime TimeFinished { get; set; }
-
- [JsonProperty("WinnerId")]
- public string WinnerId { get; set; }
-
- [JsonProperty("LoserId")]
- public string LoserId { get; set; }
-
- [JsonProperty("IsDraw")]
- public bool IsDraw { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/Statistics.cs b/Client/Models/Statistics.cs
deleted file mode 100644
index 0b4d026..0000000
--- a/Client/Models/Statistics.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Client.Models.Interfaces;
-
-namespace Client.Models
-{
- internal class Statistics : IStatistics, IOverallStatistics
- {
- public string Id { get; set; }
- public string Login { get; set; }
-
- public int Wins { get; set; }
-
- public int Loss { get; set; }
-
- public int Draws { get; set; }
-
- public double WinLossRatio { get; set; }
-
- public string TimeSpent { get; set; }
-
- public int UsedRock { get; set; }
-
- public int UsedPaper { get; set; }
-
- public int UsedScissors { get; set; }
- public int Score { get; set; }
-
- public override string ToString()
- {
- return $"Times Won: {Wins}\n" +
- $"Times Lost:{Loss}\n" +
- $"Win to Loss ratio: {WinLossRatio}\n" +
- $"{TimeSpent}\n" +
- $"Times used rock: {UsedRock}\n" +
- $"Times used paper: {UsedPaper}\n" +
- $"Times used scissors: {UsedScissors}\n" +
- $"Total score: {Score}";
- }
- }
-}
\ No newline at end of file
diff --git a/Client/Models/StringDestination.cs b/Client/Models/StringDestination.cs
deleted file mode 100644
index 4e02c13..0000000
--- a/Client/Models/StringDestination.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Client.Models
-{
- public enum StringDestination
- {
- Login,
- Password,
- Email,
- PassportType //If firstname, lastname...(data without digits!)
- }
-}
diff --git a/Client/Program.cs b/Client/Program.cs
deleted file mode 100644
index 99c05fc..0000000
--- a/Client/Program.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Client.Services.RequestProcessor.Impl;
-using System;
-using System.Threading.Tasks;
-
-namespace Client
-{
- internal static class Program
- {
- private static async Task Main()
- {
- try
- {
- var emulator = new ClientAppEmulator(new RequestPerformer());
- return await emulator.StartAsync();
- }
- catch (Exception) //todo : do this need a message?
- {
- return -1;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Client/Services/ColorTextWriterService.cs b/Client/Services/ColorTextWriterService.cs
deleted file mode 100644
index 14dc144..0000000
--- a/Client/Services/ColorTextWriterService.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Client.Services
-{
- public static class ColorTextWriterService
- {
- public static void PrintLineMessageWithSpecialColor(string msg, ConsoleColor color)
- {
- Console.ForegroundColor = color;
- Console.WriteLine(msg);
- Console.ResetColor();
- }
-
- public static void PrintMessageWithSpecialColor(string msg, ConsoleColor color)
- {
- Console.ForegroundColor = color;
- Console.Write(msg);
- Console.ResetColor();
- }
- }
-}
diff --git a/Client/Services/RequestProcessor/IRequestHandler.cs b/Client/Services/RequestProcessor/IRequestHandler.cs
deleted file mode 100644
index 61e8955..0000000
--- a/Client/Services/RequestProcessor/IRequestHandler.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Client.Services.RequestModels;
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor
-{
- public interface IRequestHandler
- {
- Task HandleRequestAsync(IRequestOptions requestOptions);
- }
-}
diff --git a/Client/Services/RequestProcessor/IRequestPerformer.cs b/Client/Services/RequestProcessor/IRequestPerformer.cs
deleted file mode 100644
index a028f21..0000000
--- a/Client/Services/RequestProcessor/IRequestPerformer.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Client.Services.RequestModels;
-using Client.Services.RequestProcessor.RequestModels.Impl;
-using System;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor
-{
- public interface IRequestPerformer
- {
- Task PerformRequestAsync(IRequestOptions requestOptions);
- }
-}
diff --git a/Client/Services/RequestProcessor/Impl/RequestHandler.cs b/Client/Services/RequestProcessor/Impl/RequestHandler.cs
deleted file mode 100644
index d43634e..0000000
--- a/Client/Services/RequestProcessor/Impl/RequestHandler.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Client.Services.RequestModels;
-using Client.Services.RequestProcessor.RequestModels.Impl;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor.Impl
-{
- public class RequestHandler : IRequestHandler
- {
- private HttpClient _client;
- public async Task HandleRequestAsync(IRequestOptions requestOptions)
- {
- var handler = new HttpClientHandler();
- handler.ServerCertificateCustomValidationCallback += (sender, certificate, chain, errors) => true;
- _client = new HttpClient(handler);
- if (requestOptions == null) throw new ArgumentNullException(nameof(requestOptions));
- if (!requestOptions.IsValid) throw new ArgumentOutOfRangeException(nameof(requestOptions));
-
- using var msg = new HttpRequestMessage(MapMethod(requestOptions.Method), new Uri(requestOptions.Address));
- try
- {
- if (MapMethod(requestOptions.Method) == HttpMethod.Delete)
- {
- using var responseD = await _client.SendAsync(msg);
- var bodyD = await responseD.Content.ReadAsStringAsync();
- return new Response(true, (int)responseD.StatusCode, bodyD);
- }
-
- if (MapMethod(requestOptions.Method) != HttpMethod.Get)
- {
- msg.Content = new StringContent(requestOptions.Body, Encoding.UTF8, requestOptions.ContentType);
- using var responseForPushingData = await _client.SendAsync(msg);
- var bodyForPushing = await responseForPushingData.Content.ReadAsStringAsync();
- return new Response(true, (int)responseForPushingData.StatusCode, bodyForPushing);
- }
- using var response = await _client.SendAsync(msg);
- var body = await response.Content.ReadAsStringAsync();
- return new Response(true, (int)response.StatusCode, body);
- }
- catch (HttpRequestException) //todo: probably redo
- {
- return new Response(false, 500, "Server is not responding!");
- }
- }
- private static HttpMethod MapMethod(RequestMethod method)
- {
- return method switch
- {
- RequestMethod.Get => HttpMethod.Get,
- RequestMethod.Post => HttpMethod.Post,
- RequestMethod.Put => HttpMethod.Put,
- RequestMethod.Patch => HttpMethod.Patch,
- RequestMethod.Delete => HttpMethod.Delete,
- _ => throw new ArgumentOutOfRangeException(nameof(method), method, "Invalid request method")
- };
- }
- }
-}
diff --git a/Client/Services/RequestProcessor/Impl/RequestPerformer.cs b/Client/Services/RequestProcessor/Impl/RequestPerformer.cs
deleted file mode 100644
index e5b73d5..0000000
--- a/Client/Services/RequestProcessor/Impl/RequestPerformer.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Client.Services.RequestModels;
-using Client.Services.RequestProcessor.RequestModels.Impl;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor.Impl
-{
- public class RequestPerformer : IRequestPerformer
- {
- public RequestPerformer()
- {
- RequestHandler = new RequestHandler();
- }
- public readonly IRequestHandler RequestHandler;
- public async Task PerformRequestAsync(IRequestOptions requestOptions)
- {
- IResponse response = null;
- if (requestOptions == null) throw new ArgumentNullException(nameof(requestOptions));
- if (!requestOptions.IsValid) throw new ArgumentOutOfRangeException(nameof(requestOptions));
- try
- {
- response = await RequestHandler.HandleRequestAsync(requestOptions);
- }
- catch (TimeoutException) //todo: Probably redo
- {
- response = new Response(false, 408, null);
- }
- return response;
- }
- }
-}
diff --git a/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs b/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs
deleted file mode 100644
index 5d0e7e1..0000000
--- a/Client/Services/RequestProcessor/RequestModels/IRequestOptions.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Client.Services.RequestModels
-{
- public interface IRequestOptions
- {
- string Name { get; }
- string Address { get; }
- RequestMethod Method { get; }
- string ContentType { get; }
- string Body { get; }
- bool IsValid { get; }
- }
-}
diff --git a/Client/Services/RequestProcessor/RequestModels/IResponse.cs b/Client/Services/RequestProcessor/RequestModels/IResponse.cs
deleted file mode 100644
index 2387385..0000000
--- a/Client/Services/RequestProcessor/RequestModels/IResponse.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Client.Services.RequestModels
-{
- public interface IResponse
- {
- public bool Handled { get; }
- public int Code { get; }
- public string Content { get; }
- }
-}
\ No newline at end of file
diff --git a/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs b/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs
deleted file mode 100644
index afa9ef9..0000000
--- a/Client/Services/RequestProcessor/RequestModels/Impl/RequestOptions.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Client.Services.RequestModels;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor.RequestModels.Impl
-{
- public class RequestOptions : IRequestOptions
- {
- public string Name { get; set; }
-
- public string Address { get; set; }
-
- public RequestMethod Method { get; set; }
-
- public string ContentType { get; set; }
-
- public string Body { get; set; }
-
- public bool IsValid { get; set; }
- }
-}
diff --git a/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs b/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs
deleted file mode 100644
index d5703e8..0000000
--- a/Client/Services/RequestProcessor/RequestModels/Impl/Response.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Client.Services.RequestModels;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Client.Services.RequestProcessor.RequestModels.Impl
-{
- public class Response : IResponse
- {
- public Response(bool handled, int code, string content)
- {
- Handled = handled;
- Code = code;
- Content = content;
- }
- public bool Handled { get; set; }
-
- public int Code { get; set; }
-
- public string Content { get; set; }
- }
-}
diff --git a/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs b/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs
deleted file mode 100644
index 893fe81..0000000
--- a/Client/Services/RequestProcessor/RequestModels/RequestMethod.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Client.Services.RequestModels
-{
- public enum RequestMethod
- {
- Undefined = 0,
- Get = 1,
- Post = 2,
- Put = 3,
- Patch = 4,
- Delete =5
- }
-}
diff --git a/Client/Services/StringPlaceholder.cs b/Client/Services/StringPlaceholder.cs
deleted file mode 100644
index b7fc5b8..0000000
--- a/Client/Services/StringPlaceholder.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Client.Models;
-using Client.Validations;
-
-namespace Client.Services
-{
- class StringPlaceholder
- {
- private StringDestination destination;
- public StringPlaceholder()
- {
- destination = StringDestination.Login;
- }
- public StringPlaceholder(StringDestination destination)
- {
- this.destination = destination;
- }
- public string BuildNewSpecialDestinationString(string msg, bool isNeedConfirmation = false)
- {
- string output;
- while (true)
- {
- bool passwordNotConfirmed = true;
- if(destination == StringDestination.PassportType || destination == StringDestination.Email)
- ColorTextWriterService.PrintLineMessageWithSpecialColor($"What is your {msg}?", ConsoleColor.Yellow);
- else
- ColorTextWriterService.PrintLineMessageWithSpecialColor($"Try to come up with {msg}?", ConsoleColor.Yellow);
- Console.Write($"{msg}--> ");
- output = Console.ReadLine()
- ?.Trim()
- .Replace(" ", "");
- if (String.IsNullOrEmpty(output))
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong data!", ConsoleColor.Red);
- continue;
- }
- if(destination == StringDestination.Password && output.Length < 6)
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong password length!", ConsoleColor.Red);
- continue;
- }
- if (destination == StringDestination.Email && !StringValidator.IsEmailValid(output))
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("This email is not valid!", ConsoleColor.Red);
- continue;
- }
- if (destination == StringDestination.Password)
- {
- if (isNeedConfirmation == true)
- break;
- ColorTextWriterService.PrintLineMessageWithSpecialColor("You need to confirm password!", ConsoleColor.Yellow);
- string confirmationPassword;
- do
- {
- Console.Write("Confirmation--> ");
- confirmationPassword = Console.ReadLine()
- ?.Trim()
- .Replace(" ", "");
- if (String.IsNullOrEmpty(output))
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Wrong data!", ConsoleColor.Red);
- continue;
- }
- if (output == confirmationPassword)
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Password confirmed", ConsoleColor.Green);
- passwordNotConfirmed = false;
- }
- else
- ColorTextWriterService.PrintLineMessageWithSpecialColor("Passwords dont match!",ConsoleColor.Red);
- } while (passwordNotConfirmed);
- }
- if (destination == StringDestination.PassportType && StringValidator.IsStringContainsDigits(output))
- {
- ColorTextWriterService.PrintLineMessageWithSpecialColor("You cannot enter nameType with digits!", ConsoleColor.Red);
- continue;
- }
- break;
- }
- return output;
- }
- }
-}
diff --git a/Client/Validations/StringValidator.cs b/Client/Validations/StringValidator.cs
deleted file mode 100644
index a1df9d2..0000000
--- a/Client/Validations/StringValidator.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Linq;
-using System.Text.RegularExpressions;
-
-namespace Client.Validations
-{
- public static class StringValidator
- {
- public static bool IsStringContainsDigits(string str)
- {
- var res = str.Any(ch => Char.IsDigit(ch));
- return res;
- }
- public static bool IsEmailValid(string email)
- {
- var emailPattern = "[.\\-_a-z0-9]+@([a-z0-9][\\-a-z0-9]+\\.)+[a-z]{2,6}";
- var match = Regex.Match(email,emailPattern,RegexOptions.IgnoreCase);
- return match.Success;
- }
- }
-}
diff --git a/Common/RockPaperScissors.Common/Client/Client.cs b/Common/RockPaperScissors.Common/Client/Client.cs
new file mode 100644
index 0000000..6e33849
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Client/Client.cs
@@ -0,0 +1,169 @@
+using System.Net;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using OneOf;
+
+namespace RockPaperScissors.Common.Client;
+
+public sealed class Client : IClient
+{
+ private static readonly HttpClient HttpClient = new()
+ {
+ BaseAddress = null,
+ DefaultRequestVersion = HttpVersion.Version20,
+ DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
+ DefaultRequestHeaders =
+ {
+ {"Accept", MediaTypeNames.Application.Json}
+ }
+ };
+
+ private Client(string baseAddress)
+ {
+ HttpClient.BaseAddress = new Uri(baseAddress);
+ }
+
+ public static Client Create(string baseAddress) => new(baseAddress);
+
+ public async Task> GetAsync(
+ string url,
+ CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(HttpClient.BaseAddress!, url),
+ Version = HttpClient.DefaultRequestVersion,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ };
+
+ using var response =
+ await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ public async Task> GetAsync(
+ string url,
+ (string HeaderKey, string HeaderValue) headerValues,
+ CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ Headers = {
+ {
+ headerValues.HeaderKey, headerValues.HeaderValue
+ }
+ },
+ RequestUri = new Uri(HttpClient.BaseAddress!, url),
+ Version = HttpClient.DefaultRequestVersion,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ };
+
+ using var response =
+ await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ public async Task GetAsync(
+ string url,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(HttpClient.BaseAddress!, url),
+ Version = HttpClient.DefaultRequestVersion,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ };
+
+ using var response =
+ await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ return response.IsSuccessStatusCode;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public async Task> PostAsync(
+ string url,
+ T1 content,
+ CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Post,
+ Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, MediaTypeNames.Application.Json),
+ RequestUri = new Uri(HttpClient.BaseAddress!, url),
+ Version = HttpClient.DefaultRequestVersion,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ };
+
+ using var response =
+ await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ public async Task> PostAsync(
+ string url,
+ T1 content,
+ (string HeaderKey, string HeaderValue) headerValues,
+ CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Post,
+ Headers = {
+ {
+ headerValues.HeaderKey, headerValues.HeaderValue
+ }
+ },
+ Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, MediaTypeNames.Application.Json),
+ RequestUri = new Uri(HttpClient.BaseAddress!, url),
+ Version = HttpClient.DefaultRequestVersion,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ };
+
+ using var response =
+ await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+
+ return (await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken))!;
+ }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Client/IClient.cs b/Common/RockPaperScissors.Common/Client/IClient.cs
new file mode 100644
index 0000000..f7f7f1f
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Client/IClient.cs
@@ -0,0 +1,29 @@
+using OneOf;
+
+namespace RockPaperScissors.Common.Client;
+
+public interface IClient
+{
+ Task> GetAsync(
+ string url,
+ CancellationToken cancellationToken = default);
+
+ Task> GetAsync(
+ string url,
+ (string HeaderKey, string HeaderValue) headerValues,
+ CancellationToken cancellationToken = default);
+
+ Task GetAsync(
+ string url,
+ CancellationToken cancellationToken);
+
+ Task> PostAsync(
+ string url,
+ T1 content,
+ CancellationToken cancellationToken = default);
+
+ Task> PostAsync(
+ string url, T1 content,
+ (string HeaderKey, string HeaderValue) headerValues,
+ CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/CustomException.cs b/Common/RockPaperScissors.Common/CustomException.cs
new file mode 100644
index 0000000..a1fdba4
--- /dev/null
+++ b/Common/RockPaperScissors.Common/CustomException.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common;
+
+public sealed class CustomException
+{
+ [JsonConstructor]
+ public CustomException(int code, string message)
+ {
+ Message = message;
+ Code = code;
+ }
+
+ public CustomException(string message)
+ {
+ Message = message;
+ Code = 400;
+ }
+
+ [JsonPropertyName("code")]
+ public int Code { get; init; }
+
+ [JsonPropertyName("message")]
+ public string Message { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Enums/Move.cs b/Common/RockPaperScissors.Common/Enums/Move.cs
new file mode 100644
index 0000000..bbb657a
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Enums/Move.cs
@@ -0,0 +1,12 @@
+namespace RockPaperScissors.Common.Enums;
+
+public enum Move
+{
+ None = 0,
+
+ Rock = 1,
+
+ Paper = 2,
+
+ Scissors = 3,
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Enums/PlayerState.cs b/Common/RockPaperScissors.Common/Enums/PlayerState.cs
new file mode 100644
index 0000000..8b3ca7a
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Enums/PlayerState.cs
@@ -0,0 +1,12 @@
+namespace RockPaperScissors.Common.Enums;
+
+public enum PlayerState
+{
+ None = 0,
+
+ Lose = 1,
+
+ Win = 2,
+
+ Draw = 3,
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Enums/StringDestination.cs b/Common/RockPaperScissors.Common/Enums/StringDestination.cs
new file mode 100644
index 0000000..3dec296
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Enums/StringDestination.cs
@@ -0,0 +1,9 @@
+namespace RockPaperScissors.Common.Enums;
+
+public enum StringDestination
+{
+ Login,
+ Password,
+ Email,
+ PassportType //If firstname, lastname...(data without digits!)
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs b/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs
new file mode 100644
index 0000000..adaa602
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Extensions/QueryBuilder.cs
@@ -0,0 +1,133 @@
+using System.Buffers;
+using System.Collections;
+
+namespace RockPaperScissors.Common.Extensions;
+
+public sealed class QueryBuilder : IReadOnlyCollection>
+{
+ private readonly IList> _valuePairs;
+
+ ///
+ public int Count { get; private set; }
+
+ ///
+ /// Constructor.
+ ///
+ public QueryBuilder()
+ {
+ _valuePairs = new List>();
+ }
+
+ ///
+ /// Constructor.
+ ///
+ public QueryBuilder(int count)
+ {
+ Count = count;
+ _valuePairs = new List>(count);
+ }
+
+ ///
+ /// Constructor.
+ ///
+ /// Collection of KeyValuePairs.
+ public QueryBuilder(IReadOnlyCollection> parameters)
+ {
+ Count += parameters.Sum(pair => pair.Key.Length + pair.Value.Length);
+
+ _valuePairs = new List>(parameters);
+ }
+
+ ///
+ /// Adds key and it's values to the collection.
+ ///
+ /// Key.
+ /// Collection of values.
+ public void Add(string key, IEnumerable values)
+ {
+ Count += key.Length;
+
+ foreach (var value in values)
+ {
+ Count += value.Length;
+
+ _valuePairs.Add(KeyValuePair.Create(key, value));
+ }
+ }
+
+ ///
+ /// Adds key and it's value to the collection.
+ ///
+ /// Key.
+ /// Value.
+ public void Add(string key, string? value)
+ {
+ Count += key.Length + value.Length;
+
+ _valuePairs.Add(KeyValuePair.Create(key, value));
+ }
+
+ ///
+ public override string ToString()
+ {
+ if (Count is 0)
+ {
+ return string.Empty;
+ }
+
+ var queryLength = Count * 2;
+
+ var isStackAlloc = queryLength <= 64;
+ var currentPosition = 0;
+
+ var array = isStackAlloc ? null : ArrayPool.Shared.Rent(queryLength);
+ var resultSpan = isStackAlloc ? stackalloc char[queryLength] : array;
+
+ try
+ {
+ var first = true;
+ for (var i = 0; i < _valuePairs.Count; i++)
+ {
+ var pair = _valuePairs[i];
+ resultSpan[currentPosition] = first ? '?' : '&';
+ first = false;
+ currentPosition++;
+
+ var escapeKey = Uri.EscapeDataString(pair.Key);
+ escapeKey.CopyTo(resultSpan[currentPosition..]);
+ currentPosition += escapeKey.Length;
+
+ resultSpan[currentPosition++] = '=';
+
+ var escapedValue = Uri.EscapeDataString(pair.Value);
+ escapedValue.CopyTo(resultSpan[currentPosition..]);
+ currentPosition += escapedValue.Length;
+ }
+
+ var endIndex = resultSpan.IndexOf('\0');
+
+ return endIndex is -1
+ ? resultSpan[..currentPosition].ToString()
+ : resultSpan[..(endIndex > currentPosition ? currentPosition : endIndex)].ToString();
+ }
+ finally
+ {
+ if (array is not null)
+ {
+ ArrayPool.Shared.Return(array);
+ }
+ }
+ }
+
+ ///
+ public IEnumerator> GetEnumerator()
+ {
+ return _valuePairs.GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _valuePairs.GetEnumerator();
+ }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs b/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs
new file mode 100644
index 0000000..9038de9
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Extensions/TextWriteExtensions.cs
@@ -0,0 +1,108 @@
+using System.Text.RegularExpressions;
+using RockPaperScissors.Common.Enums;
+
+namespace RockPaperScissors.Common.Extensions;
+
+public static class TextWriteExtensions
+{
+ private static readonly Regex EmailRegex =
+ new("[.\\-_a-z0-9]+@([a-z0-9][\\-a-z0-9]+\\.)+[a-z]{2,6}", RegexOptions.Compiled);
+
+ public static void Print(this string message, ConsoleColor color)
+ {
+ Console.ForegroundColor = color;
+ Console.WriteLine(message);
+ Console.ResetColor();
+ }
+
+ public static string BuildString(this string msg, StringDestination destination, bool isNeedConfirmation = false)
+ {
+ string output;
+ while (true)
+ {
+ var passwordNotConfirmed = true;
+
+ Print(
+ destination is StringDestination.PassportType or StringDestination.Email
+ ? $"What is your {msg}?"
+ : $"Try to come up with {msg}", ConsoleColor.Yellow);
+
+ Console.Write($"{msg}--> ");
+
+ output = Console.ReadLine()
+ ?.Trim()
+ .Replace(" ", "") ?? string.Empty;
+
+ if (string.IsNullOrEmpty(output))
+ {
+ Print("Wrong data!", ConsoleColor.Red);
+ continue;
+ }
+
+ switch (destination)
+ {
+ case StringDestination.Password when output.Length < 6:
+ Print("Wrong password length!", ConsoleColor.Red);
+ continue;
+ case StringDestination.Email when !IsEmailValid(output):
+ Print("This email is not valid!", ConsoleColor.Red);
+ continue;
+ }
+
+ if (destination is StringDestination.Password)
+ {
+ if (isNeedConfirmation)
+ {
+ break;
+ }
+
+ Print("You need to confirm password!", ConsoleColor.Yellow);
+ do
+ {
+ Console.Write("Confirmation--> ");
+ var confirmationPassword = Console.ReadLine()
+ ?.Trim()
+ .Replace(" ", "");
+
+ if (string.IsNullOrEmpty(output))
+ {
+ Print("Wrong data!", ConsoleColor.Red);
+ continue;
+ }
+
+ if (output != confirmationPassword)
+ {
+ Print("Passwords don't match!",ConsoleColor.Red);
+ continue;
+ }
+
+ Print("Password confirmed", ConsoleColor.Green);
+ passwordNotConfirmed = false;
+ }
+ while (passwordNotConfirmed);
+ }
+ if (destination is StringDestination.PassportType && ContainsDigits(output))
+ {
+ Print("You cannot enter nameType with digits!", ConsoleColor.Red);
+ continue;
+ }
+ break;
+ }
+
+ return output;
+ }
+
+ public static bool ContainsDigits(this string str)
+ {
+ var res = str.Any(char.IsDigit);
+
+ return res;
+ }
+
+ public static bool IsEmailValid(this string email)
+ {
+ var match = EmailRegex.Match(email);
+
+ return match.Success;
+ }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Models/AccountDto.cs b/Common/RockPaperScissors.Common/Models/AccountDto.cs
new file mode 100644
index 0000000..f3c0f92
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Models/AccountDto.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Models;
+
+public sealed class AccountDto
+{
+ [JsonPropertyName("login")]
+ [Required(ErrorMessage = "Login is required!")]
+ public string Login { get; init; }
+
+ [JsonPropertyName("password")]
+ [Required(ErrorMessage = "Password is required!")]
+ [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length")]
+ public string Password { get; init;}
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Models/StatisticsDto.cs b/Common/RockPaperScissors.Common/Models/StatisticsDto.cs
new file mode 100644
index 0000000..b0a60f1
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Models/StatisticsDto.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Models;
+
+public sealed class StatisticsDto
+{
+ [JsonPropertyName("login")]
+ public string Login { get; init; }
+
+ [JsonPropertyName("score")]
+ public int Score { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Requests/LoginRequest.cs b/Common/RockPaperScissors.Common/Requests/LoginRequest.cs
new file mode 100644
index 0000000..71891b3
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Requests/LoginRequest.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Requests;
+
+public sealed class LoginRequest
+{
+ [JsonPropertyName("login")]
+ [Required(ErrorMessage = "Login is required!")]
+ public string Login { get; init; }
+
+ [JsonPropertyName("password")]
+ [Required(ErrorMessage = "Password is required!!")]
+ [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length")]
+ public string Password { get; init; }
+
+ [JsonPropertyName("lastRequestTime")]
+ public DateTimeOffset LastRequestTime { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs b/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs
new file mode 100644
index 0000000..a825a99
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Requests/MakeMoveRequest.cs
@@ -0,0 +1,10 @@
+using RockPaperScissors.Common.Enums;
+
+namespace RockPaperScissors.Common.Requests;
+
+public sealed class MakeMoveRequest
+{
+ public string RoundId { get; init; }
+
+ public Move Move { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs b/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs
new file mode 100644
index 0000000..171d2e4
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Requests/RegisterRequest.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Requests;
+
+public sealed class RegisterRequest
+{
+ [JsonPropertyName("login")]
+ [Required(ErrorMessage = "Login is required!")]
+ public string Login { get; init; }
+
+ [JsonPropertyName("password")]
+ [Required(ErrorMessage = "Password is required!")]
+ [StringLength(20, MinimumLength = 6, ErrorMessage = "Invalid password length. Must be 6-20")]
+ public string Password { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs b/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs
new file mode 100644
index 0000000..c8977c2
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Responses/AllStatisticsResponse.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Responses;
+
+public sealed class AllStatisticsResponse
+{
+ [JsonPropertyName("login")]
+ public string Login { get; init; }
+
+ ///
+ /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points.
+ ///
+ [JsonPropertyName("score")]
+ public int Score { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Responses/LoginResponse.cs b/Common/RockPaperScissors.Common/Responses/LoginResponse.cs
new file mode 100644
index 0000000..d7d1802
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Responses/LoginResponse.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Responses;
+
+public sealed class LoginResponse
+{
+ ///
+ /// Gets or sets user token (used in header).
+ ///
+ [JsonPropertyName("token")]
+ public string Token { get; init; }
+
+ ///
+ /// Gets or sets user login.
+ ///
+ [JsonPropertyName("login")]
+ public string Login { get; init; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs b/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs
new file mode 100644
index 0000000..fce1513
--- /dev/null
+++ b/Common/RockPaperScissors.Common/Responses/PersonalStatisticsResponse.cs
@@ -0,0 +1,60 @@
+using System.Text.Json.Serialization;
+
+namespace RockPaperScissors.Common.Responses;
+
+public sealed class PersonalStatisticsResponse
+{
+ ///
+ /// Total amount of Wins
+ ///
+ [JsonPropertyName("wins")]
+ public int Wins { get; set; }
+
+ ///
+ /// Total amount of Loses
+ ///
+ [JsonPropertyName("loss")]
+ public int Loss { get; set; }
+
+ ///
+ /// Total amount of Draws. OBSOLETE
+ ///
+ [JsonPropertyName("draws")]
+ public int Draws { get; set; }
+
+ ///
+ /// Ratio Wins to Losses. Win/Loss * 100
+ ///
+ [JsonPropertyName("winLossRatio")]
+ public double WinLossRatio { get; set; }
+
+ ///
+ /// Ratio for the last 7 days
+ ///
+ [JsonPropertyName("timeSpent")]
+ public string TimeSpent { get; set; }
+
+ ///
+ /// Times used rock
+ ///
+ [JsonPropertyName("usedRock")]
+ public int UsedRock { get; set; }
+
+ ///
+ /// Times used Paper
+ ///
+ [JsonPropertyName("usedPaper")]
+ public int UsedPaper { get; set; }
+
+ ///
+ /// Times used Scissors
+ ///
+ [JsonPropertyName("usedScissors")]
+ public int UsedScissors { get; set; }
+
+ ///
+ /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points.
+ ///
+ [JsonPropertyName("score")]
+ public int Score { get; set; }
+}
\ No newline at end of file
diff --git a/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj b/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj
new file mode 100644
index 0000000..09d3722
--- /dev/null
+++ b/Common/RockPaperScissors.Common/RockPaperScissors.Common.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Common/RockPaperScissors.Common/UrlTemplates.cs b/Common/RockPaperScissors.Common/UrlTemplates.cs
new file mode 100644
index 0000000..b278632
--- /dev/null
+++ b/Common/RockPaperScissors.Common/UrlTemplates.cs
@@ -0,0 +1,28 @@
+namespace RockPaperScissors.Common;
+
+public static class UrlTemplates
+{
+ // Account-related
+ public const string RegisterUrl = "api/account/register";
+ public const string LoginUrl = "api/account/login";
+ public const string LogoutUrl = "api/account/logout";
+
+ // Statistics-related
+ public const string AllStatistics = "api/statistics/all";
+ public const string PersonalStatistics = "api/statistics/personal";
+
+ // Room-related
+ public const string CreateRoom = "api/room/create";
+ public const string JoinPublicRoom = "api/room/public/join";
+ public const string JoinPrivateRoom = "api/room/private/{roomCode}/join";
+ public const string UpdateRoom = "api/room/{roomId}/update";
+ public const string DeleteRoom = "api/room/{roomId}/delete";
+ public const string ChangeStatus = "api/room/{roomId}/playerstatus";
+ public const string CheckRoomUpdateTicks = "api/room/{roomId}/status";
+
+ // Round-related
+ public const string CreateRound = "api/round/create";
+ public const string MakeMove = "api/round/{roundId}/move/{move}";
+ public const string UpdateRound = "api/round/{roundId}/update";
+ public const string CheckRoundUpdateTicks = "api/round/{roundId}/status";
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 924f974..98e44a4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# RockPaperScissorsGame
-This is second project of Parimatch Tech Academy .NET course.
+This is second project of PariMatch Tech Academy .NET course.
[Technical requirement](https://docs.google.com/document/d/13lsfyUkMJBZAiXt8OlBWYdKq9zExdHdDhBBH00s5tv4/edit?usp=sharing)
-[Table of progress](https://docs.google.com/spreadsheets/d/1h2YSv9YhTiELJAsEIE5oPusIhlloKiyxRdfDVqGc_JI/edit?usp=sharing)
+~~[Table of progress](https://docs.google.com/spreadsheets/d/1h2YSv9YhTiELJAsEIE5oPusIhlloKiyxRdfDVqGc_JI/edit?usp=sharing)~~
+
+[Trello Kanban](https://trello.com/b/vofn5Dhn/rock-paper-scissors)
diff --git a/RockPaperScissorsGame.sln b/RockPaperScissorsGame.sln
index 8d353c0..3a2d5c2 100644
--- a/RockPaperScissorsGame.sln
+++ b/RockPaperScissorsGame.sln
@@ -3,9 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{53166F83-4032-4CCF-B172-C2517BFBA424}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{B6D69162-F9E0-4165-A288-00FF1D073291}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{540EE2A5-907D-4E6A-A051-22D01DBFE913}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Host", "Server\Server.Host\Server.Host.csproj", "{DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Data", "Server\Server.Data\Server.Data.csproj", "{BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Bll", "Server\Server.Bll\Server.Bll.csproj", "{74D55924-777B-4FC3-B8F5-37508B2EBAE9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Authentication", "Server\Server.Authentication\Server.Authentication.csproj", "{1FC79883-C4C0-4C61-A3F1-D699F7A793B2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Host", "Client\Client.Host\Client.Host.csproj", "{45AC8461-58BE-4675-AD96-89CF88CF8CA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Account", "Client\Client.Account\Client.Account.csproj", "{32045C10-4395-4010-A365-6CAF4D993680}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.StartMenu", "Client\Client.StartMenu\Client.StartMenu.csproj", "{BB2630C0-3F25-49F1-9393-4B75A7014187}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RockPaperScissors.Common", "Common\RockPaperScissors.Common\RockPaperScissors.Common.csproj", "{4E6B589E-C9C5-4191-B435-6FE4283C1B58}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Statistics", "Client\Client.Statistics\Client.Statistics.csproj", "{994CC558-A9A9-4E58-A894-EE06993DAC58}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.MainMenu", "Client\Client.MainMenu\Client.MainMenu.csproj", "{3928C9ED-4F7C-4D3E-8265-C98EBC85F318}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -13,14 +33,46 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {53166F83-4032-4CCF-B172-C2517BFBA424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {53166F83-4032-4CCF-B172-C2517BFBA424}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {53166F83-4032-4CCF-B172-C2517BFBA424}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {53166F83-4032-4CCF-B172-C2517BFBA424}.Release|Any CPU.Build.0 = Release|Any CPU
- {B6D69162-F9E0-4165-A288-00FF1D073291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B6D69162-F9E0-4165-A288-00FF1D073291}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B6D69162-F9E0-4165-A288-00FF1D073291}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B6D69162-F9E0-4165-A288-00FF1D073291}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74D55924-777B-4FC3-B8F5-37508B2EBAE9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1FC79883-C4C0-4C61-A3F1-D699F7A793B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45AC8461-58BE-4675-AD96-89CF88CF8CA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {32045C10-4395-4010-A365-6CAF4D993680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {32045C10-4395-4010-A365-6CAF4D993680}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {32045C10-4395-4010-A365-6CAF4D993680}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {32045C10-4395-4010-A365-6CAF4D993680}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BB2630C0-3F25-49F1-9393-4B75A7014187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BB2630C0-3F25-49F1-9393-4B75A7014187}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BB2630C0-3F25-49F1-9393-4B75A7014187}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BB2630C0-3F25-49F1-9393-4B75A7014187}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E6B589E-C9C5-4191-B435-6FE4283C1B58}.Release|Any CPU.Build.0 = Release|Any CPU
+ {994CC558-A9A9-4E58-A894-EE06993DAC58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {994CC558-A9A9-4E58-A894-EE06993DAC58}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {994CC558-A9A9-4E58-A894-EE06993DAC58}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {994CC558-A9A9-4E58-A894-EE06993DAC58}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3928C9ED-4F7C-4D3E-8265-C98EBC85F318}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -28,4 +80,15 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4ADC353B-52B1-43B2-B26D-3279A6ACC978}
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DDDC0C74-C130-46DD-A0BC-1AC7FFF626F9} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}
+ {BDF34CB3-4D51-45A3-B9BB-7DE6953B9AC5} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}
+ {74D55924-777B-4FC3-B8F5-37508B2EBAE9} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}
+ {1FC79883-C4C0-4C61-A3F1-D699F7A793B2} = {08EA7A60-9BAA-4B5F-B6EA-21A68328B2F6}
+ {45AC8461-58BE-4675-AD96-89CF88CF8CA3} = {540EE2A5-907D-4E6A-A051-22D01DBFE913}
+ {32045C10-4395-4010-A365-6CAF4D993680} = {540EE2A5-907D-4E6A-A051-22D01DBFE913}
+ {BB2630C0-3F25-49F1-9393-4B75A7014187} = {540EE2A5-907D-4E6A-A051-22D01DBFE913}
+ {994CC558-A9A9-4E58-A894-EE06993DAC58} = {540EE2A5-907D-4E6A-A051-22D01DBFE913}
+ {3928C9ED-4F7C-4D3E-8265-C98EBC85F318} = {540EE2A5-907D-4E6A-A051-22D01DBFE913}
+ EndGlobalSection
EndGlobal
diff --git a/Server/Account b/Server/Account
deleted file mode 100644
index abbc85c..0000000
--- a/Server/Account
+++ /dev/null
@@ -1 +0,0 @@
-{"d3441061-4c3d-43ca-8aef-b733525ecd22":{"Id":"d3441061-4c3d-43ca-8aef-b733525ecd22","Login":"123","Password":"123123"}}
\ No newline at end of file
diff --git a/Server/Contracts/AccountDto.cs b/Server/Contracts/AccountDto.cs
deleted file mode 100644
index bec930a..0000000
--- a/Server/Contracts/AccountDto.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Server.Contracts
-{
- public class AccountDto
- {
- public string SessionId { get; set; }
-
- [Required(ErrorMessage = "Login is required!")]
- public string Login { get; init; }
- [Required(ErrorMessage = "Password is required!!")]
- [StringLength(20, MinimumLength=6, ErrorMessage = "Invalid password length")]
- public string Password { get; init; }
- public DateTime LastRequest { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Server/Contracts/StatisticsDto.cs b/Server/Contracts/StatisticsDto.cs
deleted file mode 100644
index 0770d11..0000000
--- a/Server/Contracts/StatisticsDto.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Newtonsoft.Json;
-
-namespace Server.Contracts
-{
- public class StatisticsDto
- {
- [JsonProperty("Login")]
- public string Login { get; set; }
-
- [JsonProperty("Score")]
- public int Score { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Server/Controllers/RoomController.cs b/Server/Controllers/RoomController.cs
deleted file mode 100644
index 8b95afa..0000000
--- a/Server/Controllers/RoomController.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using Server.GameLogic.LogicServices;
-using Server.GameLogic.Models.Impl;
-using System;
-using System.Net;
-using System.Net.Mime;
-using System.Threading.Tasks;
-
-namespace Server.Controllers
-{
- [ApiController]
- [Route("/room")]
- [Produces(MediaTypeNames.Application.Json)]
- public class RoomController : ControllerBase
- {
- private readonly IRoomCoordinator _roomManager;
-
- public RoomController(
- IRoomCoordinator roomManager)
- {
- _roomManager = roomManager;
- }
-
- [HttpPost]
- [Route("create/{sessionId}&{isPrivate}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> CreateRoom(string sessionId, bool isPrivate)
- {
- try
- {
- var resultRoom = await _roomManager.CreateRoom(sessionId, isPrivate);
- if (resultRoom != null)
- {
- return resultRoom;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
-
- }
-
- [HttpPost]
- [Route("join/{sessionId}&{roomId}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> JoinPrivateRoom(string sessionId, string roomId)
- {
- try
- {
- var resultRoom = await _roomManager.JoinPrivateRoom(sessionId, roomId);
-
- if (resultRoom != null)
- {
- return resultRoom;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
- }
-
- [HttpGet]
- [Route("join/{sessionId}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> JoinPublicRoom(string sessionId)
- {
- try
- {
- var resultRoom = await _roomManager.JoinPublicRoom(sessionId);
-
- if (resultRoom != null)
- {
- return resultRoom;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
-
- }
-
- [HttpPut]
- [Route("updateState/{sessionId}&{state}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> UpdatePlayerState(string sessionId, bool state)
- {
- try
- {
- var resultRound = await _roomManager.UpdatePlayerStatus(sessionId, state);
- if (resultRound != null)
- return resultRound;
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
- }
-
- [HttpGet]
- [Route("updateState/{roomId}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> Update(string roomId)
- {
- try
- {
- var resultRound = await _roomManager.UpdateRoom(roomId);
- return resultRound;
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
- }
-
- [HttpGet]
- [Route("create/training/{sessionId}")]
- [ProducesResponseType(typeof(Room), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> CreateTrainingRoom(string sessionId)
- {
- try
- {
- var resultRound = await _roomManager.CreateTrainingRoom(sessionId);
- return resultRound;
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
- }
- [HttpDelete]
- [Route("delete/{roomId}")]
- [ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)] //Probably set new HttpStatus
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> DeleteRoomExit(string roomId)
- {
- try
- {
- var deleted = await _roomManager.DeleteRoom(roomId);
- if (deleted)
- return Ok("Room was deleted successfully!");
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
- }
- }
-}
diff --git a/Server/Controllers/RoundController.cs b/Server/Controllers/RoundController.cs
deleted file mode 100644
index 66cf2b8..0000000
--- a/Server/Controllers/RoundController.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Server.GameLogic.LogicServices;
-using Server.GameLogic.Models.Impl;
-using System;
-using System.Net;
-using System.Net.Mime;
-using System.Threading.Tasks;
-
-namespace Server.Controllers
-{
- [ApiController]
- [Route("/round")]
- [Produces(MediaTypeNames.Application.Json)]
- public class RoundController:ControllerBase
- {
- public RoundController(
- IRoundCoordinator roundManager,
- ILogger logger)
- {
- _roundManager = roundManager;
- _logger = logger;
- }
- [HttpGet]
- [Route("get/{roomId}")]
- [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> GetActualActiveRoundForCurrentRoom(string roomId)
- {
- try
- {
- var resultedRound = await _roundManager.GetCurrentActiveRoundForSpecialRoom(roomId);
- if (resultedRound != null)
- {
- return resultedRound;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
-
- }
- [HttpGet]
- [Route("get/update/{roomId}")]
- [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> UpdateCurrentRound(string roomId)
- {
- try
- {
- var resultedRound = await _roundManager.UpdateRound(roomId);
-
- if (resultedRound != null)
- {
- return resultedRound;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
-
- }
-
- [HttpPatch]
- [Route("move/{roomId}&{sessionId}&{move}")]
- [ProducesResponseType(typeof(Round), (int)HttpStatusCode.OK)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task> PlaceYourMoveToActiveRound(string roomId, string sessionId, int move)
- {
- try
- {
- var resultedRound = await _roundManager.MakeMove(roomId,sessionId,move);
- if (resultedRound != null)
- {
- return resultedRound;
- }
- return BadRequest();
- }
- catch (Exception exception)
- {
- return BadRequest(exception.Message);
- }
-
- }
-
- private readonly IRoundCoordinator _roundManager;
- private readonly ILogger _logger;
- }
-}
diff --git a/Server/Controllers/StatisticsController.cs b/Server/Controllers/StatisticsController.cs
deleted file mode 100644
index 7f48ca4..0000000
--- a/Server/Controllers/StatisticsController.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Server.Contracts;
-using Server.Mappings;
-using Server.Models;
-using Server.Services.Interfaces;
-
-namespace Server.Controllers
-{
-
- [ApiController]
- [Route("/statistics")]
- public class OverallStatisticsController : ControllerBase
- {
- private readonly IDeserializedObject _deserializedStatistics;
- private readonly IAccountManager _accountManager;
- private readonly ILogger _logger; //todo: add somewhere logger
-
-
- public OverallStatisticsController(
- IDeserializedObject deserializedStatistics,
- IAccountManager accountManager,
- ILogger logger)
- {
- _deserializedStatistics = deserializedStatistics;
- _accountManager = accountManager;
- _logger = logger;
- }
-
- [HttpGet]
- [Route("overallStatistics")]
- [ProducesResponseType(typeof(IEnumerable), (int) HttpStatusCode.OK)]
- [ProducesResponseType((int) HttpStatusCode.BadRequest)]
- public ActionResult> GetOverallStatistics()
- {
- try
- {
- var deserialized = _deserializedStatistics.ConcurrentDictionary.Values;
-
- var statisticsList = deserialized.Where(x => x.Score > 10).AsParallel().ToArray();
-
- if (statisticsList.Length < 1)
- return NotFound();
-
- var resultList = statisticsList.Select(statistics => statistics.ToStatisticsDto()).ToList();
-
- return resultList;
- }
- catch (Exception exceptions) //todo: custom exception OR NOT :)
- {
- return BadRequest(exceptions.Message);
- }
-
- }
-
-
- [HttpGet]
- [Route("personalStatistics/{sessionId}")]
- [ProducesResponseType(typeof(Statistics), (int) HttpStatusCode.OK)]
- [ProducesResponseType((int) HttpStatusCode.BadRequest)]
- public ActionResult> GetPersonalStatistics(string sessionId)
- {
- try
- {
- var deserialized = _deserializedStatistics.ConcurrentDictionary.Values;
-
- var thisUserId = _accountManager.AccountsActive
- .FirstOrDefault(x => x.Key.Equals(sessionId)).Value.Id;
-
- var resultStatistics = deserialized.FirstOrDefault(x => x.Id.Equals(thisUserId));
-
- return Ok(resultStatistics);
- }
- catch (Exception exceptions) //todo: custom exception
- {
- return BadRequest(exceptions.Message);
- }
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Controllers/UserController.cs b/Server/Controllers/UserController.cs
deleted file mode 100644
index 71becc4..0000000
--- a/Server/Controllers/UserController.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Net;
-using System.Net.Mime;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Server.Contracts;
-using Server.Exceptions.LogIn;
-using Server.Exceptions.Register;
-using Server.GameLogic.LogicServices;
-using Server.Models;
-using Server.Services.Interfaces;
-
-namespace Server.Controllers
-{
- [ApiController]
- [Route ("/user")]
-
- [Consumes(MediaTypeNames.Application.Json)]
- [Produces(MediaTypeNames.Application.Json)]
- public class UserController : ControllerBase
- {
- private readonly IStorage _accountStorage;
-
- private readonly IStorage _statisticsStorage; //Just to write new statistics field into file along with account
-
- private readonly ILogger _logger;
-
- private readonly IAccountManager _accountManager;
- public UserController(
- IStorage users,
- IStorage statisticsStorage,
- IAccountManager accountManager,
- ILogger logger,
- IRoomCoordinator roomCoordinator
- )
- {
- _accountStorage = users;
- _statisticsStorage = statisticsStorage;
- _accountManager = accountManager;
- _logger = logger;
- }
- ///
- /// Method to log in an account
- ///
- /// Data Transfer Object of account
- /// Status code and response string
- [HttpPost]
- [Route("login")]
- [ProducesResponseType(typeof(string),(int)HttpStatusCode.OK)]
- [ProducesResponseType(typeof(string),(int)HttpStatusCode.BadRequest)]
- public async Task> Login(AccountDto accountDto)
- {
- try
- {
- await _accountManager.LogInAsync(accountDto);
- return Ok($"Signed In as {accountDto.Login}");
-
- }
- catch (ValidationException exception)
- {
- _logger.Log(LogLevel.Warning,"Validation error");
- return BadRequest(exception.Message);
- }
- catch (LoginErrorException exception)
- {
- _logger.Log(LogLevel.Error,exception.Message);
- return BadRequest(exception.Message);
- }
-
- }
-
- ///
- /// Method to register a new account
- ///
- /// Data Transfer Object of account
- /// HttpStatusCode and response string
- [HttpPost]
- [Route("create")]
- [ProducesResponseType(typeof(string), (int)HttpStatusCode.Created)]
- [ProducesResponseType(typeof(string),(int)HttpStatusCode.BadRequest)]
- public async Task> CreateAccount(AccountDto accountDto)
- {
- try
- {
- var account = new Account
- {
- Id = Guid.NewGuid().ToString(),
- Login = accountDto.Login,
- Password = accountDto.Password
- };
- var statistics = new Statistics
- {
- Id = account.Id,
- Login = account.Login,
- };
-
- await _accountStorage.AddAsync(account);
- await _statisticsStorage.AddAsync(statistics);
-
- return Created("", $"Account [{account.Login}] successfully created");
- }
- catch (ValidationException exception)
- {
- _logger.Log(LogLevel.Warning,exception.Message);
- return BadRequest(exception.Message);
- }
- catch (UnknownReasonException exception)
- {
- _logger.Log(LogLevel.Warning, exception.Message);
- return BadRequest(exception.Message);
- }
- }
-
- ///
- /// Method to Log out of account
- ///
- /// Session id of a client
- /// HttpStatusCode
- [Route("logout")]
- [HttpGet("logout/{sessionId}")]
- [ProducesResponseType(typeof(int), (int) HttpStatusCode.OK)]
- [ProducesResponseType(typeof(int), (int) HttpStatusCode.BadRequest)]
- public async Task> LogOut(string sessionId)
- {
- var result = await _accountManager.LogOutAsync(sessionId);
- if (result)
- return (int) HttpStatusCode.OK;
- return (int) HttpStatusCode.Forbidden;
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/LogIn/InvalidCredentialsException.cs b/Server/Exceptions/LogIn/InvalidCredentialsException.cs
deleted file mode 100644
index 3cfba50..0000000
--- a/Server/Exceptions/LogIn/InvalidCredentialsException.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Server.Exceptions.LogIn
-{
- public class InvalidCredentialsException : LoginErrorException
- {
- protected InvalidCredentialsException()
- {
-
- }
-
- public InvalidCredentialsException(string message) :
- base($"Invalid username of password!")
- {
- }
-
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/LogIn/LoginCooldownException.cs b/Server/Exceptions/LogIn/LoginCooldownException.cs
deleted file mode 100644
index 6adc778..0000000
--- a/Server/Exceptions/LogIn/LoginCooldownException.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Server.Exceptions.LogIn
-{
- public class LoginCooldownException : LoginErrorException
- {
- protected LoginCooldownException()
- {
-
- }
-
- public LoginCooldownException(string message, int afterTime) :
- base($"You got a cooldown! Try in {afterTime} seconds")
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/LogIn/LoginErrorException.cs b/Server/Exceptions/LogIn/LoginErrorException.cs
deleted file mode 100644
index 2051446..0000000
--- a/Server/Exceptions/LogIn/LoginErrorException.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-
-namespace Server.Exceptions.LogIn
-{
- public class LoginErrorException : Exception
- {
- protected LoginErrorException()
- {
-
- }
-
- protected LoginErrorException(string message) :
- base(message)
- {
-
- }
-
- public LoginErrorException(string message, Exception inner)
- : base(message, inner)
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/LogIn/UserAlreadySignedInException.cs b/Server/Exceptions/LogIn/UserAlreadySignedInException.cs
deleted file mode 100644
index 3c859f0..0000000
--- a/Server/Exceptions/LogIn/UserAlreadySignedInException.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Server.Exceptions.LogIn
-{
- public class UserAlreadySignedInException : LoginErrorException
- {
- public UserAlreadySignedInException()
- {
-
- }
-
- public UserAlreadySignedInException(string message) : base(string.Format($"Already signed in!"))
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/LogIn/UserNotFoundException.cs b/Server/Exceptions/LogIn/UserNotFoundException.cs
deleted file mode 100644
index f24f04e..0000000
--- a/Server/Exceptions/LogIn/UserNotFoundException.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Server.Exceptions.LogIn
-{
- public class UserNotFoundException : LoginErrorException
- {
- public UserNotFoundException()
- {
- }
- public UserNotFoundException(string message) :
- base($"This user is not found!")
- {
- }
- }
-}
diff --git a/Server/Exceptions/Register/AlreadyExistsException.cs b/Server/Exceptions/Register/AlreadyExistsException.cs
deleted file mode 100644
index 0861364..0000000
--- a/Server/Exceptions/Register/AlreadyExistsException.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Server.Exceptions.Register
-{
- public class AlreadyExistsException : UnknownReasonException
- {
- public AlreadyExistsException()
- {
-
- }
-
- public AlreadyExistsException(string message) : base(string.Format($"This account already exists"))
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Exceptions/Register/UnknownReasonException.cs b/Server/Exceptions/Register/UnknownReasonException.cs
deleted file mode 100644
index 1fc2609..0000000
--- a/Server/Exceptions/Register/UnknownReasonException.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-
-namespace Server.Exceptions.Register
-{
- public class UnknownReasonException : Exception
- {
- public UnknownReasonException()
- {
-
- }
-
- public UnknownReasonException(string message) :
- base(message)
- {
-
- }
-
- public UnknownReasonException(string message, Exception inner) :
- base(message, inner)
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs b/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs
deleted file mode 100644
index 180040a..0000000
--- a/Server/GameLogic/Exceptions/TwinkGameRoomCreationException.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace Server.GameLogic.Exceptions
-{
- public class TwinkGameRoomCreationException:Exception
- {
- public TwinkGameRoomCreationException() :
- base($"Failed to creaate one more game when you are sitting in another room!")
- {
- }
- }
-}
diff --git a/Server/GameLogic/LogicServices/IRoomCoordinator.cs b/Server/GameLogic/LogicServices/IRoomCoordinator.cs
deleted file mode 100644
index 2517a43..0000000
--- a/Server/GameLogic/LogicServices/IRoomCoordinator.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using Server.GameLogic.Models.Impl;
-using System.Collections.Concurrent;
-using System.Threading.Tasks;
-
-namespace Server.GameLogic.LogicServices
-{
- public interface IRoomCoordinator
- {
-
- ///
- /// List of active rooms. Made for monitoring
- ///
- ConcurrentDictionary ActiveRooms { get; }
-
- ///
- /// Method to create a room
- ///
- ///
- ///
- ///
- Task CreateRoom(string sessionId, bool isPrivate);
-
- ///
- /// Method to join a private room by id
- ///
- ///
- ///
- ///
- Task JoinPrivateRoom(string sessionId, string roomId);
-
- ///
- /// Method to join random public room
- ///
- ///
- ///
- Task JoinPublicRoom(string sessionId);
-
- ///
- /// Method to play with Bot
- ///
- ///
- ///
- Task CreateTrainingRoom(string sessionId);
-
- ///
- /// OBSOLETE method to update Rooms
- ///
- ///
- ///
- Task UpdateRoom(Room updated);
-
- ///
- /// Method to update Rooms
- ///
- ///
- ///
- Task UpdateRoom(string roomId);
-
- ///
- /// Updates player status
- ///
- ///
- ///
- ///
- Task UpdatePlayerStatus(string sessionId, bool isReady);
-
- ///
- /// Deletes room
- ///
- ///
- ///
- Task DeleteRoom(string roomId);
- }
-}
diff --git a/Server/GameLogic/LogicServices/IRoundCoordinator.cs b/Server/GameLogic/LogicServices/IRoundCoordinator.cs
deleted file mode 100644
index bb9f9f3..0000000
--- a/Server/GameLogic/LogicServices/IRoundCoordinator.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Collections.Concurrent;
-using System.Threading.Tasks;
-using Server.GameLogic.Models.Impl;
-
-namespace Server.GameLogic.LogicServices
-{
- public interface IRoundCoordinator
- {
- ///
- /// Task to make a move
- ///
- /// current room Id
- /// Current account Id
- /// Move that he has done
- /// Round
- Task MakeMove(string roomId, string accountId, int move);
-
- ///
- /// Dictionary of active rounds. Made for monitoring
- ///
- ConcurrentDictionary ActiveRounds { get; set; }
-
- ///
- /// Gets current active round for special room
- ///
- /// id of this round
- /// Round
- Task GetCurrentActiveRoundForSpecialRoom(string roundId);
-
- ///
- /// Updates rounds
- ///
- /// id of this room
- /// Round
- Task UpdateRound(string roomId);
- }
-}
\ No newline at end of file
diff --git a/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs b/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs
deleted file mode 100644
index 0f8bdb5..0000000
--- a/Server/GameLogic/LogicServices/Impl/RoomCoordinator.cs
+++ /dev/null
@@ -1,296 +0,0 @@
-using Server.Exceptions.LogIn;
-using Server.GameLogic.Exceptions;
-using Server.GameLogic.Models.Impl;
-using Server.Models;
-using Server.Services.Interfaces;
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Server.GameLogic.Models;
-
-namespace Server.GameLogic.LogicServices.Impl
-{
- public class RoomCoordinator : IRoomCoordinator
- {
- private readonly IAccountManager _accountManager;
-
- private readonly IRoundCoordinator _roundCoordinator;
-
- private static readonly Random Random = new();
-
- private Timer _timer;
-
- public ConcurrentDictionary ActiveRooms { get; }
-
- public RoomCoordinator(
- IAccountManager accountManager,
- IRoundCoordinator roundCoordinator)
- {
- _accountManager = accountManager;
- _roundCoordinator = roundCoordinator;
- ActiveRooms = new ConcurrentDictionary();
- _timer = new Timer(CheckRoomDate, null, 0, 10000);
- }
-
- public async Task CreateRoom(string sessionId, bool isPrivate)
- {
- var tasks = Task.Factory.StartNew(() =>
- {
- var account = GetAccountBySessionId(sessionId);
-
- if (ActiveRooms.Any(x => x.Value.Players.Any(p => p.Key.Equals(account.Id))))
- throw new TwinkGameRoomCreationException();
-
- var newRoom = new Room
- {
- RoomId = RandomString(),
- Players = new ConcurrentDictionary(),
- IsPrivate = isPrivate,
- IsReady = false,
- IsRoundEnded = false,
- IsFull = false,
- CreationTime = DateTime.Now
- };
-
- if (newRoom.Players.TryAdd(account.Id, false))
- {
- ActiveRooms.TryAdd(newRoom.RoomId, newRoom);
- }
-
- //_timer = new Timer(tm, null, 0, 10000); //todo: implement
- return newRoom;
- });
- return await tasks;
- }
-
- public async Task JoinPublicRoom(string sessionId)
- {
- var tasks = Task.Factory.StartNew(() =>
- {
- var thisRoom = ActiveRooms
- .FirstOrDefault(x =>
- x.Value.IsPrivate == false
- && x.Value.Players.Count < 2)
- .Value;
-
- var thisAccount = GetAccountBySessionId(sessionId);
-
- thisRoom.Players.TryAdd(thisAccount.Id, false);
-
- return UpdateRoom(thisRoom).Result;
- });
- return await tasks;
- }
-
- public async Task CreateTrainingRoom(string sessionId)
- {
- var tasks = Task.Factory.StartNew(() =>
- {
- var account = GetAccountBySessionId(sessionId);
-
- if (ActiveRooms.Any(x => x.Value.Players.Any(p => p.Key.Equals(account.Id))))
- throw new TwinkGameRoomCreationException();
-
- var newRoom = new Room
- {
- RoomId = RandomString(),
- Players = new ConcurrentDictionary(),
- IsPrivate = true,
- IsReady = false,
- IsRoundEnded = false,
- IsFull = false,
- CreationTime = DateTime.Now
- };
-
- newRoom.Players.TryAdd(account.Id, false);
- newRoom.Players.TryAdd("Bot", true);
- newRoom.IsFull = true;
- ActiveRooms.TryAdd(newRoom.RoomId, newRoom);
-
- return newRoom;
- });
- return await tasks;
- }
-
- public async Task JoinPrivateRoom(string sessionId, string roomId)
- {
- var tasks = Task.Run(() =>
- {
- if (!ActiveRooms.TryGetValue(roomId, out var thisRoom))
- return null; //todo:exception;
-
- if (thisRoom.Players.Count == 2)
- return null;
-
- var newRoom = thisRoom;
- var thisAccount = GetAccountBySessionId(sessionId);
- newRoom.Players.TryAdd(thisAccount.Id, false);
-
- if (newRoom.Players.Count > 1)
- newRoom.IsFull = true;
-
- return ActiveRooms.TryUpdate(roomId,
- newRoom, thisRoom)
- ? newRoom
- : null; //todo: change to exception;
- });
-
- return await tasks;
- }
-
- private async void CheckRoomDate(object state)
- {
- var threads = Task.Factory.StartNew(() =>
- {
- if (ActiveRooms.IsEmpty) return;
- foreach (var room in ActiveRooms)
- {
- if (room.Value.CreationTime.AddMinutes(5) < DateTime.Now && room.Value.CurrentRoundId == null)
- ActiveRooms.TryRemove(room);
- }
- });
- await Task.WhenAll(threads);
- }
-
- public async Task DeleteRoom(string roomId)
- {
- var tasks = Task.Factory.StartNew(() =>
- ActiveRooms.TryRemove(roomId, out _));
- return await tasks;
- }
-
- public async Task UpdateRoom(Room updated)
- {
- var thread = Task.Factory.StartNew(() =>
- {
- ActiveRooms.TryGetValue(updated.RoomId, out var room);
- if (room == null)
- {
- return null; //todo: change into exception;
- }
-
- return ActiveRooms.TryUpdate(room.RoomId,
- updated, room)
- ? room
- : null;
- });
- return await thread;
- }
- public async Task UpdatePlayerStatus(string sessionId, bool isReady)
- {
- var account = GetAccountBySessionId(sessionId);
-
- var room = ActiveRooms.Values
- .FirstOrDefault(x => x.Players.Keys
- .Any(p => p
- .Equals(account.Id)));
-
- var thisRoom = GetRoomByRoomId(room?.RoomId);
-
- if (thisRoom == null)
- return null; //Never performs
-
-
- var (key, oldValue) =
- thisRoom.Players.FirstOrDefault(x => x.Key == account.Id);
-
-
- thisRoom.Players.TryUpdate(key, isReady, oldValue);
-
- if (thisRoom.Players.Values.All(x => x) && thisRoom.Players.Count == 2)
- {
- thisRoom.IsReady = true;
-
- thisRoom.IsFull = true;
-
- if (thisRoom.CurrentRoundId != null)
- return thisRoom;
-
- var round = new Round
- {
- Id = Guid.NewGuid()
- .ToString(),
- IsFinished = false,
- PlayerMoves = new ConcurrentDictionary(),
- TimeFinished = DateTime.Now,
- WinnerId = null,
- LoserId = null,
- };
-
- foreach (var value in thisRoom.Players.Keys.ToList())
- {
- round.PlayerMoves.TryAdd(value, RequiredGameMove.Default);
- }
-
- thisRoom.CurrentRoundId = round.Id;
-
- _roundCoordinator.ActiveRounds.TryAdd(thisRoom.RoomId, round);
-
- }
-
- return await UpdateRoom(thisRoom);
- }
- public async Task UpdateRoom(string roomId)
- {
- try
- {
- var room = GetRoomByRoomId(roomId);
-
- var thisRound = _roundCoordinator.ActiveRounds.FirstOrDefault(x => x.Key.Equals(room.RoomId));
-
- if (thisRound.Value != null && thisRound.Value.IsFinished)
- {
- room.IsReady = false;
- room.IsRoundEnded = false;
- room.CurrentRoundId = null;
- foreach (var (key, value) in room.Players)
- {
- if (key.Equals("Bot"))
- room.Players.TryUpdate(key, true, value);
- else
- {
- room.Players.TryUpdate(key, false, value);
- }
- }
- _roundCoordinator.ActiveRounds.TryRemove(thisRound);
-
- await UpdateRoom(room);
- }
-
- return await UpdateRoom(room);
- }
- catch (Exception exception)
- {
- return null;
- }
-
- }
-
- #region PrivateMethods
- private static string RandomString()
- {
- const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
- return new string(Enumerable.Repeat(chars, 5)
- .Select(s => s[Random.Next(s.Length)]).ToArray());
- }
- private Account GetAccountBySessionId(string sessionId)
- {
- _accountManager.AccountsActive.TryGetValue(sessionId, out var account);
- if (account != null)
- return account;
- throw new UserNotFoundException(nameof(account));
-
- }
- private Room GetRoomByRoomId(string roomId)
- {
- return ActiveRooms.TryGetValue(roomId, out var thisRoom)
- ? thisRoom
- : throw new UserNotFoundException();
- }
-
- #endregion
-
- }
-}
\ No newline at end of file
diff --git a/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs b/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs
deleted file mode 100644
index 81ea48e..0000000
--- a/Server/GameLogic/LogicServices/Impl/RoundCoordinator.cs
+++ /dev/null
@@ -1,332 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
-using System.Threading.Tasks;
-using Server.GameLogic.Models;
-using Server.GameLogic.Models.Impl;
-using Server.Models;
-using Server.Services.Interfaces;
-
-namespace Server.GameLogic.LogicServices.Impl
-{
- public class RoundCoordinator : IRoundCoordinator
- {
- private readonly IStorage _storageRounds;
-
- private readonly IStorage _storageStatistics;
-
- private readonly IAccountManager _accountManager;
-
- public ConcurrentDictionary ActiveRounds { get; set; }
-
- public RoundCoordinator(
- IStorage storageRounds,
- IAccountManager accountManager,
- IStorage storageStatistics)
- {
- _storageRounds = storageRounds;
- _accountManager = accountManager;
- _storageStatistics = storageStatistics;
- ActiveRounds = new ConcurrentDictionary();
- }
-
-
- public async Task MakeMove(string roomId, string sessionId, int move)
- {
- var tasks = Task.Factory.StartNew(async () =>
- {
- var accountId = _accountManager.GetActiveAccountBySessionId(sessionId).Id;
- ActiveRounds.TryGetValue(roomId, out var thisRound);
-
- if (thisRound == null)
- return null; //todo: exception;
-
- if (thisRound.IsFinished)
- {
- return thisRound;
- }
-
- //************************************************************************************************************************************
- var elapsedTime = DateTime.Now.Subtract(thisRound.TimeFinished);
- if (elapsedTime.Seconds>= 200 &&
- thisRound.PlayerMoves.Any(x => x.Value.Equals(RequiredGameMove.Default)))
- {
- var dictionary = thisRound.PlayerMoves;
- var first = dictionary.Keys.First();
- var last = dictionary.Keys.Last();
-
- if (dictionary[first] == dictionary[last])
- thisRound.IsDraw = false;
- else if (dictionary[first] == RequiredGameMove.Default)
- {
- thisRound.LoserId = first;
- thisRound.WinnerId = last;
- }
- else
- {
- thisRound.LoserId = last;
- thisRound.WinnerId = first;
- }
- thisRound.TimeFinished = DateTime.Now;
- thisRound.IsFinished = true;
-
- await UpdateRound(thisRound);
- return thisRound;
- }
-
- thisRound.TimeFinished = DateTime.Now;
-
- //************************************************************************************************************************
-
- var botPlays = false;
- if (thisRound.PlayerMoves.Any(x => x.Key.Equals("Bot")))
- {
- thisRound.PlayerMoves = RockPaperScissors.ChangeBotState(thisRound.PlayerMoves);
- botPlays = true;
- }
- thisRound.PlayerMoves = RockPaperScissors.UpdateMove(thisRound.PlayerMoves, accountId, move);
-
- if (thisRound.PlayerMoves.Values.All(x => x != RequiredGameMove.Default))
- {
- var winner = RockPaperScissors.MoveComparator(thisRound.PlayerMoves);
-
- if (string.IsNullOrEmpty(winner))
- {
- thisRound.IsDraw = false;
- thisRound.WinnerId = "DRAW";
- thisRound.LoserId = "DRAW";
- await UpdateRound(thisRound);
- }
-
- if (botPlays)
- {
- thisRound.IsFinished = true;
- var thisPlayerKey =
- thisRound.LoserId = thisRound.PlayerMoves
- .FirstOrDefault(x => x.Key != "Bot").Key;
- if (winner == "Bot")
- {
- thisRound.WinnerId = winner;
- thisRound.LoserId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==thisPlayerKey).Value.Login;
- }
- else
- {
- thisRound.WinnerId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==winner).Value.Login;
- thisRound.LoserId = "Bot";
- }
- }
-
- else
- {
- var loserId = thisRound.PlayerMoves.FirstOrDefault(x => x.Key != winner).Key;
-
- ////////////////////////////////////////////////////////////////////////////
- if (thisRound.WinnerId == "DRAW" || thisRound.LoserId == "DRAW")
- {
- thisRound.IsFinished = true;
- thisRound.WinnerId = "DRAW";
- thisRound.LoserId = "DRAW";
- thisRound.TimeFinished = DateTime.Now;
- }
- else
- {
- thisRound.IsFinished = true;
- thisRound.WinnerId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==winner).Value.Login;
- thisRound.LoserId = _accountManager.AccountsActive.FirstOrDefault(x=> x.Value.Id==loserId).Value.Login;
- thisRound.TimeFinished = DateTime.Now;
- }
- _storageRounds.Add(thisRound);
- await FillStatistics(thisRound);
- }
-
- }
-
- await UpdateRound(thisRound);
-
- return thisRound;
-
- });
- return await await tasks; //AWAIT AWAIT?
-
- }
-
- private async Task FillStatistics(IRound thisRound)
- {
- if (thisRound.WinnerId == "DRAW" || thisRound.LoserId == "DRAW")
- return;
- var keys = thisRound.PlayerMoves.Keys;
- foreach (var key in keys) //FIX
- {
- var thisAccountLogin = _accountManager.AccountsActive.FirstOrDefault(x => x.Value.Id == key);
- var statistics = await _storageStatistics.GetAsync(key); //here is the problem.
-
- if (thisRound.WinnerId.Equals(thisAccountLogin.Value.Login))
- {
- statistics.Wins += 1;
- statistics.Score += 4;
- }
- else
- {
- statistics.Loss += 1;
-
- if (statistics.Score == 0)
- statistics.Score = 0;
- else
- {
- statistics.Score -= 2;
- }
- }
-
- var playerMove =
- thisRound.PlayerMoves.FirstOrDefault(x => x.Key.Equals(key)).Value;
- switch (playerMove) //NOT TO ADD ANYTHING ELSE
- {
- case RequiredGameMove.Paper:
- statistics.UsedPaper += 1;
- break;
- case RequiredGameMove.Rock:
- statistics.UsedRock += 1;
- break;
- case RequiredGameMove.Scissors:
- statistics.UsedScissors += 1;
- break;
- }
-
- if (statistics.Loss != 0)
- // ReSharper disable once PossibleLossOfFraction
- statistics.WinLossRatio = statistics.Wins / statistics.Loss * 100d;
- else
- {
- statistics.WinLossRatio = 100d;
- }
-
- var allRound = await _storageRounds.GetAllAsync();
-
- int wins=0, loss=0;
-
- foreach (var round in allRound)
- {
- if (!InRange(round.TimeFinished,DateTime.Now.AddDays(-7), DateTime.Now)) continue;
- if (round.WinnerId.Equals(key))
- wins++;
- else if (round.LoserId.Equals(key))
- {
- loss++;
- }
- }
-
- var winRate = 0d;
- if (loss == 0)
- winRate = 0d;
- else
- {
- winRate = (float) wins / loss * 100d;
- }
-
- statistics.TimeSpent = $"Last 7 days win rate: {winRate}%";
-
- await _storageStatistics.UpdateAsync(key, statistics);
-
- }
- }
-
- private static bool InRange(DateTime dateToCheck, DateTime startDate, DateTime endDate)
- {
- return dateToCheck >= startDate && dateToCheck < endDate;
- }
-
-
- //*****************************
- private async Task UpdateRound(Round updated)
- {
- var task = Task.Factory.StartNew(async () =>
- {
- var roomId =
- ActiveRounds.Where(x => x.Value
- .Equals(updated)).ToArray();
-
-
-
- if (updated.IsFinished)
- {
- //if (!updated.PlayerMoves.All(x => x.Key != "Bot") || updated.IsDraw) return updated;
-
-
- //ActiveRounds.TryRemove(roomId[0].Key, out _);
-
- return updated;
- }
- ActiveRounds.TryGetValue(roomId[0].Key, out var oldRoom);
- ActiveRounds.TryUpdate(roomId[0].Key, updated, oldRoom);
-
- return updated;
- });
-
- return await await task;
- }
-
- //********************************
- public async Task UpdateRound(string roomId)
- {
- var task = Task.Factory.StartNew(async () =>
- {
- ActiveRounds.TryGetValue(roomId, out var updated);
-
- if (updated == null)
- return null; //todo: add exception;
-
- //***************************************************************
- var elapsedTime = DateTime.Now.Subtract(updated.TimeFinished);
- if (elapsedTime.Seconds>= 20 &&
- updated.PlayerMoves.Any(x => x.Value.Equals(RequiredGameMove.Default)))
- {
- var dictionary = updated.PlayerMoves;
- var first = dictionary.Keys.First();
- var last = dictionary.Keys.First();
-
- if (dictionary[first] == dictionary[last])
- updated.IsDraw = false;
- else if (dictionary[first] == RequiredGameMove.Default)
- {
- updated.LoserId = first;
- updated.WinnerId = last;
- }
- else
- {
- updated.LoserId = last;
- updated.WinnerId = first;
- }
- updated.TimeFinished = DateTime.Now;
- updated.IsFinished = true;
-
- await UpdateRound(updated);
- return updated;
- }
- //*****************************************************************
-
- if (updated.IsFinished)
- {
- //if(updated.PlayerMoves.Keys.Any(x=> x!="Bot"))
- //await _storageRounds.AddAsync(updated);
-
- //ActiveRounds.TryRemove(roomId, out _);
-
- return updated;
- }
-
- ActiveRounds.TryGetValue(roomId, out var oldRoom); //Do something with this
- ActiveRounds.TryUpdate(roomId, updated, oldRoom);
-
- return updated;
- });
-
- return await await task; //Task>??????????????????
- }
- public async Task GetCurrentActiveRoundForSpecialRoom(string roundId)
- {
- var tasks = Task.Factory.StartNew(() => ActiveRounds.TryGetValue(roundId, out var thisRound) ? thisRound : null);
- //todo: change null to exception;
- return await tasks;
- }
- }
-}
\ No newline at end of file
diff --git a/Server/GameLogic/Models/IRoom.cs b/Server/GameLogic/Models/IRoom.cs
deleted file mode 100644
index de5ec72..0000000
--- a/Server/GameLogic/Models/IRoom.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Newtonsoft.Json;
-using System;
-using System.Collections.Concurrent;
-
-namespace Server.GameLogic.Models
-{
- public interface IRoom
- {
- [JsonProperty("RoomId")]
- string RoomId { get; set; }
- [JsonProperty("Players")]
- ConcurrentDictionary Players { get; set; }
- [JsonProperty("CurrentRoundId")]
- string CurrentRoundId { get; set; }
- [JsonProperty("CreationTime")]
- DateTime CreationTime { get; set; }
- }
-
-}
diff --git a/Server/GameLogic/Models/IRound.cs b/Server/GameLogic/Models/IRound.cs
deleted file mode 100644
index 96b9509..0000000
--- a/Server/GameLogic/Models/IRound.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Text.Json.Serialization;
-
-namespace Server.GameLogic.Models
-{
- public interface IRound
- {
- [JsonPropertyName("Id")]
- string Id { get; init; }
-
- [JsonPropertyName("RoomId")]
- string RoomId { get; set; }
-
- [JsonPropertyName("Moves")]
- public ConcurrentDictionary PlayerMoves { get; set; }
-
- [JsonPropertyName("IsFinished")]
- bool IsFinished { get; set; }
-
- [JsonPropertyName("TimeFinished")]
- DateTime TimeFinished { get; set; }
-
- [JsonPropertyName("WinnerId")]
- string WinnerId { get; set; }
-
- [JsonPropertyName("LoserId")]
- string LoserId { get; set; }
- public bool IsDraw { get; set; }
- }
-}
diff --git a/Server/GameLogic/Models/Impl/Room.cs b/Server/GameLogic/Models/Impl/Room.cs
deleted file mode 100644
index f97de69..0000000
--- a/Server/GameLogic/Models/Impl/Room.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-
-namespace Server.GameLogic.Models.Impl
-{
- public class Room : IRoom
- {
- ///
- /// Id of the room. Consists of 5 randomized chars
- ///
- public string RoomId { get; set; }
-
- ///
- /// ConcurrentDictionary of all players.
- /// Where string - account Id. Bool - flag is he ready
- ///
- public ConcurrentDictionary Players { get; set; }
-
- ///
- /// Id of current round
- ///
- public string CurrentRoundId { get; set; }
-
- ///
- /// Flag is this room is private
- ///
- public bool IsPrivate { get; set; }
-
- ///
- /// Flag if everyone in this room is ready
- ///
- public bool IsReady { get; set; }
-
- ///
- /// Flag if room is full
- ///
- public bool IsFull { get; set; }
-
- ///
- /// Creation date. After 5 minutes of inactivity will be deleted
- ///
- public DateTime CreationTime { get; set; }
-
- ///
- /// Flag is current count has ended
- ///
- public bool IsRoundEnded { get; set; }
-
- }
-}
diff --git a/Server/GameLogic/Models/Impl/Round.cs b/Server/GameLogic/Models/Impl/Round.cs
deleted file mode 100644
index 40051be..0000000
--- a/Server/GameLogic/Models/Impl/Round.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-
-namespace Server.GameLogic.Models.Impl
-{
- public class Round : IRound
- {
- ///
- /// Id of current round. GUID
- ///
- public string Id { get; init; }
-
- ///
- /// Id of room, Where this round is situated. OBSOLETE
- ///
-
- public string RoomId { get; set; }
-
- ///
- /// Flag is this round has ended
- ///
- public bool IsFinished { get; set; }
-
- ///
- /// Dictionary of player Id and his move
- ///
- public ConcurrentDictionary PlayerMoves { get; set; }
-
- ///
- /// Is the result is draw
- ///
- public bool IsDraw { get; set; }
-
- ///
- /// Time of finishing this room. Also used to check 20 seconds for the move.
- ///
- public DateTime TimeFinished { get; set; }
-
- ///
- /// Login of winner Id
- ///
- public string WinnerId { get; set; }
-
- ///
- /// Login of loser Id
- ///
- public string LoserId { get; set; }
-
- }
-}
diff --git a/Server/GameLogic/Models/RequiredGameMove.cs b/Server/GameLogic/Models/RequiredGameMove.cs
deleted file mode 100644
index bcf6cd4..0000000
--- a/Server/GameLogic/Models/RequiredGameMove.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Server.GameLogic.Models
-{
- ///
- /// Enumerator of available player operations
- ///
- public enum RequiredGameMove
- {
- Default = 0,
- Rock = 1,
- Paper = 2,
- Scissors = 3
- }
-}
diff --git a/Server/GameLogic/RockPaperScissors.cs b/Server/GameLogic/RockPaperScissors.cs
deleted file mode 100644
index acd94ce..0000000
--- a/Server/GameLogic/RockPaperScissors.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
-using Server.GameLogic.Models;
-
-namespace Server.GameLogic
-{
- public static class RockPaperScissors
- {
- public static ConcurrentDictionary UpdateMove(
- ConcurrentDictionary playerMoves, string accountId, int move)
- {
- playerMoves.TryUpdate(accountId, (RequiredGameMove)move,
- playerMoves.FirstOrDefault(x=> x.Key.Equals(accountId)).Value);
-
- return playerMoves;
- }
-
- ///
- /// Implementation or Rock Paper Scissors Game.
- ///
- /// ConcurrentDictionary of players
- /// string Winner
- public static string MoveComparator(
- ConcurrentDictionary playerMoves)
- {
- var surrenderMove = 0;
- var winner = string.Empty;
-
- foreach (var (key, value) in playerMoves)
- {
-
- if (surrenderMove == 0 && string.IsNullOrEmpty(winner))
- {
- winner = key;
- surrenderMove = (int)value;
- }
- else
- {
- if ((int)value == surrenderMove)
- {
- return string.Empty;
- }
-
- if (surrenderMove == 1 && (int)value == 3)
- continue;
- if (surrenderMove == 2 && (int)value == 1)
- continue;
- else if (surrenderMove == 3 && (int)value == 2)
- continue;
- else if (surrenderMove == 1 && (int)value == 2)
- winner = key;
- else if (surrenderMove == 2 && (int)value == 3)
- winner = key;
- else if (surrenderMove == 3 && (int)value == 1)
- winner = key;
- }
- }
- return winner;
- }
-
- ///
- /// Changes Bot move if he is in room
- ///
- /// ConcurrentDictionary of players
- /// ConcurrentDictionary of players
- public static ConcurrentDictionary ChangeBotState(
- ConcurrentDictionary playerMoves)
- {
- var (key, value) = playerMoves.FirstOrDefault(x => x.Key.Equals("Bot"));
- playerMoves.TryUpdate(key, GenerateRandomMove(), value);
-
- return playerMoves;
- }
-
- ///
- /// Gets randomized move for the bot
- ///
- /// Enumerator RequiredGameMove
- private static RequiredGameMove GenerateRandomMove()
- {
- var r = new Random();
- var rInt = r.Next(1, 4);
-
- return (RequiredGameMove)rInt;
- }
- }
-
-}
\ No newline at end of file
diff --git a/Server/Mappings/StatisticsMappings.cs b/Server/Mappings/StatisticsMappings.cs
deleted file mode 100644
index 82aa919..0000000
--- a/Server/Mappings/StatisticsMappings.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Server.Contracts;
-using Server.Models;
-
-namespace Server.Mappings
-{
- public static class StatisticsMappings
- {
- ///
- /// Method to map Big statistics to a small overall statistics
- ///
- /// Big statistics of all accounts
- /// StatisticsDto
- public static StatisticsDto ToStatisticsDto(this Statistics statistics)
- {
- return statistics == null
- ? null
- : new StatisticsDto
- {
- Login = statistics.Login,
- Score = statistics.Score
-
- };
-
- }
- }
-}
\ No newline at end of file
diff --git a/Server/Models/Account.cs b/Server/Models/Account.cs
deleted file mode 100644
index 2e13543..0000000
--- a/Server/Models/Account.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using Server.Models.Interfaces;
-
-namespace Server.Models
-{
- public class Account : IAccount
- {
-
- ///
- /// Id of account. Unique to everyone and similar with Statistics Id
- ///
- public string Id { get; init; }
-
- ///
- /// Nick name of Account
- ///
- public string Login { get; set; }
-
- ///
- /// Password of the Account
- ///
- [StringLength(20, MinimumLength=5, ErrorMessage = "Invalid password length")]
- public string Password { get; set; }
-
- }
-}
\ No newline at end of file
diff --git a/Server/Models/Interfaces/IAccount.cs b/Server/Models/Interfaces/IAccount.cs
deleted file mode 100644
index dd2d657..0000000
--- a/Server/Models/Interfaces/IAccount.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Text.Json.Serialization;
-namespace Server.Models.Interfaces
-{
- public interface IAccount
- {
- [JsonPropertyName("Id")]
- string Id { get; }
-
- [JsonPropertyName("Login")]
- string Login { get; set; }
-
- [JsonPropertyName("Password")]
- string Password { get; set; }
-
-
-
- }
-}
\ No newline at end of file
diff --git a/Server/Models/Interfaces/IStatistics.cs b/Server/Models/Interfaces/IStatistics.cs
deleted file mode 100644
index f6d05c7..0000000
--- a/Server/Models/Interfaces/IStatistics.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Server.Models.Interfaces
-{
- public interface IStatistics
- {
- [JsonPropertyName("Id")]
- string Id { get; set; }
-
- [JsonPropertyName("UserLogin")]
- string Login { get; set; }
-
- [JsonPropertyName("Wins")]
- int Wins { get; set; }
-
- [JsonPropertyName("Loss")]
- int Loss { get; set; }
-
- [JsonPropertyName("WinToLossRatio")]
- double WinLossRatio { get; set; }
-
- [JsonPropertyName("TimeSpent")]
- string TimeSpent { get; set; }
-
- [JsonPropertyName("UsedRock")]
- int UsedRock { get; set; }
-
- [JsonPropertyName("UsedPaper")]
- int UsedPaper { get; set; }
-
- [JsonPropertyName("UsedScissors")]
- int UsedScissors { get; set; }
-
- [JsonPropertyName("Score")]
- int Score { get; set; }
-
- }
-}
\ No newline at end of file
diff --git a/Server/Models/Statistics.cs b/Server/Models/Statistics.cs
deleted file mode 100644
index 526bcee..0000000
--- a/Server/Models/Statistics.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Server.Models.Interfaces;
-
-namespace Server.Models
-{
- public class Statistics : IStatistics
- {
- ///
- /// Id of statistics. Equivalent to Account Id
- ///
- public string Id { get; set; }
-
- ///
- /// Nick name of Account
- ///
- public string Login { get; set; }
-
- ///
- /// Total amount of Wins
- ///
- public int Wins { get; set; }
-
- ///
- /// Total amount of Loses
- ///
- public int Loss { get; set; }
-
- ///
- /// Total amount of Draws. OBSOLETE
- ///
-
- public int Draws { get; set; }
-
- ///
- /// Ratio Wins to Losses. Win/Loss * 100
- ///
- public double WinLossRatio { get; set; }
-
- ///
- /// Ratio for the last 7 days
- ///
- public string TimeSpent { get; set; }
-
- ///
- /// Times used rock
- ///
- public int UsedRock { get; set; }
-
- ///
- /// Times used Paper
- ///
- public int UsedPaper { get; set; }
-
- ///
- /// Times used Scissors
- ///
- public int UsedScissors { get; set; }
-
- ///
- /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points.
- ///
- public int Score { get; set; }
-
-
-
- }
-}
\ No newline at end of file
diff --git a/Server/Program.cs b/Server/Program.cs
deleted file mode 100644
index bb16d90..0000000
--- a/Server/Program.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Serilog;
-
-namespace Server
-{
- public static class Program
- {
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- private static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureLogging(loggingBuilder =>
- {
- loggingBuilder.ClearProviders();
- loggingBuilder.SetMinimumLevel(LogLevel.Information);
- loggingBuilder.AddSerilog(new LoggerConfiguration()
- .WriteTo.Console()
- .WriteTo.File("app.log")
- .CreateLogger());
- })
- .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); });
- }
-}
\ No newline at end of file
diff --git a/Server/Round b/Server/Round
deleted file mode 100644
index e69de29..0000000
diff --git a/Server/Server.Authentication/AuthOptions.cs b/Server/Server.Authentication/AuthOptions.cs
new file mode 100644
index 0000000..eb59595
--- /dev/null
+++ b/Server/Server.Authentication/AuthOptions.cs
@@ -0,0 +1,46 @@
+using System.Text;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Server.Authentication;
+
+///
+/// Options for JWT token..
+///
+public sealed class AuthOptions
+{
+ private static readonly SymmetricSecurityKey DefaultKey = new(Encoding.ASCII.GetBytes(PrivateKey));
+
+ ///
+ /// Token issuer (producer).
+ ///
+ public string Issuer { get; init; } = AppDomain.CurrentDomain.FriendlyName;
+
+ ///
+ /// Token audience (consumer).
+ ///
+ public string Audience { get; init; } = "Player";
+
+ ///
+ /// Token secret part.
+ ///
+ public static string PrivateKey => "RockPaperScissors";
+
+ ///
+ /// Token life time.
+ ///
+ public TimeSpan LifeTime { get; init; } = TimeSpan.FromHours(3);
+
+ ///
+ /// Require HTTPS.
+ ///
+ public bool RequireHttps { get; init; } = false;
+
+ ///
+ /// Getting a symmetric security key.
+ ///
+ /// .
+ public static SymmetricSecurityKey GetSymmetricSecurityKey()
+ {
+ return DefaultKey;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Exceptions/UserException.cs b/Server/Server.Authentication/Exceptions/UserException.cs
new file mode 100644
index 0000000..d9cf49a
--- /dev/null
+++ b/Server/Server.Authentication/Exceptions/UserException.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Http;
+
+namespace Server.Authentication.Exceptions;
+
+///
+/// User-related custom structure exception.
+///
+public sealed class UserException
+{
+ ///
+ /// Constructor.
+ ///
+ /// Exception message.
+ public UserException(string message)
+ {
+ Code = StatusCodes.Status400BadRequest;
+ Message = message;
+ }
+
+ ///
+ /// Gets response code.
+ ///
+ public int Code { get; }
+
+ ///
+ /// Gets exception message.
+ ///
+ public string Message { get; }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs b/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs
new file mode 100644
index 0000000..9695652
--- /dev/null
+++ b/Server/Server.Authentication/Exceptions/UserExceptionsTemplates.cs
@@ -0,0 +1,45 @@
+namespace Server.Authentication.Exceptions;
+
+///
+/// Compile-time exception templates for Authentication module.
+///
+internal static class UserExceptionsTemplates
+{
+ ///
+ /// Builds exception template for Cooldown of Client login attempts.
+ ///
+ /// Client username.
+ /// Date of removing cooldown.
+ /// Created string message.
+ internal static string UserCoolDown(this string username, DateTimeOffset tillCooldownDate)
+ => $"User [{username}] has been cooled down till \"{tillCooldownDate}\".";
+
+ ///
+ /// Builds exception template for invalid credentials for user login attempt.
+ ///
+ /// Client input username.
+ /// Created string message.
+ internal static string UserInvalidCredentials(this string username)
+ => $"Invalid Credentials for user \"{username}\".";
+
+ ///
+ /// Builds exception template for not found client by input parameters.
+ ///
+ /// Client input username.
+ /// Created string message.
+ internal static string UserNotFound(this string username)
+ => $"User with login \"{username}\" is not found.";
+
+ ///
+ /// Builds exception template for registering client, that already exists.
+ ///
+ /// Client input username.
+ /// Created string message.
+ internal static string UserAlreadyExists(this string username)
+ => $"User with login \"{username}\" already exists.";
+
+ ///
+ /// Default template, when error is unknown.
+ ///
+ internal const string UnknownError = "Unknown error occured. Please try again.";
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Extensions/AuthenticationExtension.cs b/Server/Server.Authentication/Extensions/AuthenticationExtension.cs
new file mode 100644
index 0000000..d6b76de
--- /dev/null
+++ b/Server/Server.Authentication/Extensions/AuthenticationExtension.cs
@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using Server.Authentication.Services;
+
+namespace Server.Authentication.Extensions;
+
+///
+/// Authentication services extension.
+///
+public static class AuthenticationExtension
+{
+ ///
+ /// Registers authentication.
+ ///
+ /// Service collection.
+ public static IServiceCollection AddAuthentications(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddOptions();
+
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ var jwtOptions = services
+ .BuildServiceProvider()
+ .GetRequiredService>()
+ .Value;
+
+ options.RequireHttpsMetadata = jwtOptions.RequireHttps;
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidIssuer = jwtOptions.Issuer,
+
+ ValidateAudience = true,
+ ValidAudience = jwtOptions.Audience,
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.Zero,
+
+ IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
+ ValidateIssuerSigningKey = true
+ };
+ });
+
+ services.AddTransient();
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/HashingBase64.cs b/Server/Server.Authentication/HashingBase64.cs
new file mode 100644
index 0000000..6fe2eb0
--- /dev/null
+++ b/Server/Server.Authentication/HashingBase64.cs
@@ -0,0 +1,51 @@
+namespace Server.Authentication;
+
+///
+/// Default hashing base64 methods.
+///
+public static class HashingBase64
+{
+ ///
+ /// Decodes input string from BASE64 to a regular string.
+ ///
+ /// Encoded in BASE64 payload.
+ /// Decoded string.
+ public static string DecodeBase64(this string encodedData)
+ {
+ var encodedDataAsBytes
+ = Convert.FromBase64String(encodedData);
+
+ return
+ System.Text.Encoding.ASCII.GetString(encodedDataAsBytes);
+ }
+
+ ///
+ /// Encodes input string to BASE64 From a regular string.
+ ///
+ /// Payload to be encoded.
+ /// Encoded string.
+ public static string EncodeBase64(this string initialData)
+ {
+ var dataArray = System.Text.Encoding.ASCII.GetBytes(initialData);
+
+ return Convert.ToBase64String(dataArray);
+ }
+
+ ///
+ /// Compares initial encoded data with base64 data.
+ ///
+ /// Encoded data.
+ /// Initial payload.
+ ///
+ ///
+ /// true - when initial data, encoded in base64 is identical with base64 data.
+ ///
+ /// false - otherwise.
+ ///
+ public static bool IsHashEqual(this string base64Data, string initialData)
+ {
+ var baseDecoded = EncodeBase64(initialData);
+
+ return base64Data.Equals(baseDecoded, StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Models/AccountOutputModel.cs b/Server/Server.Authentication/Models/AccountOutputModel.cs
new file mode 100644
index 0000000..7ad8fd2
--- /dev/null
+++ b/Server/Server.Authentication/Models/AccountOutputModel.cs
@@ -0,0 +1,17 @@
+namespace Server.Authentication.Models;
+
+///
+/// Model for output of user account.
+///
+public sealed class AccountOutputModel
+{
+ ///
+ /// Gets or sets user token (used in header).
+ ///
+ public string Token { get; init; }
+
+ ///
+ /// Gets or sets user login.
+ ///
+ public string Login { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Models/Roles.cs b/Server/Server.Authentication/Models/Roles.cs
new file mode 100644
index 0000000..76bcbb4
--- /dev/null
+++ b/Server/Server.Authentication/Models/Roles.cs
@@ -0,0 +1,17 @@
+namespace Server.Authentication.Models;
+
+///
+/// User roles.
+///
+public static class Roles
+{
+ ///
+ /// Admin role.
+ ///
+ public const string Admin = "admin";
+
+ ///
+ /// User role.
+ ///
+ public const string User = "user";
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Server.Authentication.csproj b/Server/Server.Authentication/Server.Authentication.csproj
new file mode 100644
index 0000000..967b793
--- /dev/null
+++ b/Server/Server.Authentication/Server.Authentication.csproj
@@ -0,0 +1,19 @@
+
+
+
+ true
+ net6
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Server/Server.Authentication/Services/AttemptValidationService.cs b/Server/Server.Authentication/Services/AttemptValidationService.cs
new file mode 100644
index 0000000..960d209
--- /dev/null
+++ b/Server/Server.Authentication/Services/AttemptValidationService.cs
@@ -0,0 +1,71 @@
+using System.Collections.Concurrent;
+
+namespace Server.Authentication.Services;
+
+///
+/// Static service with validation features.
+///
+internal static class AttemptValidationService
+{
+ // key - userId. Value - count of failed attempts
+ private static readonly ConcurrentDictionary FailedAttempts = new();
+ private static readonly ConcurrentDictionary CoolDownCollection = new();
+
+ public static bool TryInsertFailAttempt(this string userId)
+ {
+ if (string.IsNullOrEmpty(userId))
+ {
+ return false;
+ }
+
+ if (FailedAttempts.TryGetValue(userId, out var failedAttempts))
+ {
+ if (failedAttempts >= 2)
+ {
+ // todo: in options
+ CoolDownCollection.TryAdd(userId, DateTimeOffset.UtcNow.AddMinutes(2));
+ FailedAttempts.TryRemove(userId, out _);
+
+ return true;
+ }
+ }
+ FailedAttempts.AddOrUpdate(userId, 1, (_, i) => i + 1);
+
+ return true;
+ }
+
+ public static int? CountFailedAttempts(this string userId)
+ {
+ if (string.IsNullOrEmpty(userId))
+ {
+ return default;
+ }
+
+ return FailedAttempts.TryGetValue(userId, out var failedAttempts)
+ ? failedAttempts
+ : default;
+ }
+
+ public static bool IsCoolDown(this string userId, out DateTimeOffset coolDownDate)
+ {
+ if (string.IsNullOrEmpty(userId))
+ {
+ coolDownDate = default;
+ return false;
+ }
+
+ if (!CoolDownCollection.TryGetValue(userId, out coolDownDate))
+ {
+ return false;
+ }
+
+ if (coolDownDate >= DateTimeOffset.UtcNow)
+ {
+ return true;
+ }
+
+ CoolDownCollection.TryRemove(userId, out coolDownDate);
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Services/AuthService.cs b/Server/Server.Authentication/Services/AuthService.cs
new file mode 100644
index 0000000..7edb77f
--- /dev/null
+++ b/Server/Server.Authentication/Services/AuthService.cs
@@ -0,0 +1,185 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using OneOf;
+using Server.Authentication.Exceptions;
+using Server.Authentication.Models;
+using Server.Data.Context;
+using Server.Data.Entities;
+
+namespace Server.Authentication.Services;
+
+///
+internal sealed class AuthService : IAuthService
+{
+ private static readonly SigningCredentials SigningCredentials = new(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
+ private static readonly SemaphoreSlim Semaphore = new(initialCount: 1, maxCount: 1);
+ private static readonly JwtSecurityTokenHandler TokenHandler = new();
+
+ private readonly ServerContext _repository;
+ private readonly AuthOptions _authOptions;
+ private readonly ILogger _logger;
+
+ ///
+ /// Constructor.
+ ///
+ /// .
+ /// .
+ /// .
+ public AuthService(
+ ILogger logger,
+ IOptions authOptions,
+ ServerContext repository)
+ {
+ _logger = logger;
+ _repository = repository;
+ _authOptions = authOptions.Value;
+ }
+
+ ///
+ public async Task> RegisterAsync(
+ string login,
+ string password)
+ {
+ if (string.IsNullOrWhiteSpace(login))
+ {
+ _logger.LogError("Login should not be 'empty'");
+
+ return new UserException(nameof(login).UserInvalidCredentials());
+ }
+
+ if (string.IsNullOrEmpty(password))
+ {
+ _logger.LogError("Password should not be 'empty'");
+
+ return new UserException(nameof(password).UserInvalidCredentials());
+ }
+
+ var release = await Semaphore.WaitAsync(100);
+
+ try
+ {
+ if (await _repository.Accounts.AnyAsync(account => account.Login.Equals(login.ToLower())))
+ {
+ var exceptionMessage = login.UserAlreadyExists();
+
+ _logger.LogError("Error occured : {ExceptionMessage}", exceptionMessage);
+
+ return new UserException(exceptionMessage);
+ }
+
+ var accountId = Guid.NewGuid().ToString();
+
+ var account = new Account
+ {
+ Id = accountId,
+ Login = login,
+ Password = password.EncodeBase64(),
+ };
+
+ _repository.Accounts.Add(account);
+
+ var accountStatistics = new Statistics
+ {
+ Id = accountId,
+ AccountId = accountId
+ };
+
+ _repository.StatisticsEnumerable.Add(accountStatistics);
+ await _repository.SaveChangesAsync();
+
+ return StatusCodes.Status200OK;
+ }
+ catch
+ {
+ _logger.LogWarning("Unable to process account for {Login}", login);
+
+ return new UserException(UserExceptionsTemplates.UnknownError);
+ }
+ finally
+ {
+ if (release)
+ {
+ Semaphore.Release();
+ }
+ }
+ }
+
+ ///
+ public async Task> LoginAsync(string login, string password)
+ {
+ var userAccount = await _repository.Accounts.FirstOrDefaultAsync(account => account.Login.ToLower().Equals(login.ToLower()));
+
+ string exceptionMessage;
+
+ if (userAccount is null)
+ {
+ exceptionMessage = login.UserNotFound();
+ _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage);
+
+ return new UserException(exceptionMessage);
+ }
+
+ if (login.IsCoolDown(out var coolRequestDate))
+ {
+ exceptionMessage = login.UserCoolDown(coolRequestDate);
+ _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage);
+
+ return new UserException(exceptionMessage);
+ }
+
+ if (userAccount.Password.IsHashEqual(password))
+ {
+ return new AccountOutputModel
+ {
+ Token = BuildToken(userAccount),
+ Login = userAccount.Login
+ };
+ }
+
+ login.TryInsertFailAttempt();
+
+ exceptionMessage = login.UserInvalidCredentials();
+ _logger.LogWarning("Error occured: {ExceptionMessage}", exceptionMessage);
+
+ return new UserException(exceptionMessage);
+ }
+
+ private string BuildToken(Account accountModel)
+ {
+ var now = DateTime.UtcNow;
+
+ var encodedJwt = TokenHandler.CreateEncodedJwt(
+ _authOptions.Issuer,
+ _authOptions.Audience,
+ GetClaimsIdentity(accountModel.Id),
+ now,
+ now.Add(_authOptions.LifeTime),
+ now,
+ SigningCredentials);
+
+ return encodedJwt;
+ }
+
+ private static ClaimsIdentity GetClaimsIdentity(string userId)
+ {
+ var claims = new[]
+ {
+ new Claim(ClaimsIdentity.DefaultNameClaimType, userId),
+ new Claim(ClaimsIdentity.DefaultRoleClaimType, Roles.User)
+ };
+ var claimsIdentity =
+ new ClaimsIdentity(
+ claims,
+ JwtBearerDefaults.AuthenticationScheme,
+ ClaimsIdentity.DefaultNameClaimType,
+ ClaimsIdentity.DefaultRoleClaimType);
+
+ return claimsIdentity;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Authentication/Services/IAuthService.cs b/Server/Server.Authentication/Services/IAuthService.cs
new file mode 100644
index 0000000..b48a630
--- /dev/null
+++ b/Server/Server.Authentication/Services/IAuthService.cs
@@ -0,0 +1,39 @@
+using Microsoft.AspNetCore.Http;
+using OneOf;
+using Server.Authentication.Exceptions;
+using Server.Authentication.Models;
+using Server.Data.Entities;
+
+namespace Server.Authentication.Services;
+
+///
+/// Authentication service.
+///
+public interface IAuthService
+{
+ ///
+ /// Registers new entity of Client. ().
+ ///
+ /// Client login, case insensitive.
+ /// Client password.
+ ///
+ ///
+ /// int - if everything is fine.
+ ///
+ /// UserException - If some case of error occured. (User exists, validation error, unknown error).
+ ///
+ Task> RegisterAsync(string login, string password);
+
+ ///
+ /// Signs in client by credentials and building JWT token.
+ ///
+ /// Client login, case insensitive.
+ /// Client password.
+ ///
+ ///
+ /// AccountOutputModel - Constructed object with login and JWT token.
+ ///
+ /// UserException - If some case of error occured. (User exists, validation error, unknown error).
+ ///
+ Task> LoginAsync(string login, string password);
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Exceptions/ExceptionTemplates.cs b/Server/Server.Bll/Exceptions/ExceptionTemplates.cs
new file mode 100644
index 0000000..be194a1
--- /dev/null
+++ b/Server/Server.Bll/Exceptions/ExceptionTemplates.cs
@@ -0,0 +1,20 @@
+namespace Server.Bll.Exceptions;
+
+internal static class ExceptionTemplates
+{
+ // GENERAL EXCEPTION MESSAGES
+ internal const string Unknown = "Unknown error occured. Please try again";
+ internal const string NotAllowed = "You are not allowed to do this.";
+ internal static string NotExists(string entity) => $"{entity} does not exist.";
+
+ // ROOM EXCEPTIONS
+ internal const string TwinkRoom = "Failed to create one more game when you are sitting in another room.";
+ internal const string RoomFull = "This room is full.";
+ internal const string AlreadyInRoom = "You are already in room.";
+ internal const string RoomNotFull = "Room is not full.";
+ internal const string NoAvailableRooms = "Sorry, there are no public rooms available right now.";
+
+ // ROUND EXCEPTIONS
+ internal const string RoundAlreadyCreated = "Round is already creaded.";
+ internal static string RoundNotFound(int roundId) => $"Round with id \"{roundId}\" is not found";
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Extensions/MappingExtensions.cs b/Server/Server.Bll/Extensions/MappingExtensions.cs
new file mode 100644
index 0000000..e8c2e93
--- /dev/null
+++ b/Server/Server.Bll/Extensions/MappingExtensions.cs
@@ -0,0 +1,14 @@
+// using Mapster;
+// using Server.Bll.Models;
+// using Server.Data.Entities;
+//
+// namespace Server.Bll.Extensions;
+//
+// internal static class MappingExtensions
+// {
+// internal static readonly TypeAdapterSetter StatisticsAdapterConfig =
+// TypeAdapterConfig
+// .NewConfig()
+// .Map(shortStatisticsModel => shortStatisticsModel.Login, statistics => statistics.Account.Login)
+// .Map(shortStatisticsModel => shortStatisticsModel.Score, statistics => statistics.Score);
+// }
\ No newline at end of file
diff --git a/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs b/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..5edff9d
--- /dev/null
+++ b/Server/Server.Bll/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,33 @@
+using Mapster;
+using Microsoft.Extensions.DependencyInjection;
+using Server.Bll.Models;
+using Server.Bll.Services;
+using Server.Bll.Services.Interfaces;
+using Server.Data.Entities;
+
+namespace Server.Bll.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddBusinessLogic(this IServiceCollection service)
+ {
+ ArgumentNullException.ThrowIfNull(service);
+
+ TypeAdapterConfig
+ .NewConfig()
+ .Map(shortStatisticsModel => shortStatisticsModel.Login, statistics => statistics.Account.Login)
+ .Map(shortStatisticsModel => shortStatisticsModel.Score, statistics => statistics.Score);
+
+ service
+ .AddTransient()
+ .AddHostedService();
+
+ service.AddHttpContextAccessor();
+
+ service
+ .AddTransient()
+ .AddTransient();
+
+ return service;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/AccountModel.cs b/Server/Server.Bll/Models/AccountModel.cs
new file mode 100644
index 0000000..d102163
--- /dev/null
+++ b/Server/Server.Bll/Models/AccountModel.cs
@@ -0,0 +1,6 @@
+namespace Server.Bll.Models;
+
+public sealed class AccountModel
+{
+ public string Login { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/PlayerModel.cs b/Server/Server.Bll/Models/PlayerModel.cs
new file mode 100644
index 0000000..1586e6f
--- /dev/null
+++ b/Server/Server.Bll/Models/PlayerModel.cs
@@ -0,0 +1,12 @@
+namespace Server.Bll.Models;
+
+public sealed class PlayerModel
+{
+ public string Id { get; init; }
+
+ public int Move { get; init; }
+
+ public bool IsReady { get; init; }
+
+ public bool IsWinner { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/RoomModel.cs b/Server/Server.Bll/Models/RoomModel.cs
new file mode 100644
index 0000000..b39b914
--- /dev/null
+++ b/Server/Server.Bll/Models/RoomModel.cs
@@ -0,0 +1,44 @@
+namespace Server.Bll.Models;
+
+public sealed class RoomModel
+{
+ ///
+ /// Id of the room. Consists of 5 randomized chars
+ ///
+ public string Id { get; init; }
+
+ ///
+ /// Special code to join a room
+ ///
+ public string Code { get; init; }
+
+ ///
+ /// Round, linked to this room
+ ///
+ public RoundModel? Round { get; init; }
+
+ ///
+ /// .
+ ///
+ public ICollection? Players { get; init; }
+
+ ///
+ /// Flag is this room is private
+ ///
+ public bool IsPrivate { get; init; }
+
+ ///
+ /// Flag if room is full
+ ///
+ public bool IsFull { get; init; }
+
+ ///
+ /// Creation date. After 5 minutes of inactivity will be deleted
+ ///
+ public long CreationTimeTicks { get; init; }
+
+ ///
+ /// Last update time ticks.
+ ///
+ public long UpdateTicks { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/RoundModel.cs b/Server/Server.Bll/Models/RoundModel.cs
new file mode 100644
index 0000000..043df70
--- /dev/null
+++ b/Server/Server.Bll/Models/RoundModel.cs
@@ -0,0 +1,14 @@
+namespace Server.Bll.Models;
+
+public sealed class RoundModel
+{
+ public string Id { get; init; }
+
+ public bool IsFinished { get; init; }
+
+ public long StartTimeTicks { get; init; }
+
+ public long FinishTimeTicks { get; init; }
+
+ public long UpdateTicks { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/ShortStatisticsModel.cs b/Server/Server.Bll/Models/ShortStatisticsModel.cs
new file mode 100644
index 0000000..e349c94
--- /dev/null
+++ b/Server/Server.Bll/Models/ShortStatisticsModel.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Server.Bll.Models;
+
+public sealed class ShortStatisticsModel
+{
+ [JsonPropertyName("login")]
+ public string Login { get; init; }
+
+ ///
+ /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points.
+ ///
+ [JsonPropertyName("score")]
+ public int Score { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Models/StatisticsModel.cs b/Server/Server.Bll/Models/StatisticsModel.cs
new file mode 100644
index 0000000..0ed68a3
--- /dev/null
+++ b/Server/Server.Bll/Models/StatisticsModel.cs
@@ -0,0 +1,50 @@
+namespace Server.Bll.Models;
+
+public sealed class StatisticsModel
+{
+ ///
+ /// Total amount of Wins
+ ///
+ public int? Wins { get; init; }
+
+ ///
+ /// Total amount of Loses
+ ///
+ public int? Loss { get; init; }
+
+ ///
+ /// Total amount of Draws. OBSOLETE
+ ///
+
+ public int? Draws { get; init; }
+
+ ///
+ /// Ratio Wins to Losses. Win/Loss * 100
+ ///
+ public double? WinLossRatio { get; init; }
+
+ ///
+ /// Ratio for the last 7 days
+ ///
+ public string TimeSpent { get; init; }
+
+ ///
+ /// Times used rock
+ ///
+ public int? UsedRock { get; init; }
+
+ ///
+ /// Times used Paper
+ ///
+ public int? UsedPaper { get; init; }
+
+ ///
+ /// Times used Scissors
+ ///
+ public int? UsedScissors { get; init; }
+
+ ///
+ /// Total amount of Points. 1 win = 4 points. 1 lose = -2 points.
+ ///
+ public int? Score { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Options/CleanerOptions.cs b/Server/Server.Bll/Options/CleanerOptions.cs
new file mode 100644
index 0000000..e43abfd
--- /dev/null
+++ b/Server/Server.Bll/Options/CleanerOptions.cs
@@ -0,0 +1,12 @@
+namespace Server.Bll.Options;
+
+public sealed class CleanerOptions
+{
+ public const string Section = "Cleaning";
+
+ public TimeSpan CleanPeriod { get; init; }
+
+ public TimeSpan RoomOutDateTime { get; init; }
+
+ public TimeSpan RoundOutDateTime { get; init; }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Server.Bll.csproj b/Server/Server.Bll/Server.Bll.csproj
new file mode 100644
index 0000000..4656682
--- /dev/null
+++ b/Server/Server.Bll/Server.Bll.csproj
@@ -0,0 +1,25 @@
+
+
+
+ true
+ true
+ net6
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Server/Server.Bll/Services/CleanerBackgroundService.cs b/Server/Server.Bll/Services/CleanerBackgroundService.cs
new file mode 100644
index 0000000..c8792d8
--- /dev/null
+++ b/Server/Server.Bll/Services/CleanerBackgroundService.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Server.Bll.Options;
+using Server.Bll.Services.Interfaces;
+
+namespace Server.Bll.Services;
+
+public sealed class CleanerBackgroundService : BackgroundService
+{
+ private readonly IServiceScopeFactory _serviceProvider;
+ private readonly ILogger _logger;
+ private readonly PeriodicTimer _periodicTimer;
+ private readonly CleanerOptions _cleanerOptions;
+
+ public CleanerBackgroundService(
+ ILogger logger,
+ IServiceScopeFactory serviceProvider,
+ IOptions cleanerOptions)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ _cleanerOptions = cleanerOptions?.Value ?? throw new ArgumentNullException(nameof(cleanerOptions));
+ _periodicTimer = new PeriodicTimer(_cleanerOptions.CleanPeriod);
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("Starting Background service");
+
+ while (await _periodicTimer.WaitForNextTickAsync(stoppingToken))
+ {
+ await CleanJunk(_serviceProvider);
+ }
+ }
+
+ private async Task CleanJunk(IServiceScopeFactory factory)
+ {
+ using var scope = factory.CreateScope();
+ var roomService = scope.ServiceProvider.GetRequiredService();
+ var rooms = await roomService
+ .RemoveRangeAsync(_cleanerOptions.RoomOutDateTime, _cleanerOptions.RoundOutDateTime);
+
+ if (rooms > 0)
+ {
+ _logger.LogInformation("Cleaned {Room} entities", rooms.ToString());
+ }
+ }
+
+ public override void Dispose()
+ {
+ _periodicTimer.Dispose();
+
+ base.Dispose();
+ }
+
+ public override Task StopAsync(CancellationToken cancellationToken)
+ {
+ _periodicTimer.Dispose();
+
+ return base.StopAsync(cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/Interfaces/IRoomService.cs b/Server/Server.Bll/Services/Interfaces/IRoomService.cs
new file mode 100644
index 0000000..d3b0f46
--- /dev/null
+++ b/Server/Server.Bll/Services/Interfaces/IRoomService.cs
@@ -0,0 +1,22 @@
+using OneOf;
+using RockPaperScissors.Common;
+using Server.Bll.Models;
+
+namespace Server.Bll.Services.Interfaces;
+
+public interface IRoomService
+{
+ Task> CreateAsync(string userId, bool isPrivate = false, bool isTraining = false);
+
+ Task RemoveRangeAsync(TimeSpan roomOutDate, TimeSpan roundOutDate);
+
+ Task> JoinAsync(string userId, string? roomCode = null);
+
+ Task> GetAsync(string roomId);
+
+ Task GetUpdateTicksAsync(string roomId);
+
+ Task> ChangePlayerStatusAsync(string userId, string roomId, bool newStatus);
+
+ Task> DeleteAsync(string userId, string roomId);
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/Interfaces/IRoundService.cs b/Server/Server.Bll/Services/Interfaces/IRoundService.cs
new file mode 100644
index 0000000..baa9a19
--- /dev/null
+++ b/Server/Server.Bll/Services/Interfaces/IRoundService.cs
@@ -0,0 +1,12 @@
+using RockPaperScissors.Common;
+using OneOf;
+using RockPaperScissors.Common.Enums;
+
+namespace Server.Bll.Services.Interfaces;
+
+public interface IRoundService
+{
+ Task> MakeMoveAsync(string userId, string roundId, Move move);
+
+ Task GetUpdateTicksAsync(string roundId);
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs b/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs
new file mode 100644
index 0000000..92a06a3
--- /dev/null
+++ b/Server/Server.Bll/Services/Interfaces/IStatisticsService.cs
@@ -0,0 +1,12 @@
+using OneOf;
+using RockPaperScissors.Common;
+using Server.Bll.Models;
+
+namespace Server.Bll.Services.Interfaces;
+
+public interface IStatisticsService
+{
+ Task GetAllAsync();
+
+ Task> GetAsync(string userId);
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/RoomService.cs b/Server/Server.Bll/Services/RoomService.cs
new file mode 100644
index 0000000..012ffff
--- /dev/null
+++ b/Server/Server.Bll/Services/RoomService.cs
@@ -0,0 +1,254 @@
+using Mapster;
+using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
+using OneOf;
+using RockPaperScissors.Common;
+using Server.Bll.Exceptions;
+using Server.Bll.Models;
+using Server.Bll.Services.Interfaces;
+using Server.Data.Context;
+using Server.Data.Entities;
+using Server.Data.Extensions;
+
+namespace Server.Bll.Services;
+
+internal sealed class RoomService : IRoomService
+{
+ private readonly ServerContext _repository;
+
+ public RoomService(ServerContext repository)
+ {
+ _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+ }
+
+ public async Task> CreateAsync(
+ string userId, bool isPrivate, bool isTraining)
+ {
+ var doesRoomExist = await _repository.Rooms
+ .Include(room => room.Players)
+ .AnyAsync(roomPlayers => roomPlayers.Players.Any(player => player.AccountId == userId));
+
+ if (doesRoomExist)
+ {
+ return new CustomException(ExceptionTemplates.TwinkRoom);
+ }
+
+ var players = new List(2)
+ {
+ new()
+ {
+ Id = Guid.NewGuid().ToString(),
+ Account = await _repository.Accounts.FindAsync(userId),
+ AccountId = userId,
+ IsReady = false,
+ }
+ };
+
+ var room = new Room
+ {
+ Id = Guid.NewGuid().ToString(),
+ IsPrivate = isPrivate,
+ Code = Guid.NewGuid().ToString("N")[..8],
+ IsFull = false,
+ CreationTimeTicks = DateTimeOffset.UtcNow.Ticks,
+ Players = players,
+ UpdateTicks = DateTimeOffset.UtcNow.Ticks
+ };
+
+ _repository.Rooms.Add(room);
+
+ if (isTraining)
+ {
+ room.IsPrivate = true;
+ room.IsFull = true;
+ room.Players.Add(SeedingExtension.BotPlayer);
+ room.Round = RoundService.Create(room);
+ }
+
+ await _repository.SaveChangesAsync();
+
+ return room.Adapt();
+ }
+
+ public async Task> ChangePlayerStatusAsync(string userId, string roomId, bool newStatus)
+ {
+ var room = await _repository.Rooms
+ .Include(room => room.Players)
+ .FirstOrDefaultAsync(room => room.Id == roomId);
+
+ if (room is null)
+ {
+ return new CustomException($"Room with id '{roomId}' does not exist");
+ }
+
+ var currentPlayer = room.Players.FirstOrDefault(player => player.AccountId == userId);
+ if (currentPlayer is null)
+ {
+ return new CustomException("You are not able to modify this room");
+ }
+
+ currentPlayer.IsReady = newStatus;
+
+ _repository.Players.Update(currentPlayer);
+ _repository.Rooms.Update(room);
+
+ if (room.Players.All(player => player.IsReady))
+ {
+ room.Round = RoundService.Create(room);
+ await _repository.SaveChangesAsync();
+ }
+
+ await _repository.SaveChangesAsync();
+
+ return room.Adapt();
+ }
+
+ public async Task GetUpdateTicksAsync(string roomId)
+ {
+ var room = await _repository.Rooms
+ .FindAsync(roomId);
+
+ if (room is null)
+ {
+ return -1;
+ }
+
+ return room.UpdateTicks;
+ }
+
+ public async Task> JoinAsync(string userId, string? roomCode)
+ {
+ var oneOfRoom = string.IsNullOrEmpty(roomCode) ? await GetPublicAsync(userId) : await GetPrivateAsync(userId, roomCode);
+
+ if (oneOfRoom.IsT1)
+ {
+ return oneOfRoom.AsT1;
+ }
+
+ var room = oneOfRoom.AsT0;
+
+ var newPlayer = new Player
+ {
+ Id = Guid.NewGuid().ToString(),
+ Account = await _repository.Accounts.FindAsync(userId),
+ AccountId = userId,
+ IsReady = false,
+ };
+
+ room.Players.Add(newPlayer);
+ room.UpdateTicks = DateTimeOffset.UtcNow.Ticks;
+ room.IsFull = room.Players.Count is 2;
+
+ _repository.Rooms.Update(room);
+
+ await _repository.SaveChangesAsync();
+
+ return room.Adapt();
+ }
+
+ public async Task> GetAsync(string roomId)
+ {
+ var room = await _repository.Rooms.FindAsync(roomId);
+
+ if (room is null)
+ {
+ return new CustomException($"Room with id {roomId} does not exist");
+ }
+
+ return room.Adapt();
+ }
+
+ public async Task> DeleteAsync(string userId, string roomId)
+ {
+ var room = await _repository.Rooms.FindAsync(roomId);
+
+ if (room is null)
+ {
+ return new CustomException($"Room with id {roomId} does not exist");
+ }
+
+ if (room.Players.All(player => player.AccountId != userId))
+ {
+ return new CustomException("You have no rights to delete a room");
+ }
+
+ _repository.Rooms.Remove(room);
+
+ await _repository.SaveChangesAsync();
+
+ return StatusCodes.Status200OK;
+ }
+
+ public async Task RemoveRangeAsync(TimeSpan roomOutDate, TimeSpan roundOutDate)
+ {
+ var currentDate = DateTimeOffset.UtcNow.Ticks;
+
+ var rooms = await _repository.Rooms
+ .Include(room => room.Round)
+ .Where(room => room.CreationTimeTicks + roomOutDate.Ticks < currentDate && room.Round == null)
+ .ToArrayAsync();
+
+ var roomLength = rooms.Length;
+
+ var allRounds = await _repository.Rounds
+ .Where(round => round.FinishTimeTicks + roundOutDate.Ticks < currentDate)
+ .ToArrayAsync();
+
+ var roundsLength = allRounds.Length;
+
+ if (roomLength is not 0)
+ {
+ _repository.Rooms.RemoveRange(rooms);
+ }
+
+ if (roundsLength is not 0)
+ {
+ _repository.Rounds.RemoveRange(allRounds);
+ }
+
+ if (roundsLength is not 0 && roomLength is not 0)
+ {
+ await _repository.SaveChangesAsync();
+ }
+
+ return roomLength + roundsLength;
+ }
+
+ private async Task> GetPrivateAsync(string userId, string roomCode)
+ {
+ var room = await _repository.Rooms
+ .Include(room => room.Players)
+ .FirstOrDefaultAsync(room => room.Code == roomCode);
+
+ if (room is null)
+ {
+ return new CustomException(ExceptionTemplates.NotExists(nameof(Room)));
+ }
+
+ if (room.IsFull)
+ {
+ return new CustomException(ExceptionTemplates.RoomFull);
+ }
+
+ if (room.Players.Any(player => player.AccountId == userId))
+ {
+ return new CustomException(ExceptionTemplates.AlreadyInRoom);
+ }
+
+ return room;
+ }
+
+ private async Task> GetPublicAsync(string userId)
+ {
+ var room = await _repository.Rooms
+ .Include(room => room.Players)
+ .FirstOrDefaultAsync(room => !room.IsFull && !room.IsPrivate && room.Players.All(player => player.AccountId != userId));
+
+ if (room is null)
+ {
+ return new CustomException("There are no available free rooms");
+ }
+
+ return room;
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/RoundService.cs b/Server/Server.Bll/Services/RoundService.cs
new file mode 100644
index 0000000..398d519
--- /dev/null
+++ b/Server/Server.Bll/Services/RoundService.cs
@@ -0,0 +1,145 @@
+using Microsoft.EntityFrameworkCore;
+using RockPaperScissors.Common;
+using Server.Bll.Services.Interfaces;
+using Server.Data.Context;
+using Server.Data.Entities;
+using Server.Data.Extensions;
+using OneOf;
+using RockPaperScissors.Common.Enums;
+using PlayerState = Server.Data.Entities.PlayerState;
+
+namespace Server.Bll.Services;
+
+internal sealed class RoundService: IRoundService
+{
+ private readonly ServerContext _serverContext;
+
+ public RoundService(ServerContext serverContext)
+ {
+ _serverContext = serverContext ?? throw new ArgumentNullException(nameof(serverContext));
+ }
+
+ public async Task> MakeMoveAsync(string userId, string roundId, Move move)
+ {
+ var round = await _serverContext.Rounds
+ .Include(round => round.Players)
+ .Include(round => round.Room)
+ .FirstOrDefaultAsync(round => round.Id == roundId);
+
+ if (round is null)
+ {
+ return new CustomException($"Unable to find round with id '{roundId}'");
+ }
+
+ if (round.IsFinished)
+ {
+ return new CustomException($"Round has been finished.");
+ }
+
+ var updateTicks = DateTimeOffset.UtcNow.Ticks;
+ ProcessMoves(round, userId, move);
+
+ round.UpdateTicks = updateTicks;
+ round.Room.UpdateTicks = updateTicks;
+
+ _serverContext.Update(round);
+
+ await _serverContext.SaveChangesAsync();
+
+ return true;
+ }
+
+ public async Task GetUpdateTicksAsync(string roundId)
+ {
+ var round = await _serverContext.Rounds
+ .FirstOrDefaultAsync(rounds => rounds.Id== roundId);
+
+ return round?.UpdateTicks ?? -1;
+ }
+
+ private void ProcessMoves(Round round, string userId, Move move)
+ {
+ var players = round.Players;
+ var playingPlayer = players.FirstOrDefault(player => player.AccountId == userId);
+
+ if (playingPlayer is null)
+ {
+ return;
+ }
+
+ var otherPlayer = players.First(player => player.AccountId != userId);
+
+ if (otherPlayer.AccountId == SeedingExtension.BotId)
+ {
+ otherPlayer.Move = Random.Shared.Next(1, Enum.GetNames().Length);
+ }
+
+ playingPlayer.Move = (int)move;
+
+ if (otherPlayer.Move is (int)Move.None)
+ {
+ return;
+ }
+
+ var playingPlayerMove = (Move)playingPlayer.Move;
+ var otherPlayerMove = (Move)otherPlayer.Move;
+
+ playingPlayer.PlayerState = playingPlayerMove switch
+ {
+ Move.Paper => otherPlayerMove switch
+ {
+ Move.Rock => PlayerState.Win,
+ Move.Scissors => PlayerState.Lose,
+ Move.Paper => PlayerState.Draw,
+ _ => PlayerState.None,
+ },
+ Move.Rock => otherPlayerMove switch
+ {
+ Move.Rock => PlayerState.Draw,
+ Move.Scissors => PlayerState.Win,
+ Move.Paper => PlayerState.Lose,
+ _ => PlayerState.None,
+ },
+ Move.Scissors => otherPlayerMove switch
+ {
+ Move.Rock => PlayerState.Lose,
+ Move.Scissors => PlayerState.Draw,
+ Move.Paper => PlayerState.Win,
+ _ => PlayerState.None,
+ },
+ _ => PlayerState.None,
+ };
+
+ otherPlayer.PlayerState = playingPlayer.PlayerState switch
+ {
+ PlayerState.Win => PlayerState.Lose,
+ PlayerState.Lose => PlayerState.Win,
+ PlayerState.Draw => PlayerState.Draw,
+ _ => PlayerState.None,
+ };
+
+ if (playingPlayer.PlayerState is not PlayerState.None && otherPlayer.PlayerState is not PlayerState.None)
+ {
+ round.IsFinished = true;
+ }
+ }
+
+ public static Round Create(Room room)
+ {
+ var currentTime = DateTimeOffset.UtcNow.Ticks;
+ var newRound = new Round
+ {
+ Id = Guid.NewGuid().ToString(),
+ RoomId = room.Id,
+ Room = room,
+ Players = room.Players,
+ IsFinished = false,
+ StartTimeTicks = currentTime,
+ FinishTimeTicks = 0,
+ UpdateTicks = currentTime
+ };
+
+ return newRound;
+ }
+
+}
\ No newline at end of file
diff --git a/Server/Server.Bll/Services/StatisticsService.cs b/Server/Server.Bll/Services/StatisticsService.cs
new file mode 100644
index 0000000..455ed35
--- /dev/null
+++ b/Server/Server.Bll/Services/StatisticsService.cs
@@ -0,0 +1,44 @@
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+using RockPaperScissors.Common;
+using Server.Bll.Models;
+using Server.Bll.Services.Interfaces;
+using OneOf;
+using Server.Data.Context;
+
+namespace Server.Bll.Services;
+
+internal sealed class StatisticsService : IStatisticsService
+{
+ private readonly ServerContext _repository;
+
+ public StatisticsService(ServerContext repository)
+ {
+ _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+ }
+
+ public Task GetAllAsync()
+ {
+ return _repository
+ .StatisticsEnumerable
+ .Include(statistics => statistics.Account)
+ .OrderByDescending(statistics => statistics.Score)
+ .Take(10)
+ .ProjectToType()
+ .ToArrayAsync();
+ }
+
+ public async Task> GetAsync(string userId)
+ {
+ var statistics = await _repository.StatisticsEnumerable
+ .Include(stats => stats.Account)
+ .FirstOrDefaultAsync(statistics => statistics.Id.Equals(userId));
+
+ if (statistics is null)
+ {
+ return new CustomException($"Unable to get statistics for user \"{userId}\"");
+ }
+
+ return statistics.Adapt();
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Context/ServerContext.cs b/Server/Server.Data/Context/ServerContext.cs
new file mode 100644
index 0000000..295eaeb
--- /dev/null
+++ b/Server/Server.Data/Context/ServerContext.cs
@@ -0,0 +1,31 @@
+using Microsoft.EntityFrameworkCore;
+using Server.Data.Entities;
+using Server.Data.Extensions;
+
+namespace Server.Data.Context;
+
+public sealed class ServerContext : DbContext
+{
+ public DbSet Accounts { get; init; }
+
+ public DbSet Rooms { get; init; }
+
+
+ public DbSet Players { get; init; }
+
+ public DbSet Rounds { get; init; }
+
+ public DbSet StatisticsEnumerable { get; init; }
+
+ public ServerContext(DbContextOptions contextOptions)
+ :base(contextOptions) { }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasQueryFilter(round => !round.IsFinished);
+
+ modelBuilder.Entity()
+ .HasQueryFilter(statistics => statistics.AccountId != SeedingExtension.BotId);
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/Account.cs b/Server/Server.Data/Entities/Account.cs
new file mode 100644
index 0000000..d6467e9
--- /dev/null
+++ b/Server/Server.Data/Entities/Account.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Server.Data.Entities;
+
+[Table(nameof(Account))]
+public class Account
+{
+ ///
+ /// Id of account. Unique to everyone and similar with Statistics Id
+ ///
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public string Id { get; init; }
+
+ ///
+ /// Nick name of Account.
+ ///
+ public string Login { get; init; }
+
+ ///
+ /// Password of the Account
+ ///
+ public string Password { get; init; }
+
+ ///
+ /// Linked to this player statistics
+ ///
+ public virtual Statistics Statistics { get; set; }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/Player.cs b/Server/Server.Data/Entities/Player.cs
new file mode 100644
index 0000000..37911ae
--- /dev/null
+++ b/Server/Server.Data/Entities/Player.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Server.Data.Entities;
+
+[Table(nameof(Player))]
+public class Player
+{
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public string Id { get; init; }
+
+ [ForeignKey(nameof(Account))]
+ public string AccountId { get; init; }
+
+ public virtual Account Account { get; set; }
+
+ public bool IsReady { get; set; }
+
+ public int Move { get; set; }
+
+ public PlayerState PlayerState { get; set; }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/PlayerState.cs b/Server/Server.Data/Entities/PlayerState.cs
new file mode 100644
index 0000000..ed2d8a8
--- /dev/null
+++ b/Server/Server.Data/Entities/PlayerState.cs
@@ -0,0 +1,12 @@
+namespace Server.Data.Entities;
+
+public enum PlayerState
+{
+ None = 0,
+
+ Lose = 1,
+
+ Win = 2,
+
+ Draw = 3,
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/Room.cs b/Server/Server.Data/Entities/Room.cs
new file mode 100644
index 0000000..9323e9d
--- /dev/null
+++ b/Server/Server.Data/Entities/Room.cs
@@ -0,0 +1,50 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Server.Data.Entities;
+
+[Table(nameof(Room))]
+public class Room
+{
+ ///
+ /// Id of the room. Consists of 5 randomized chars
+ ///
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public string Id { get; init; }
+
+ ///
+ /// Special code to join a room
+ ///
+ public string Code { get; init; }
+
+ ///
+ /// Round, linked to this room
+ ///
+ public virtual Round Round { get; set; }
+
+ ///
+ /// .
+ ///
+ public virtual ICollection Players { get; set; }
+
+ ///
+ /// Flag is this room is private
+ ///
+ public bool IsPrivate { get; set; }
+
+ ///
+ /// Flag if room is full
+ ///
+ public bool IsFull { get; set; }
+
+ ///
+ /// Creation date. After 5 minutes of inactivity will be deleted
+ ///
+ public long CreationTimeTicks { get; set; }
+
+ ///
+ /// Last update time ticks.
+ ///
+ public long UpdateTicks { get; set; }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/Round.cs b/Server/Server.Data/Entities/Round.cs
new file mode 100644
index 0000000..495f2ea
--- /dev/null
+++ b/Server/Server.Data/Entities/Round.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Server.Data.Entities;
+
+[Table(nameof(Round))]
+public class Round
+{
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public string Id { get; init; }
+
+ [ForeignKey(nameof(Room))]
+ public string RoomId { get; set; }
+
+ public virtual Room Room { get; set; }
+
+ public virtual ICollection Players { get; set; }
+
+ public bool IsFinished { get; set; }
+
+ public bool IsDraw { get; init; }
+
+ public long StartTimeTicks { get; set; }
+
+ public long FinishTimeTicks { get; set; }
+
+ public long UpdateTicks { get; set; }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Entities/Statistics.cs b/Server/Server.Data/Entities/Statistics.cs
new file mode 100644
index 0000000..a40b109
--- /dev/null
+++ b/Server/Server.Data/Entities/Statistics.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Server.Data.Entities;
+
+[Table(nameof(Statistics))]
+public class Statistics
+{
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public string Id { get; init; }
+
+ [ForeignKey(nameof(Account))]
+ public string AccountId { get; set; }
+
+ public virtual Account Account { get; set; }
+
+ public int Wins { get; set; }
+
+ public int Loss { get; set; }
+
+ public int Draws { get; set; }
+
+ public double WinLossRatio { get; set; }
+
+ public TimeSpan TimeSpent { get; set; }
+
+ public int UsedRock { get; set; }
+
+ public int UsedPaper { get; set; }
+
+ public int UsedScissors { get; set; }
+
+ public int Score { get; set; }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Extensions/DatabaseExtension.cs b/Server/Server.Data/Extensions/DatabaseExtension.cs
new file mode 100644
index 0000000..9f8f3b1
--- /dev/null
+++ b/Server/Server.Data/Extensions/DatabaseExtension.cs
@@ -0,0 +1,21 @@
+using System.Reflection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Server.Data.Context;
+
+namespace Server.Data.Extensions;
+
+public static class DatabaseExtension
+{
+ private const string DatabaseConnection = "DatabaseConnection";
+
+ public static IServiceCollection AddDatabase(this IServiceCollection service, IConfiguration configuration)
+ {
+ return service.AddDbContext(
+ builder => builder.UseSqlite
+ (configuration.GetConnectionString(DatabaseConnection),
+ optionsBuilder => optionsBuilder.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName)),
+ ServiceLifetime.Transient);
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Extensions/SeedingExtension.cs b/Server/Server.Data/Extensions/SeedingExtension.cs
new file mode 100644
index 0000000..6756b96
--- /dev/null
+++ b/Server/Server.Data/Extensions/SeedingExtension.cs
@@ -0,0 +1,47 @@
+using Server.Data.Context;
+using Server.Data.Entities;
+
+namespace Server.Data.Extensions;
+
+public static class SeedingExtension
+{
+ public static readonly Player BotPlayer = new()
+ {
+ Id = Guid.NewGuid().ToString(),
+ AccountId = BotId,
+ IsReady = true
+ };
+
+ public const string BotId = "bot";
+
+ public static async Task EnsureBotCreated(this ServerContext context)
+ {
+ var bot = await context.Accounts.FindAsync(BotId);
+ if (bot is not null)
+ {
+ return;
+ }
+
+ var botAccount = new Account
+ {
+ Id = BotId,
+ Login = BotId,
+ Password = Guid.NewGuid().ToString()
+ };
+
+ context.Add(botAccount);
+
+ BotPlayer.Account = botAccount;
+
+ context.Add(new Statistics
+ {
+ Id = BotId,
+ Account = botAccount,
+ AccountId = BotId
+ });
+
+ context.Add(BotPlayer);
+
+ await context.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs b/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs
new file mode 100644
index 0000000..5aa848d
--- /dev/null
+++ b/Server/Server.Data/Migrations/20221023190847_InitialMigration.Designer.cs
@@ -0,0 +1,225 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Server.Data.Context;
+
+#nullable disable
+
+namespace Server.Data.Migrations
+{
+ [DbContext(typeof(ServerContext))]
+ [Migration("20221023190847_InitialMigration")]
+ partial class InitialMigration
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
+
+ modelBuilder.Entity("Server.Data.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("Login")
+ .HasColumnType("TEXT");
+
+ b.Property("Password")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Account");
+ });
+
+ modelBuilder.Entity("Server.Data.Entities.Player", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccountId")
+ .HasColumnType("TEXT");
+
+ b.Property("IsReady")
+ .HasColumnType("INTEGER");
+
+ b.Property("Move")
+ .HasColumnType("INTEGER");
+
+ b.Property("PlayerState")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoomId")
+ .HasColumnType("TEXT");
+
+ b.Property("RoundId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("RoomId");
+
+ b.HasIndex("RoundId");
+
+ b.ToTable("Player");
+ });
+
+ modelBuilder.Entity("Server.Data.Entities.Room", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .HasColumnType("TEXT");
+
+ b.Property("CreationTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsFull")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsPrivate")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdateTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Room");
+ });
+
+ modelBuilder.Entity("Server.Data.Entities.Round", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("FinishTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsDraw")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsFinished")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoomId")
+ .HasColumnType("TEXT");
+
+ b.Property("StartTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdateTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId")
+ .IsUnique();
+
+ b.ToTable("Round");
+ });
+
+ modelBuilder.Entity("Server.Data.Entities.Statistics", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccountId")
+ .HasColumnType("TEXT");
+
+ b.Property("Draws")
+ .HasColumnType("INTEGER");
+
+ b.Property("Loss")
+ .HasColumnType("INTEGER");
+
+ b.Property("Score")
+ .HasColumnType("INTEGER");
+
+ b.Property