From d76e4f7a8bbbbb0a8eedc665228a9f7592c4310c Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 13 Feb 2021 19:14:43 -0600 Subject: [PATCH] More organized communication strategy. --- .../Gameboard.ShogiUI.Sockets.csproj | 6 +- .../ClientActionHandlers/CreateGameHandler.cs | 9 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 11 +-- .../ClientActionHandlers/JoinGameHandler.cs | 16 +--- .../ClientActionHandlers/ListGamesHandler.cs | 19 +--- .../ClientActionHandlers/LoadGameHandler.cs | 16 ++-- .../ClientActionHandlers/MoveHandler.cs | 44 +++------ .../Managers/SocketCommunicationManager.cs | 96 +++++++++---------- .../Managers/SocketConnectionManager.cs | 2 +- .../Managers/Utility/Mapper.cs | 46 ++++----- Gameboard.ShogiUI.Sockets/Models/Session.cs | 68 +++++++++++++ .../Repositories/GameboardRepository.cs | 34 ++++--- .../Utility/AuthenticatedHttpClient.cs | 18 ++++ 13 files changed, 212 insertions(+), 173 deletions(-) create mode 100644 Gameboard.ShogiUI.Sockets/Models/Session.cs diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index aa056ee..038f52e 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -5,7 +5,7 @@ - + @@ -18,9 +18,5 @@ - - - - diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index b7e3773..4b72412 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -28,7 +28,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", new[] { json }); var request = JsonConvert.DeserializeObject(json); var postSessionResponse = await repository.PostSession(new PostSession { @@ -43,7 +42,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers Game = new Game { GameName = postSessionResponse.SessionName, - Players = new string[] { userName } + Players = new[] { userName } } }; @@ -52,15 +51,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers response.Error = "Game already exists."; } - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); if (request.IsPrivate) { + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); await socket.SendTextAsync(serialized); } else { - await communicationManager.BroadcastToAll(serialized); + await communicationManager.BroadcastToAll(response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 102564a..c4f2869 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -28,7 +28,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", new[] { json }); var request = JsonConvert.DeserializeObject(json); var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession { @@ -46,9 +45,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, GameName = gameName }; - var serialized = JsonConvert.SerializeObject(response); - await communicationManager.BroadcastToGame(gameName, serialized); - communicationManager.SubscribeToGame(socket, gameName, userName); + // At this time, userName hasn't subscribed and won't receive this broadcasted messages. + await communicationManager.BroadcastToGame(gameName, response); // But the player joining sees the JoinByCode occur. response = new JoinGameResponse(ClientAction.JoinByCode) @@ -56,7 +54,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, GameName = gameName }; - serialized = JsonConvert.SerializeObject(response); + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); await socket.SendTextAsync(serialized); } else @@ -67,7 +66,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers Error = "Error joining game." }; var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); await socket.SendTextAsync(serialized); } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 0a45cca..96e1ed7 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -2,7 +2,6 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Net.WebSockets; using System.Threading.Tasks; @@ -11,31 +10,28 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class JoinGameHandler : IActionHandler { - private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; public JoinGameHandler( - ILogger logger, ISocketCommunicationManager communicationManager, IGameboardRepository gameboardRepository) { - this.logger = logger; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; } public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", new[] { json }); var request = JsonConvert.DeserializeObject(json); var response = new JoinGameResponse(ClientAction.JoinGame) { PlayerName = userName }; - var joinGameResponse = await gameboardRepository.PutJoinPublicSession(request.GameName, new PutJoinPublicSession + var joinGameResponse = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession { - PlayerName = userName + PlayerName = userName, + SessionName = request.GameName }); if (joinGameResponse.JoinSucceeded) @@ -44,11 +40,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } else { - response.Error = "Game is full or code is incorrect."; + response.Error = "Game is full."; } - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); - await communicationManager.BroadcastToAll(serialized); + await communicationManager.BroadcastToAll(response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 72050fc..43431a0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,10 +1,9 @@ using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; using System.Linq; using System.Net.WebSockets; using System.Threading.Tasks; @@ -13,20 +12,16 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class ListGamesHandler : IActionHandler { - private readonly ILogger logger; private readonly IGameboardRepository repository; public ListGamesHandler( - ILogger logger, IGameboardRepository repository) { - this.logger = logger; this.repository = repository; } public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", new[] { json }); var request = JsonConvert.DeserializeObject(json); var getGamesResponse = string.IsNullOrWhiteSpace(userName) ? await repository.GetGames() @@ -34,20 +29,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers var games = getGamesResponse.Sessions .OrderBy(s => s.Player1 == userName || s.Player2 == userName) - .Select(s => - { - var players = new[] { s.Player1, s.Player2 } - .Where(p => !string.IsNullOrWhiteSpace(p)) - .ToArray(); - return new Game { GameName = s.Name, Players = players }; - }); + .Select(s => new Session(s).ToServiceModel()); // yuck + var response = new ListGamesResponse(ClientAction.ListGames) { - Games = games ?? Array.Empty() + Games = games }; var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); await socket.SendTextAsync(serialized); } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 97e7c32..380e938 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,5 +1,6 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -29,33 +30,28 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", json); var request = JsonConvert.DeserializeObject(json); - var response = new LoadGameResponse(ClientAction.LoadGame); var getGameResponse = await gameboardRepository.GetGame(request.GameName); var getMovesResponse = await gameboardRepository.GetMoves(request.GameName); + var response = new LoadGameResponse(ClientAction.LoadGame); if (getGameResponse == null || getMovesResponse == null) { response.Error = $"Could not find game."; } else { - var session = getGameResponse.Session; - var players = new[] { session.Player1, session.Player2 } - .Where(p => !string.IsNullOrWhiteSpace(p)) - .ToArray(); - response.Game = new Game { GameName = session.Name, Players = players }; + var session = new Session(getGameResponse.Session); + communicationManager.SubscribeToGame(socket, session, userName); + response.Game = session.ToServiceModel(); response.Moves = userName.Equals(session.Player1) ? getMovesResponse.Moves.Select(_ => Mapper.Map(_)) : getMovesResponse.Moves.Select(_ => Move.ConvertPerspective(Mapper.Map(_))); - - communicationManager.SubscribeToGame(socket, session.Name, userName); } var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", serialized); + logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); await socket.SendTextAsync(serialized); } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index bbce0c5..ee3cd05 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -4,7 +4,6 @@ using Gameboard.ShogiUI.Sockets.Managers.Utility; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Net.WebSockets; using System.Threading.Tasks; @@ -13,26 +12,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class MoveHandler : IActionHandler { - private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; public MoveHandler( - ILogger logger, ISocketCommunicationManager communicationManager, IGameboardRepository gameboardRepository) { - this.logger = logger; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; } public async Task Handle(WebSocket socket, string json, string userName) { - logger.LogInformation("Socket Request \n{0}\n", new[] { json }); var request = JsonConvert.DeserializeObject(json); // Basic move validation - var move = request.Move; - if (move.To.Equals(move.From)) + if (request.Move.To.Equals(request.Move.From)) { var serialized = JsonConvert.SerializeObject( new ErrorResponse(ClientAction.Move) @@ -43,33 +37,25 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers return; } - var getSessionResponse = await gameboardRepository.GetGame(request.GameName); - var isPlayer1 = userName == getSessionResponse.Session.Player1; - if (!isPlayer1) - { - // Convert the move coords to player1 perspective. - move = Move.ConvertPerspective(move); - } - + var session = (await gameboardRepository.GetGame(request.GameName)).Session; + var isPlayer2 = userName == session.Player2; + // Shogi.Api expects the move coordinates from the perspective of player 1. + var move = isPlayer2 ? Move.ConvertPerspective(request.Move) : request.Move; await gameboardRepository.PostMove(request.GameName, new PostMove(Mapper.Map(move))); - var response = new MoveResponse(ClientAction.Move) + var responseForPlayer1 = new MoveResponse(ClientAction.Move) { GameName = request.GameName, - PlayerName = userName + PlayerName = userName, + Move = isPlayer2 ? Move.ConvertPerspective(request.Move) : request.Move }; - await communicationManager.BroadcastToGame( - request.GameName, - (playerName, sslStream) => - { - response.Move = playerName.Equals(userName) - ? request.Move - : Move.ConvertPerspective(request.Move); - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); - return serialized; - } - ); + var responseForPlayer2 = new MoveResponse(ClientAction.Move) + { + GameName = request.GameName, + PlayerName = userName, + Move = isPlayer2 ? request.Move : Move.ConvertPerspective(request.Move) + }; + await communicationManager.BroadcastToGame(session.Name, responseForPlayer1, responseForPlayer2); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 8e68a7a..6923621 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -1,6 +1,8 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -16,10 +18,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers public interface ISocketCommunicationManager { Task CommunicateWith(WebSocket w, string s); - Task BroadcastToAll(string msg); - Task BroadcastToGame(string gameName, Func msgBuilder); - Task BroadcastToGame(string gameName, string msg); - void SubscribeToGame(WebSocket socket, string gameName, string playerName); + Task BroadcastToAll(IResponse response); + Task BroadcastToGame(string gameName, IResponse response); + Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + void SubscribeToGame(WebSocket socket, Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromGame(string gameName, string playerName); @@ -27,8 +29,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers public class SocketCommunicationManager : ISocketCommunicationManager { + /// Dictionary key is player name. private readonly ConcurrentDictionary connections; - private readonly ConcurrentDictionary> gameSeats; + /// Dictionary key is game name. + private readonly ConcurrentDictionary sessions; private readonly ILogger logger; private readonly ActionHandlerResolver handlerResolver; @@ -39,7 +43,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers this.logger = logger; this.handlerResolver = handlerResolver; connections = new ConcurrentDictionary(); - gameSeats = new ConcurrentDictionary>(); + sessions = new ConcurrentDictionary(); } public async Task CommunicateWith(WebSocket socket, string userName) @@ -52,7 +56,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers { var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; - + logger.LogInformation("Request \n{0}\n", message); var request = JsonConvert.DeserializeObject(message); if (!Enum.IsDefined(typeof(ClientAction), request.Action)) { @@ -68,7 +72,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers { logger.LogError(ex.Message); } - catch(WebSocketException ex) + catch (WebSocketException ex) { logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); logger.LogInformation("Probably tried writing to a closed socket."); @@ -80,93 +84,79 @@ namespace Gameboard.ShogiUI.Sockets.Managers public void SubscribeToBroadcast(WebSocket socket, string playerName) { - logger.LogInformation("Subscribing [{0}] to broadcast", playerName); connections.TryAdd(playerName, socket); } public void UnsubscribeFromBroadcastAndGames(string playerName) { - logger.LogInformation("Unsubscribing [{0}] from broadcast", playerName); connections.TryRemove(playerName, out _); - foreach (var game in gameSeats) + foreach (var kvp in sessions) { - game.Value.Remove(playerName); + var sessionName = kvp.Key; + UnsubscribeFromGame(sessionName, playerName); } } /// /// Unsubscribes the player from their current game, then subscribes to the new game. /// - public void SubscribeToGame(WebSocket socket, string gameName, string playerName) + public void SubscribeToGame(WebSocket socket, Session session, string playerName) { // Unsubscribe from any other games - foreach (var kvp in gameSeats) + foreach (var kvp in sessions) { var gameNameKey = kvp.Key; UnsubscribeFromGame(gameNameKey, playerName); } // Subscribe - logger.LogInformation("Subscribing player [{0}] to game [{1}]", playerName, gameName); - var addSuccess = gameSeats.TryAdd(gameName, new List { playerName }); - if (!addSuccess && !gameSeats[gameName].Contains(playerName)) - { - gameSeats[gameName].Add(playerName); - } + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); } public void UnsubscribeFromGame(string gameName, string playerName) { - if (gameSeats.ContainsKey(gameName)) + if (sessions.TryGetValue(gameName, out var s)) { - logger.LogInformation("Unsubscribing player [{0}] from game [{1}]", playerName, gameName); - gameSeats[gameName].Remove(playerName); - if (gameSeats[gameName].Count == 0) gameSeats.TryRemove(gameName, out _); + s.Subscriptions.TryRemove(playerName, out _); + if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); } } - public async Task BroadcastToAll(string msg) + public Task BroadcastToAll(IResponse response) { - var tasks = connections.Select(kvp => + var message = JsonConvert.SerializeObject(response); + logger.LogInformation($"Broadcasting\n{0}", message); + var tasks = new List(connections.Count); + foreach (var kvp in connections) { - var player = kvp.Key; var socket = kvp.Value; - logger.LogInformation("Broadcasting to player [{0}] \n{1}\n", new[] { player, msg }); - return socket.SendTextAsync(msg); - }); - await Task.WhenAll(tasks); + tasks.Add(socket.SendTextAsync(message)); + } + return Task.WhenAll(tasks); } - public async Task BroadcastToGame(string gameName, string msg) + public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) { - if (gameSeats.ContainsKey(gameName)) + if (sessions.TryGetValue(gameName, out var session)) { - var tasks = gameSeats[gameName] - .Select(playerName => - { - logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg); - return connections[playerName]; - }) - .Where(stream => stream != null) - .Select(socket => socket.SendTextAsync(msg)); - await Task.WhenAll(tasks); + var serialized1 = JsonConvert.SerializeObject(forPlayer1); + var serialized2 = JsonConvert.SerializeObject(forPlayer2); + return Task.WhenAll( + session.SendToPlayer1(serialized1), + session.SendToPlayer2(serialized2)); } + return Task.CompletedTask; } - public async Task BroadcastToGame(string gameName, Func msgBuilder) + public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) { - if (gameSeats.ContainsKey(gameName)) + if (sessions.TryGetValue(gameName, out var session)) { - var tasks = gameSeats[gameName] - .Select(playerName => - { - var socket = connections[playerName]; - var msg = msgBuilder(playerName, socket); - logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg); - return socket.SendTextAsync(msg); - }); - await Task.WhenAll(tasks); + var serialized = JsonConvert.SerializeObject(messageForAllPlayers); + return session.Broadcast(serialized); } + return Task.CompletedTask; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 0e5d729..05374e3 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -30,7 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers var oneTimeToken = context.Request.Query["token"][0]; var tokenAsGuid = Guid.Parse(oneTimeToken); var userName = tokenManager.GetUsername(tokenAsGuid); - if (!string.IsNullOrEmpty(userName)) + if (userName != null) { var socket = await context.WebSockets.AcceptWebSocketAsync(); await communicationManager.CommunicateWith(socket, userName); diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs index 9d84fac..7302fe5 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs @@ -1,38 +1,38 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.FSharp.Core; -using GameboardTypes = Gameboard.Shogi.Api.ServiceModels.Types; +using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Managers.Utility { public static class Mapper { - public static GameboardTypes.Move Map(Move source) + public static ShogiApi.Move Map(Move source) { var from = source.From; var to = source.To; - FSharpOption pieceFromCaptured = source.PieceFromCaptured switch + FSharpOption pieceFromCaptured = source.PieceFromCaptured switch { - "B" => new FSharpOption(GameboardTypes.PieceName.Bishop), - "G" => new FSharpOption(GameboardTypes.PieceName.GoldenGeneral), - "K" => new FSharpOption(GameboardTypes.PieceName.King), - "k" => new FSharpOption(GameboardTypes.PieceName.Knight), - "L" => new FSharpOption(GameboardTypes.PieceName.Lance), - "P" => new FSharpOption(GameboardTypes.PieceName.Pawn), - "R" => new FSharpOption(GameboardTypes.PieceName.Rook), - "S" => new FSharpOption(GameboardTypes.PieceName.SilverGeneral), + "B" => new FSharpOption(ShogiApi.WhichPieceName.Bishop), + "G" => new FSharpOption(ShogiApi.WhichPieceName.GoldenGeneral), + "K" => new FSharpOption(ShogiApi.WhichPieceName.King), + "k" => new FSharpOption(ShogiApi.WhichPieceName.Knight), + "L" => new FSharpOption(ShogiApi.WhichPieceName.Lance), + "P" => new FSharpOption(ShogiApi.WhichPieceName.Pawn), + "R" => new FSharpOption(ShogiApi.WhichPieceName.Rook), + "S" => new FSharpOption(ShogiApi.WhichPieceName.SilverGeneral), _ => null }; - var target = new GameboardTypes.Move + var target = new ShogiApi.Move { - Origin = new GameboardTypes.BoardLocation { X = from.X, Y = from.Y }, - Destination = new GameboardTypes.BoardLocation { X = to.X, Y = to.Y }, + Origin = new ShogiApi.BoardLocation { X = from.X, Y = from.Y }, + Destination = new ShogiApi.BoardLocation { X = to.X, Y = to.Y }, IsPromotion = source.IsPromotion, PieceFromCaptured = pieceFromCaptured }; return target; } - public static Move Map(GameboardTypes.Move source) + public static Move Map(ShogiApi.Move source) { var origin = source.Origin; var destination = source.Destination; @@ -41,14 +41,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.Utility { pieceFromCaptured = source.PieceFromCaptured.Value switch { - GameboardTypes.PieceName.Bishop => "B", - GameboardTypes.PieceName.GoldenGeneral => "G", - GameboardTypes.PieceName.King => "K", - GameboardTypes.PieceName.Knight => "k", - GameboardTypes.PieceName.Lance => "L", - GameboardTypes.PieceName.Pawn => "P", - GameboardTypes.PieceName.Rook => "R", - GameboardTypes.PieceName.SilverGeneral => "S", + ShogiApi.WhichPieceName.Bishop => "B", + ShogiApi.WhichPieceName.GoldenGeneral => "G", + ShogiApi.WhichPieceName.King => "K", + ShogiApi.WhichPieceName.Knight => "k", + ShogiApi.WhichPieceName.Lance => "L", + ShogiApi.WhichPieceName.Pawn => "P", + ShogiApi.WhichPieceName.Rook => "R", + ShogiApi.WhichPieceName.SilverGeneral => "S", _ => "" }; } diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs new file mode 100644 index 0000000..1e824f7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -0,0 +1,68 @@ +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class Session + { + public string Name { get; } + public string Player1 { get; } + public string Player2 { get; } + + public ConcurrentDictionary Subscriptions { get; } + + public Session(Shogi.Api.ServiceModels.Types.Session session) + { + Name = session.Name; + Player1 = session.Player1; + Player2 = session.Player2; + Subscriptions = new ConcurrentDictionary(); + } + + public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); + + public Task Broadcast(string message) + { + var tasks = new List(Subscriptions.Count); + foreach (var kvp in Subscriptions) + { + var socket = kvp.Value; + tasks.Add(socket.SendTextAsync(message)); + } + return Task.WhenAll(tasks); + } + + public Task SendToPlayer1(string message) + { + if (Subscriptions.TryGetValue(Player1, out var socket)) + { + return socket.SendTextAsync(message); + } + return Task.CompletedTask; + } + + public Task SendToPlayer2(string message) + { + if (Subscriptions.TryGetValue(Player2, out var socket)) + { + return socket.SendTextAsync(message); + } + return Task.CompletedTask; + } + + public Game ToServiceModel() + { + var players = new List(2) { Player1 }; + if (!string.IsNullOrWhiteSpace(Player2)) players.Add(Player2); + return new Game + { + GameName = Name, + Players = players.ToArray() + }; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 96c3e44..25583c9 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -1,10 +1,10 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; +using Gameboard.ShogiUI.Sockets.Repositories.Utility; using Newtonsoft.Json; using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; namespace Gameboard.ShogiUI.Sockets.Repositories { @@ -17,7 +17,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories Task GetMoves(string gameName); Task PostSession(PostSession request); Task PostJoinPrivateSession(PostJoinPrivateSession request); - Task PutJoinPublicSession(string gameName, PutJoinPublicSession request); + Task PutJoinPublicSession(PutJoinPublicSession request); Task PostMove(string gameName, PostMove request); Task PostJoinCode(string gameName, string userName); Task GetPlayer(string userName); @@ -26,6 +26,11 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public class GameboardRepository : IGameboardRepository { + private const string GetSessionsRoute = "Sessions"; + private const string PostSessionRoute = "Session"; + private const string JoinSessionRoute = "Session/Join"; + private const string PlayerRoute = "Player"; + private const string MediaType = "application/json"; private readonly IAuthenticatedHttpClient client; public GameboardRepository(IAuthenticatedHttpClient client) { @@ -34,7 +39,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task GetGames() { - var response = await client.GetAsync("Sessions"); + var response = await client.GetAsync(GetSessionsRoute); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(json); } @@ -63,25 +68,24 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task PostSession(PostSession request) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - var response = await client.PostAsync("Session", content); + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + var response = await client.PostAsync(PostSessionRoute, content); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(json); } - public async Task PutJoinPublicSession(string gameName, PutJoinPublicSession request) + public async Task PutJoinPublicSession(PutJoinPublicSession request) { - var uri = $"Session/{gameName}/Join"; - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - var response = await client.PostAsync(Uri.EscapeUriString(uri), content); + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + var response = await client.PutAsync(JoinSessionRoute, content); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(json); } public async Task PostJoinPrivateSession(PostJoinPrivateSession request) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - var response = await client.PostAsync("Session/Join", content); + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + var response = await client.PostAsync(JoinSessionRoute, content); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(json); } @@ -97,7 +101,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task PostMove(string gameName, PostMove request) { var uri = $"Session/{gameName}/Move"; - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); await client.PostAsync(Uri.EscapeUriString(uri), content); } @@ -105,7 +109,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { var uri = $"JoinCode/{gameName}"; var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); - var content = new StringContent(serialized, Encoding.UTF8, "application/json"); + var content = new StringContent(serialized, Encoding.UTF8, MediaType); var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(json); } @@ -120,8 +124,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task PostPlayer(PostPlayer request) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - return await client.PostAsync("Player", content); + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + return await client.PostAsync(PlayerRoute, content); } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs index a25c7d7..b9b7ba0 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs @@ -13,6 +13,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility Task DeleteAsync(string requestUri); Task GetAsync(string requestUri); Task PostAsync(string requestUri, HttpContent content); + Task PutAsync(string requestUri, HttpContent content); } public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient @@ -89,6 +90,23 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility await response.Content.ReadAsStringAsync()); return response; } + public async new Task PutAsync(string requestUri, HttpContent content) + { + var response = await base.PutAsync(requestUri, content); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + await RefreshBearerToken(); + response = await base.PutAsync(requestUri, content); + } + logger.LogInformation( + "Repository PUT to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n", + BaseAddress, + requestUri, + response.StatusCode, + await content.ReadAsStringAsync(), + await response.Content.ReadAsStringAsync()); + return response; + } public async new Task DeleteAsync(string requestUri) { var response = await base.DeleteAsync(requestUri);