From d76e4f7a8bbbbb0a8eedc665228a9f7592c4310c Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 13 Feb 2021 19:14:43 -0600 Subject: [PATCH 01/27] 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); From 8d79c756165026f4fefbf05ff4b44461c5aea4ca Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 19 Feb 2021 20:19:11 -0600 Subject: [PATCH 02/27] Better communication --- .../Socket/Types/Coords.cs | 8 ---- .../Socket/Types/Move.cs | 39 ++++-------------- ...Gameboard.ShogiUI.Sockets.UnitTests.csproj | 19 +++++++++ .../Models/CoordsModelShould.cs | 27 ++++++++++++ Gameboard.ShogiUI.Sockets.sln | 11 +++++ .../Managers/BoardManager.cs | 6 +++ .../ClientActionHandlers/LoadGameHandler.cs | 10 ++--- .../ClientActionHandlers/MoveHandler.cs | 26 ++++-------- .../Managers/Utility/Mapper.cs | 6 +-- Gameboard.ShogiUI.Sockets/Models/Coords.cs | 41 +++++++++++++++++++ Gameboard.ShogiUI.Sockets/Models/Move.cs | 27 ++++++++++++ 11 files changed, 156 insertions(+), 64 deletions(-) delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs create mode 100644 Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj create mode 100644 Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/Coords.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/Move.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs deleted file mode 100644 index d64dae4..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types -{ - public class Coords - { - public int X { get; set; } - public int Y { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs index f815b00..3ad3719 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs @@ -1,33 +1,12 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { - public class Move - { - public string PieceFromCaptured { get; set; } - public Coords From { get; set; } - public Coords To { get; set; } - public bool IsPromotion { get; set; } - - /// - /// Toggles perspective of this move. (ie from player 1 to player 2) - /// - public static Move ConvertPerspective(Move m) - { - var convertedMove = new Move - { - To = new Coords - { - X = 8 - m.To.X, - Y = 8 - m.To.Y - }, - From = new Coords - { - X = 8 - m.From.X, - Y = 8 - m.From.Y - }, - IsPromotion = m.IsPromotion, - PieceFromCaptured = m.PieceFromCaptured - }; - return convertedMove; - } - } + public class Move + { + public string PieceFromCaptured { get; set; } + /// Board position notation, like A3 or G1 + public string From { get; set; } + /// Board position notation, like A3 or G1 + public string To { get; set; } + public bool IsPromotion { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj b/Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj new file mode 100644 index 0000000..02bde77 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs b/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs new file mode 100644 index 0000000..d0c1961 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using Gameboard.ShogiUI.Sockets.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Gameboard.ShogiUI.Sockets.UnitTests.Models +{ + [TestClass] + public class CoordsModelShould + { + [TestMethod] + public void ConvertToNotation() + { + var letters = "ABCDEFGHI"; + + for (var x = 0; x < 8; x++) // file + { + for (var y = 0; y < 8; y++) // rank + { + var move = new Coords(x, y); + var actual = move.ToBoardNotation(); + var expected = $"{letters[x]}{y + 1}"; + actual.Should().Be(expected); + } + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 85cdd75..adfe2f0 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -7,6 +7,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Sockets.UnitTests", "Gameboard.ShogiUI.Sockets.UnitTests\Gameboard.ShogiUI.Sockets.UnitTests.csproj", "{8D753AD0-0985-415C-80B3-CCADF3AE1DF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,10 +25,17 @@ Global {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU + {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8D753AD0-0985-415C-80B3-CCADF3AE1DF9} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} EndGlobalSection diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs new file mode 100644 index 0000000..49f21a4 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -0,0 +1,6 @@ +namespace Gameboard.ShogiUI.Sockets.Managers +{ + public class BoardManager + { + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 380e938..b0fac4f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -41,13 +41,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } else { - var session = new Session(getGameResponse.Session); - communicationManager.SubscribeToGame(socket, session, userName); + var sessionModel = new Session(getGameResponse.Session); + communicationManager.SubscribeToGame(socket, sessionModel, userName); - response.Game = session.ToServiceModel(); - response.Moves = userName.Equals(session.Player1) - ? getMovesResponse.Moves.Select(_ => Mapper.Map(_)) - : getMovesResponse.Moves.Select(_ => Move.ConvertPerspective(Mapper.Map(_))); + response.Game = sessionModel.ToServiceModel(); + response.Moves = getMovesResponse.Moves.Select(_ => Mapper.Map(_).ToServiceModel()); } var serialized = JsonConvert.SerializeObject(response); diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index ee3cd05..fdc535b 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -2,11 +2,11 @@ using Gameboard.ShogiUI.Sockets.Extensions; 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 Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Newtonsoft.Json; using System.Net.WebSockets; using System.Threading.Tasks; +using Gameboard.ShogiUI.Sockets.Models; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { @@ -24,12 +24,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(WebSocket socket, string json, string userName) { - var request = JsonConvert.DeserializeObject(json); + var request = JsonConvert.DeserializeObject(json); // Basic move validation if (request.Move.To.Equals(request.Move.From)) { var serialized = JsonConvert.SerializeObject( - new ErrorResponse(ClientAction.Move) + new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) { Error = "Error: moving piece from tile to the same tile." }); @@ -37,25 +37,17 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers return; } + var moveModel = new Move(request.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))); + await gameboardRepository.PostMove(request.GameName, new PostMove(Mapper.Map(moveModel))); - var responseForPlayer1 = new MoveResponse(ClientAction.Move) + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) { GameName = request.GameName, PlayerName = userName, - Move = isPlayer2 ? Move.ConvertPerspective(request.Move) : request.Move + Move = moveModel.ToServiceModel() }; - 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); + await communicationManager.BroadcastToGame(session.Name, response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs index 7302fe5..ea59d0e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.Models; using Microsoft.FSharp.Core; using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; @@ -55,8 +55,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.Utility var target = new Move { - From = new Coords { X = origin.X, Y = origin.Y }, - To = new Coords { X = destination.X, Y = destination.Y }, + From = new Coords(origin.X, origin.Y), + To = new Coords(destination.X, destination.Y), IsPromotion = source.IsPromotion, PieceFromCaptured = pieceFromCaptured }; diff --git a/Gameboard.ShogiUI.Sockets/Models/Coords.cs b/Gameboard.ShogiUI.Sockets/Models/Coords.cs new file mode 100644 index 0000000..2104cde --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/Coords.cs @@ -0,0 +1,41 @@ +using System; +using System.Text.RegularExpressions; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class Coords + { + private const string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private const char A = 'A'; + public int X { get; } + public int Y { get; } + public Coords(int x, int y) + { + X = x; + Y = y; + } + + public string ToBoardNotation() + { + var file = (char)(X + A); + var rank = Y + 1; + return $"{file}{rank}"; + } + + public static Coords FromBoardNotation(string notation) + { + if (string.IsNullOrEmpty(notation)) + { + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Coords(file - A, rank); + } + throw new ArgumentException("Board notation not recognized."); // TODO: Move this error handling to the service layer. + } + return new Coords(-1, -1); // Temporarily this is how I tell Gameboard.API that a piece came from the hand. + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs new file mode 100644 index 0000000..932285a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -0,0 +1,27 @@ +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class Move + { + public string PieceFromCaptured { get; set; } + public Coords From { get; set; } + public Coords To { get; set; } + public bool IsPromotion { get; set; } + + public Move() { } + public Move(ServiceModels.Socket.Types.Move move) + { + From = Coords.FromBoardNotation(move.From); + To = Coords.FromBoardNotation(move.To); + PieceFromCaptured = move.PieceFromCaptured; + IsPromotion = move.IsPromotion; + } + + public ServiceModels.Socket.Types.Move ToServiceModel() => new ServiceModels.Socket.Types.Move + { + From = From.ToBoardNotation(), + IsPromotion = IsPromotion, + PieceFromCaptured = PieceFromCaptured, + To = To.ToBoardNotation() + }; + } +} From f644795cd39b83d85d2a8f75800235c5e0a079bc Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 23 Feb 2021 18:03:23 -0600 Subject: [PATCH 03/27] checkpoint --- .gitignore | 1 + Benchmarking/Benchmarking.csproj | 16 + Benchmarking/Benchmarks.cs | 58 ++ Gameboard.ShogiUI.BoardState/Array2D.cs | 29 + Gameboard.ShogiUI.BoardState/BoardVector.cs | 62 +++ Gameboard.ShogiUI.BoardState/Extensions.cs | 19 + .../Gameboard.ShogiUI.BoardState.csproj | 7 + Gameboard.ShogiUI.BoardState/Move.cs | 10 + Gameboard.ShogiUI.BoardState/Piece.cs | 53 ++ Gameboard.ShogiUI.BoardState/Position.cs | 35 ++ Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 522 ++++++++++++++++++ Gameboard.ShogiUI.BoardState/WhichPiece.cs | 14 + Gameboard.ShogiUI.BoardState/WhichPlayer.cs | 8 + .../Socket/Messages/ListGames.cs | 2 +- .../Socket/Messages/LoadGame.cs | 2 +- .../Socket/Types/BoardState.cs | 11 + .../Socket/Types/Game.cs | 17 +- .../Socket/Types/Piece.cs | 14 + .../Socket/Types/WhichPiece.cs | 14 + Gameboard.ShogiUI.Sockets.sln | 24 +- .../Gameboard.ShogiUI.Sockets.csproj | 1 + .../Gameboard.ShogiUI.Sockets.csproj.user | 12 - .../Managers/BoardManager.cs | 33 +- .../ClientActionHandlers/CreateGameHandler.cs | 12 +- .../ClientActionHandlers/IActionHandler.cs | 2 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 25 +- .../ClientActionHandlers/JoinGameHandler.cs | 16 +- .../ClientActionHandlers/ListGamesHandler.cs | 16 +- .../ClientActionHandlers/LoadGameHandler.cs | 46 +- .../ClientActionHandlers/MoveHandler.cs | 36 +- .../Managers/SocketCommunicationManager.cs | 29 +- .../Managers/Utility/Mapper.cs | 67 --- Gameboard.ShogiUI.Sockets/Models/Move.cs | 64 ++- Gameboard.ShogiUI.Sockets/Startup.cs | 1 + .../BoardState/BoardVectorShould.cs | 26 + .../BoardState/ShogiBoardShould.cs | 322 +++++++++++ .../Gameboard.ShogiUI.UnitTests.csproj | 0 .../Sockets}/CoordsModelShould.cs | 2 +- 38 files changed, 1451 insertions(+), 177 deletions(-) create mode 100644 Benchmarking/Benchmarking.csproj create mode 100644 Benchmarking/Benchmarks.cs create mode 100644 Gameboard.ShogiUI.BoardState/Array2D.cs create mode 100644 Gameboard.ShogiUI.BoardState/BoardVector.cs create mode 100644 Gameboard.ShogiUI.BoardState/Extensions.cs create mode 100644 Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj create mode 100644 Gameboard.ShogiUI.BoardState/Move.cs create mode 100644 Gameboard.ShogiUI.BoardState/Piece.cs create mode 100644 Gameboard.ShogiUI.BoardState/Position.cs create mode 100644 Gameboard.ShogiUI.BoardState/ShogiBoard.cs create mode 100644 Gameboard.ShogiUI.BoardState/WhichPiece.cs create mode 100644 Gameboard.ShogiUI.BoardState/WhichPlayer.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs create mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs create mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs rename Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj => Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj (100%) rename {Gameboard.ShogiUI.Sockets.UnitTests/Models => Gameboard.ShogiUI.UnitTests/Sockets}/CoordsModelShould.cs (91%) diff --git a/.gitignore b/.gitignore index 26787d3..70b8a69 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ Thumbs.db #Luke bin obj +*.user diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj new file mode 100644 index 0000000..78da0b4 --- /dev/null +++ b/Benchmarking/Benchmarking.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Exe + + + + + + + + + + + diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs new file mode 100644 index 0000000..8cb0685 --- /dev/null +++ b/Benchmarking/Benchmarks.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using Gameboard.ShogiUI.BoardState; +using System; + +namespace Benchmarking +{ + public class Benchmarks + { + private Move[] moves; + + public Benchmarks() + { + moves = new[] + { + // P1 Rook + new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + // P2 Gold + new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + // P1 Pawn + new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + // P2 other Gold + new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + // P1 same Pawn + new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + // P2 Pawn + new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + // P1 Pawn takes P2 Pawn + new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + // P2 King + new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + // P1 Pawn promotes + new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + // P2 King retreat + new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + }; + } + + [Benchmark] + public void OnlyValidMoves_NewBoard() + { + var board = new ShogiBoard(); + foreach (var move in moves) + { + board.TryMove(move); + } + } + } + + public class Program + { + public static void Main(string[] args) + { + BenchmarkRunner.Run(); + Console.WriteLine("Done"); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs new file mode 100644 index 0000000..f2d7e0c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.BoardState +{ + public class Array2D : IEnumerable + { + private readonly T[] array; + private readonly int width; + + public Array2D(int width, int height) + { + this.width = width; + array = new T[width * height]; + } + + public T this[int x, int y] + { + get => array[y * width + x]; + set => array[y * width + x] = value; + } + + IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/BoardVector.cs b/Gameboard.ShogiUI.BoardState/BoardVector.cs new file mode 100644 index 0000000..5568ac5 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/BoardVector.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; + +namespace Gameboard.ShogiUI.BoardState +{ + /// + /// Provides normalized BoardVectors relative to player. + /// "Up" for player 1 is "Down" for player 2; that sort of thing. + /// + public class Direction + { + private static readonly BoardVector PositiveX = new BoardVector(1, 0); + private static readonly BoardVector NegativeX = new BoardVector(-1, 0); + private static readonly BoardVector PositiveY = new BoardVector(0, 1); + private static readonly BoardVector NegativeY = new BoardVector(0, -1); + private static readonly BoardVector PositiveYX = new BoardVector(1, 1); + private static readonly BoardVector NegativeYX = new BoardVector(-1, -1); + private static readonly BoardVector NegativeYPositiveX = new BoardVector(1, -1); + private static readonly BoardVector PositiveYNegativeX = new BoardVector(-1, 1); + + private readonly WhichPlayer whichPlayer; + public Direction(WhichPlayer whichPlayer) + { + this.whichPlayer = whichPlayer; + } + + public BoardVector Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; + public BoardVector Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; + public BoardVector Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; + public BoardVector Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; + public BoardVector UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; + public BoardVector UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; + public BoardVector DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; + public BoardVector DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; + public BoardVector KnightLeft => whichPlayer == WhichPlayer.Player1 ? new BoardVector(-1, 2) : new BoardVector(1, -2); + public BoardVector KnightRight => whichPlayer == WhichPlayer.Player1 ? new BoardVector(1, 2) : new BoardVector(-1, -2); + + } + + [DebuggerDisplay("[{X}, {Y}]")] + public class BoardVector + { + public int X { get; set; } + public int Y { get; set; } + public bool IsValidBoardPosition => X > -1 && X < 9 && Y > -1 && Y < 9; + public bool IsHand => X < 0 && Y < 0; // TODO: Find a better way to distinguish positions vs hand. + public BoardVector(int x, int y) + { + X = x; + Y = y; + } + + public BoardVector Add(BoardVector other) => new BoardVector(X + other.X, Y + other.Y); + public override bool Equals(object obj) => (obj is BoardVector other) && other.X == X && other.Y == Y; + public override int GetHashCode() + { + // [0,3] should hash different than [3,0] + return X.GetHashCode() * 3 + Y.GetHashCode() * 5; + } + public static bool operator ==(BoardVector a, BoardVector b) => a.Equals(b); + public static bool operator !=(BoardVector a, BoardVector b) => !a.Equals(b); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Extensions.cs b/Gameboard.ShogiUI.BoardState/Extensions.cs new file mode 100644 index 0000000..19dd810 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Extensions.cs @@ -0,0 +1,19 @@ +using System; +namespace Gameboard.ShogiUI.BoardState +{ + public static class Extensions + { + public static void ForEachNotNull(this Piece[,] array, Action action) + { + for (var x = 0; x < array.GetLength(0); x++) + for (var y = 0; y < array.GetLength(1); y++) + { + var piece = array[x, y]; + if (piece != null) + { + action(piece, x, y); + } + } + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj new file mode 100644 index 0000000..f208d30 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs new file mode 100644 index 0000000..f87ebad --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -0,0 +1,10 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public class Move + { + public WhichPiece? PieceFromCaptured { get; set; } + public BoardVector From { get; set; } + public BoardVector To { get; set; } + public bool IsPromotion { get; set; } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Piece.cs new file mode 100644 index 0000000..9a7ff64 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Piece.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; + +namespace Gameboard.ShogiUI.BoardState +{ + [DebuggerDisplay("{WhichPiece} {Owner}")] + public class Piece + { + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + + public Piece(WhichPiece piece, WhichPlayer owner) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = false; + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldenGeneral; + + public string ShortName => WhichPiece switch + { + WhichPiece.King => " K ", + WhichPiece.GoldenGeneral => " G ", + WhichPiece.SilverGeneral => IsPromoted ? "^S^" : " S ", + WhichPiece.Bishop => IsPromoted ? "^B^" : " B ", + WhichPiece.Rook => IsPromoted ? "^R^" : " R ", + WhichPiece.Knight => IsPromoted ? "^k^" : " k ", + WhichPiece.Lance => IsPromoted ? "^L^" : " L ", + WhichPiece.Pawn => IsPromoted ? "^P^" : " P ", + _ => " ? ", + }; + + public void ToggleOwnership() + { + Owner = Owner == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; + } + + public void Promote() => IsPromoted = true; + + public void Demote() => IsPromoted = false; + + public void Capture() + { + ToggleOwnership(); + Demote(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Position.cs b/Gameboard.ShogiUI.BoardState/Position.cs new file mode 100644 index 0000000..71552d7 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Position.cs @@ -0,0 +1,35 @@ +using System; + +namespace Gameboard.ShogiUI.BoardState +{ + public class Position + { + private int x; + private int y; + + public int X + { + get => x; + set { + if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); + x = value; + } + } + + public int Y + { + get => y; + set + { + if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); + y = value; + } + } + + public Position(int x, int y) + { + X = x; + Y = y; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs new file mode 100644 index 0000000..0d45761 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Gameboard.ShogiUI.BoardState +{ + /// + /// Facilitates Shogi board state transitions, cognisant of Shogi rules. + /// The board is always from Player1's perspective. + /// [0,0] is the lower-left position, [8,8] is the higher-right position + /// + public class ShogiBoard + { + private delegate void MoveSetCallback(Piece piece, BoardVector position); + private ShogiBoard validationBoard; + + public IReadOnlyDictionary> Hands { get; } + public Piece[,] Board { get; } + public List MoveHistory { get; } + public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; + public WhichPlayer? InCheck { get; private set; } + public bool IsCheckmate { get; private set; } + + public ShogiBoard() + { + Board = new Piece[9, 9]; + MoveHistory = new List(20); + Hands = new Dictionary> { + { WhichPlayer.Player1, new List()}, + { WhichPlayer.Player2, new List()}, + }; + InitializeBoardState(); + } + public ShogiBoard(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!TryMove(moves[i])) + { + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + } + } + } + + /// + /// Attempts a given move. Returns false if the move is illegal. + /// + public bool TryMove(Move move) + { + // Try making the move in a "throw away" board. + if (validationBoard == null) + { + validationBoard = new ShogiBoard(MoveHistory); + } + var isValid = move.PieceFromCaptured.HasValue + ? validationBoard.PlaceFromHand(move) + : validationBoard.PlaceFromBoard(move); + if (!isValid) + { + // Invalidate the "throw away" board. + validationBoard = null; + return false; + } + // Assert that this move does not put the moving player in check. + if (validationBoard.EvaluateCheck(WhoseTurn)) return false; + + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + // The move is valid and legal; update board state. + if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); + else PlaceFromBoard(move); + + // Evaluate check + InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; + if (InCheck.HasValue) + { + //IsCheckmate = EvaluateCheckmate(); + } + return true; + } + + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; + + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, x, y) => // For each piece... + { + if (!isCheckmate) return; // Short circuit + + var from = new BoardVector(x, y); + if (piece.Owner == InCheck) // Owned by the player in check... + { + var positionsToCheck = new List(10); + IterateMoveSet(from, (innerPiece, position) => + { + if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... + }); + + // And evaluate if any move gets the player out of check. + foreach (var position in positionsToCheck) + { + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + if (moveSuccess) + { + Console.WriteLine($"Not check mate"); + isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); + validationBoard = null; + } + } + } + }); + return isCheckmate; + } + /// True if the move was successful. + private bool PlaceFromHand(Move move) + { + if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move + var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); + if (index < 0) return false; // Invalid move + if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. + + var minimumY = 0; + switch (move.PieceFromCaptured.Value) + { + case WhichPiece.Knight: + // Knight cannot be placed onto the farthest two ranks from the hand. + minimumY = WhoseTurn == WhichPlayer.Player1 ? 2 : 6; + break; + case WhichPiece.Lance: + case WhichPiece.Pawn: + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + minimumY = WhoseTurn == WhichPlayer.Player1 ? 1 : 7; + break; + } + if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; + if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; + + // Mutate the board. + Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; + Hands[WhoseTurn].RemoveAt(index); + + return true; + } + /// True if the move was successful. + private bool PlaceFromBoard(Move move) + { + var fromPiece = Board[move.From.X, move.From.Y]; + if (fromPiece == null) return false; // Invalid move + if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. + if (ValidateMoveAgainstMoveSet(move) == false) return false; // Invalid move; move not part of move-set. + + var captured = Board[move.To.X, move.To.Y]; + if (captured != null) + { + if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. + captured.Capture(); + Hands[captured.Owner].Add(captured); + } + + //Mutate the board. + if (move.IsPromotion) + { + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Y > 5)) + { + fromPiece.Promote(); + } + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Y < 3)) + { + fromPiece.Promote(); + } + } + Board[move.To.X, move.To.Y] = fromPiece; + Board[move.From.X, move.From.Y] = null; + MoveHistory.Add(move); + return true; + } + public void PrintStateAsAscii() + { + var builder = new StringBuilder(); + builder.Append(" Player 2"); + builder.AppendLine(); + for (var y = 8; y > -1; y--) + { + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append('|'); + for (var x = 0; x < 9; x++) + { + var piece = Board[x, y]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", piece.ShortName); + } + builder.Append('|'); + } + builder.AppendLine(); + } + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" Player 1"); + Console.WriteLine(builder.ToString()); + } + #region Rules Validation + /// + /// Evaluate if a player is in check given the current board state. + /// + private bool EvaluateCheck(WhichPlayer whichPlayer) + { + var inCheck = false; + // Iterate every board piece... + Board.ForEachNotNull((piece, x, y) => + { + // ...that belongs to the opponent... + if (piece.Owner != whichPlayer) + { + IterateMoveSet(new BoardVector(x, y), (threatenedPiece, position) => + { + // ...and threatens the player's king. + inCheck |= + threatenedPiece?.WhichPiece == WhichPiece.King + && threatenedPiece?.Owner == whichPlayer; + }); + } + }); + return inCheck; + } + private bool EvaluateCheck2(WhichPlayer whichPlayer) + { + var inCheck = false; + MoveSetCallback checkKingThreat = (piece, position) => + { + inCheck |= + piece?.WhichPiece == WhichPiece.King + && piece?.Owner == whichPlayer; + }; + // Find interesting pieces + var longRangePiecePositions = new List(8); + Board.ForEachNotNull((piece, x, y) => + { + if (piece.Owner != whichPlayer) + { + switch (piece.WhichPiece) + { + case WhichPiece.Bishop: + case WhichPiece.Rook: + longRangePiecePositions.Add(new BoardVector(x, y)); + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) longRangePiecePositions.Add(new BoardVector(x, y)); + break; + } + } + }); + + foreach(var position in longRangePiecePositions) + { + IterateMoveSet(position, checkKingThreat); + } + + return inCheck; + } + private bool ValidateMoveAgainstMoveSet(Move move) + { + var isValid = false; + IterateMoveSet(move.From, (piece, position) => + { + if (piece?.Owner != WhoseTurn && position == move.To) + { + isValid = true; + } + }); + + return isValid; + } + /// + /// Iterate through the possible moves of a piece at a given position. + /// + private void IterateMoveSet(BoardVector from, MoveSetCallback callback) + { + // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. + // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. + var piece = Board[from.X, from.Y]; + switch (piece.WhichPiece) + { + case WhichPiece.King: + IterateKingMoveSet(from, callback); + break; + case WhichPiece.GoldenGeneral: + IterateGoldenGeneralMoveSet(from, callback); + break; + case WhichPiece.SilverGeneral: + IterateSilverGeneralMoveSet(from, callback); + break; + case WhichPiece.Bishop: + IterateBishopMoveSet(from, callback); + break; + case WhichPiece.Rook: + IterateRookMoveSet(from, callback); + break; + case WhichPiece.Knight: + IterateKnightMoveSet(from, callback); + break; + case WhichPiece.Lance: + IterateLanceMoveSet(from, callback); + break; + case WhichPiece.Pawn: + IteratePawnMoveSet(from, callback); + break; + } + } + private void IterateKingMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.Down, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + } + private void IterateGoldenGeneralMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.Down, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + } + private void IterateSilverGeneralMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + } + } + private void IterateBishopMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.UpLeft, callback); + BoardWalk(from, direction.UpRight, callback); + BoardWalk(from, direction.DownLeft, callback); + BoardWalk(from, direction.DownRight, callback); + if (piece.IsPromoted) + { + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + BoardStep(from, direction.Down, callback); + } + } + private void IterateRookMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.Up, callback); + BoardWalk(from, direction.Left, callback); + BoardWalk(from, direction.Right, callback); + BoardWalk(from, direction.Down, callback); + if (piece.IsPromoted) + { + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + } + } + private void IterateKnightMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardStep(from, direction.KnightLeft, callback); + BoardStep(from, direction.KnightRight, callback); + } + } + private void IterateLanceMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.Up, callback); + } + } + private void IteratePawnMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece?.WhichPiece == WhichPiece.Pawn) + { + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + } + } + } + /// + /// Useful for iterating the board for pieces that move many spaces. + /// + /// A function that returns true if walking should continue. + private void BoardWalk(BoardVector from, BoardVector direction, MoveSetCallback callback) + { + var foundAnotherPiece = false; + var to = from.Add(direction); + while (to.IsValidBoardPosition && !foundAnotherPiece) + { + var piece = Board[to.X, to.Y]; + callback(piece, to); + to = to.Add(direction); + foundAnotherPiece = piece != null; + } + } + + /// + /// Useful for iterating the board for pieces that move only one space. + /// + private void BoardStep(BoardVector from, BoardVector direction, MoveSetCallback callback) + { + var to = from.Add(direction); + if (to.IsValidBoardPosition) + { + callback(Board[to.X, to.Y], to); + } + } + #endregion + + #region Initialize + private void ResetEmptyRows() + { + for (int y = 3; y < 6; y++) + for (int x = 0; x < 9; x++) + Board[x, y] = null; + } + private void ResetFrontRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 2 : 6; + for (int x = 0; x < 9; x++) Board[x, y] = new Piece(WhichPiece.Pawn, player); + } + private void ResetMiddleRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 1 : 7; + + Board[0, y] = null; + for (int x = 2; x < 7; x++) Board[x, y] = null; + Board[8, y] = null; + if (player == WhichPlayer.Player1) + { + Board[1, y] = new Piece(WhichPiece.Bishop, player); + Board[7, y] = new Piece(WhichPiece.Rook, player); + } + else + { + Board[1, y] = new Piece(WhichPiece.Rook, player); + Board[7, y] = new Piece(WhichPiece.Bishop, player); + } + } + private void ResetRearRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 0 : 8; + + Board[0, y] = new Piece(WhichPiece.Lance, player); + Board[1, y] = new Piece(WhichPiece.Knight, player); + Board[2, y] = new Piece(WhichPiece.SilverGeneral, player); + Board[3, y] = new Piece(WhichPiece.GoldenGeneral, player); + Board[4, y] = new Piece(WhichPiece.King, player); + Board[5, y] = new Piece(WhichPiece.GoldenGeneral, player); + Board[6, y] = new Piece(WhichPiece.SilverGeneral, player); + Board[7, y] = new Piece(WhichPiece.Knight, player); + Board[8, y] = new Piece(WhichPiece.Lance, player); + } + private void InitializeBoardState() + { + ResetRearRow(WhichPlayer.Player1); + ResetMiddleRow(WhichPlayer.Player1); + ResetFrontRow(WhichPlayer.Player1); + ResetEmptyRows(); + ResetFrontRow(WhichPlayer.Player2); + ResetMiddleRow(WhichPlayer.Player2); + ResetRearRow(WhichPlayer.Player2); + } + #endregion + } +} diff --git a/Gameboard.ShogiUI.BoardState/WhichPiece.cs b/Gameboard.ShogiUI.BoardState/WhichPiece.cs new file mode 100644 index 0000000..a0dd88c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public enum WhichPiece + { + King, + GoldenGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs new file mode 100644 index 0000000..1e8de13 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index 1f6541f..c45d51c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -13,7 +13,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; private set; } public string Error { get; set; } - public IEnumerable Games { get; set; } + public ICollection Games { get; set; } public ListGamesResponse(ClientAction action) { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index 19e6c08..c457791 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; private set; } public Game Game { get; set; } - public IEnumerable Moves { get; set; } + public IReadOnlyList Moves { get; set; } public string Error { get; set; } public LoadGameResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs new file mode 100644 index 0000000..b42c398 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public class BoardState + { + public Piece[,] Board { get; set; } + public IReadOnlyCollection Player1Hand { get; set; } + public IReadOnlyCollection Player2Hand { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index a4a2ebe..3f5ddc6 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,8 +1,13 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { - public class Game - { - public string GameName { get; set; } - public string[] Players { get; set; } - } + public class Game + { + public string GameName { get; set; } + /// + /// Players[0] is the session owner, Players[1] is the other guy + /// + public IReadOnlyList Players { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs new file mode 100644 index 0000000..bb5ef62 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public class Piece + { + public WhichPiece WhichPiece { get; set; } + + /// + /// True if this piece is controlled by you. + /// + public bool IsControlledByMe { get; set; } + + public bool IsPromoted { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs new file mode 100644 index 0000000..b83e22e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public enum WhichPiece + { + King, + GoldGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index adfe2f0..38ccb7d 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -9,7 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Sockets.UnitTests", "Gameboard.ShogiUI.Sockets.UnitTests\Gameboard.ShogiUI.Sockets.UnitTests.csproj", "{8D753AD0-0985-415C-80B3-CCADF3AE1DF9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.BoardState", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.BoardState.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,16 +29,24 @@ Global {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.Build.0 = Release|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.Build.0 = Release|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.Build.0 = Release|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 038f52e..50c2ac9 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -15,6 +15,7 @@ + diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user deleted file mode 100644 index 2adf92b..0000000 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user +++ /dev/null @@ -1,12 +0,0 @@ - - - - ApiControllerEmptyScaffolder - root/Controller - AspShogiSockets - false - - - ProjectDebugger - - \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs index 49f21a4..494c738 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -1,6 +1,35 @@ -namespace Gameboard.ShogiUI.Sockets.Managers +using Gameboard.ShogiUI.BoardState; +using System.Collections.Concurrent; + +namespace Gameboard.ShogiUI.Sockets.Managers { - public class BoardManager + public interface IBoardManager { + void Add(string sessionName, ShogiBoard board); + ShogiBoard Get(string sessionName); + } + + public class BoardManager : IBoardManager + { + private readonly ConcurrentDictionary Boards; + + public BoardManager() + { + Boards = new ConcurrentDictionary(); + } + + public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); + + public ShogiBoard Get(string sessionName) + { + if (Boards.TryGetValue(sessionName, out var board)) + return board; + return null; + } + + public string GetBoardState() + { + return string.Empty; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index 4b72412..d8244a7 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,15 +1,15 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; 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; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + // TODO: This doesn't need to be a socket action. + // It can be an API route and still tell socket connections about the new session. public class CreateGameHandler : IActionHandler { private readonly ILogger logger; @@ -26,13 +26,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var postSessionResponse = await repository.PostSession(new PostSession { SessionName = request.GameName, - PlayerName = userName, // TODO : Investigate if needed by UI + PlayerName = userName, IsPrivate = request.IsPrivate }); @@ -53,9 +53,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (request.IsPrivate) { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } else { diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs index 5168598..20f98ab 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs @@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers /// /// Responsible for parsing json and handling the request. /// - Task Handle(WebSocket socket, string json, string userName); + Task Handle(string json, string userName); } public delegate IActionHandler ActionHandlerResolver(ClientAction action); diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index c4f2869..f8edb5e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -1,11 +1,9 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; 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; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -26,7 +24,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession @@ -37,37 +35,32 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (joinGameResponse.JoinSucceeded) { - var gameName = (await repository.GetGame(joinGameResponse.SessionName)).Session.Name; - // Other members of the game see a regular JoinGame occur. var response = new JoinGameResponse(ClientAction.JoinGame) { PlayerName = userName, - GameName = gameName + GameName = joinGameResponse.SessionName }; - // At this time, userName hasn't subscribed and won't receive this broadcasted messages. - await communicationManager.BroadcastToGame(gameName, response); + // At this time, userName hasn't subscribed and won't receive this message. + await communicationManager.BroadcastToGame(joinGameResponse.SessionName, response); - // But the player joining sees the JoinByCode occur. + // The player joining sees the JoinByCode occur. response = new JoinGameResponse(ClientAction.JoinByCode) { PlayerName = userName, - GameName = gameName + GameName = joinGameResponse.SessionName }; - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } else { var response = new JoinGameResponse(ClientAction.JoinByCode) { PlayerName = userName, + GameName = joinGameResponse.SessionName, Error = "Error joining game." }; - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 96e1ed7..c00aa64 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -3,7 +3,6 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Newtonsoft.Json; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -20,13 +19,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName - }; var joinGameResponse = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession { @@ -34,15 +29,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers SessionName = request.GameName }); + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName, + GameName = request.GameName + }; if (joinGameResponse.JoinSucceeded) { - response.GameName = request.GameName; + await communicationManager.BroadcastToAll(response); } else { response.Error = "Game is full."; + await communicationManager.BroadcastToPlayers(response, userName); } - await communicationManager.BroadcastToAll(response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 43431a0..d4379e3 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,26 +1,29 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Models; +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 Newtonsoft.Json; using System.Linq; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + // TODO: This doesn't need to be a socket action. + // It can be an HTTP route. public class ListGamesHandler : IActionHandler { + private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; public ListGamesHandler( + ISocketCommunicationManager communicationManager, IGameboardRepository repository) { + this.communicationManager = communicationManager; this.repository = repository; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var getGamesResponse = string.IsNullOrWhiteSpace(userName) @@ -33,11 +36,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers var response = new ListGamesResponse(ClientAction.ListGames) { - Games = games + Games = games.ToList() }; - var serialized = JsonConvert.SerializeObject(response); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index b0fac4f..db15bfa 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,56 +1,66 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.BoardState; 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.Linq; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + /// + /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. + /// public class LoadGameHandler : IActionHandler { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; + private readonly IBoardManager boardManager; public LoadGameHandler( ILogger logger, ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + IGameboardRepository gameboardRepository, + IBoardManager boardManager) { this.logger = logger; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; + this.boardManager = boardManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var getGameResponse = await gameboardRepository.GetGame(request.GameName); - var getMovesResponse = await gameboardRepository.GetMoves(request.GameName); + var gameTask = gameboardRepository.GetGame(request.GameName); + var moveTask = gameboardRepository.GetMoves(request.GameName); - var response = new LoadGameResponse(ClientAction.LoadGame); + var getGameResponse = await gameTask; + var getMovesResponse = await moveTask; if (getGameResponse == null || getMovesResponse == null) { - response.Error = $"Could not find game."; + logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); + var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; + await communicationManager.BroadcastToPlayers(response, userName); } else { - var sessionModel = new Session(getGameResponse.Session); - communicationManager.SubscribeToGame(socket, sessionModel, userName); + var sessionModel = new Models.Session(getGameResponse.Session); + var moveModels = getMovesResponse.Moves.Select(_ => new Models.Move(_)).ToList(); - response.Game = sessionModel.ToServiceModel(); - response.Moves = getMovesResponse.Moves.Select(_ => Mapper.Map(_).ToServiceModel()); + communicationManager.SubscribeToGame(sessionModel, userName); + var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); + boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); + + var response = new LoadGameResponse(ClientAction.LoadGame) + { + Game = sessionModel.ToServiceModel(), + Moves = moveModels.Select(_ => _.ToServiceModel()).ToList(), + }; + await communicationManager.BroadcastToPlayers(response, userName); } - - var serialized = JsonConvert.SerializeObject(response); - 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 fdc535b..5fe8a11 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,45 +1,47 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.Repositories; -using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Newtonsoft.Json; -using System.Net.WebSockets; -using System.Threading.Tasks; using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories; +using Newtonsoft.Json; +using System.Threading.Tasks; +using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class MoveHandler : IActionHandler { + private readonly IBoardManager boardManager; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; public MoveHandler( + IBoardManager boardManager, ISocketCommunicationManager communicationManager, IGameboardRepository gameboardRepository) { + this.boardManager = boardManager; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); // Basic move validation if (request.Move.To.Equals(request.Move.From)) { - var serialized = JsonConvert.SerializeObject( - new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) - { - Error = "Error: moving piece from tile to the same tile." - }); - await socket.SendTextAsync(serialized); + var error = new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) + { + Error = "Error: moving piece from tile to the same tile." + }; + await communicationManager.BroadcastToPlayers(error, userName); return; } var moveModel = new Move(request.Move); - var session = (await gameboardRepository.GetGame(request.GameName)).Session; - await gameboardRepository.PostMove(request.GameName, new PostMove(Mapper.Map(moveModel))); + var board = boardManager.Get(request.GameName); + var boardMove = moveModel.ToBoardModel(); + //board.Move() + await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) { @@ -47,7 +49,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, Move = moveModel.ToServiceModel() }; - await communicationManager.BroadcastToGame(session.Name, response); + await communicationManager.BroadcastToGame(request.GameName, response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 6923621..3269493 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -21,10 +21,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers 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 SubscribeToGame(Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromGame(string gameName, string playerName); + Task BroadcastToPlayers(IResponse response, params string[] playerNames); } public class SocketCommunicationManager : ISocketCommunicationManager @@ -65,7 +66,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers else { var handler = handlerResolver(request.Action); - await handler.Handle(socket, message, userName); + await handler.Handle(message, userName); } } catch (OperationCanceledException ex) @@ -100,7 +101,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// /// Unsubscribes the player from their current game, then subscribes to the new game. /// - public void SubscribeToGame(WebSocket socket, Session session, string playerName) + public void SubscribeToGame(Session session, string playerName) { // Unsubscribe from any other games foreach (var kvp in sessions) @@ -110,8 +111,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers } // Subscribe - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); + if (connections.TryGetValue(playerName, out var socket)) + { + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); + } } public void UnsubscribeFromGame(string gameName, string playerName) @@ -123,6 +127,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers } } + public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (connections.TryGetValue(name, out var socket)) + { + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); + + } + } + await Task.WhenAll(tasks); + } public Task BroadcastToAll(IResponse response) { var message = JsonConvert.SerializeObject(response); diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs deleted file mode 100644 index ea59d0e..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Microsoft.FSharp.Core; -using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Managers.Utility -{ - public static class Mapper - { - public static ShogiApi.Move Map(Move source) - { - var from = source.From; - var to = source.To; - FSharpOption pieceFromCaptured = source.PieceFromCaptured switch - { - "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 ShogiApi.Move - { - 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(ShogiApi.Move source) - { - var origin = source.Origin; - var destination = source.Destination; - string pieceFromCaptured = null; - if (source.PieceFromCaptured != null) - { - pieceFromCaptured = source.PieceFromCaptured.Value switch - { - 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", - _ => "" - }; - } - - var target = new Move - { - From = new Coords(origin.X, origin.Y), - To = new Coords(destination.X, destination.Y), - IsPromotion = source.IsPromotion, - PieceFromCaptured = pieceFromCaptured - }; - - return target; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 932285a..fcb7274 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,4 +1,9 @@ -namespace Gameboard.ShogiUI.Sockets.Models +using Gameboard.ShogiUI.BoardState; +using Microsoft.FSharp.Core; +using System; +using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; + +namespace Gameboard.ShogiUI.Sockets.Models { public class Move { @@ -15,7 +20,29 @@ PieceFromCaptured = move.PieceFromCaptured; IsPromotion = move.IsPromotion; } - + public Move(ShogiApi.Move move) + { + string pieceFromCaptured = null; + if (move.PieceFromCaptured != null) + { + pieceFromCaptured = move.PieceFromCaptured.Value switch + { + ShogiApi.WhichPieceName.Bishop => "", + 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", + _ => "" + }; + } + From = new Coords(move.Origin.X, move.Origin.Y); + To = new Coords(move.Destination.X, move.Destination.Y); + IsPromotion = move.IsPromotion; + PieceFromCaptured = pieceFromCaptured; + } public ServiceModels.Socket.Types.Move ToServiceModel() => new ServiceModels.Socket.Types.Move { From = From.ToBoardNotation(), @@ -23,5 +50,38 @@ PieceFromCaptured = PieceFromCaptured, To = To.ToBoardNotation() }; + public ShogiApi.Move ToApiModel() + { + var pieceFromCaptured = PieceFromCaptured switch + { + "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 ShogiApi.Move + { + Origin = new ShogiApi.BoardLocation { X = From.X, Y = From.Y }, + Destination = new ShogiApi.BoardLocation { X = To.X, Y = To.Y }, + IsPromotion = IsPromotion, + PieceFromCaptured = pieceFromCaptured + }; + return target; + } + public BoardState.Move ToBoardModel() + { + return new BoardState.Move + { + From = new BoardVector(From.X, From.Y), + IsPromotion = IsPromotion, + PieceFromCaptured = Enum.TryParse(PieceFromCaptured, out var whichPiece) ? whichPiece : null, + To = new BoardVector(To.X, To.Y) + }; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 7d32044..09805be 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -45,6 +45,7 @@ namespace Gameboard.ShogiUI.Sockets services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.AddSingleton(sp => action => { return action switch diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs new file mode 100644 index 0000000..85d9b99 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs @@ -0,0 +1,26 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Gameboard.ShogiUI.UnitTests.BoardState +{ + [TestClass] + public class BoardVectorShould + { + [TestMethod] + public void BeEqualWhenPropertiesAreEqual() + { + var a = new BoardVector(3, 2); + var b = new BoardVector(3, 2); + a.Should().Be(b); + a.GetHashCode().Should().Be(b.GetHashCode()); + (a == b).Should().BeTrue(); + + // Properties should not be transitively equal. + b = new BoardVector(2, 3); + a.Should().NotBe(b); + a.GetHashCode().Should().NotBe(b.GetHashCode()); + (a == b).Should().BeFalse(); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs new file mode 100644 index 0000000..8d4a22d --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -0,0 +1,322 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +namespace Gameboard.ShogiUI.UnitTests.BoardState +{ + [TestClass] + public class ShogiBoardShould + { + [TestMethod] + public void InitializeBoardState() + { + // Assert + var board = new ShogiBoard().Board; + // Assert pieces do not start promoted. + foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); + + // Assert Player1. + for (var y = 0; y < 3; y++) + for (var x = 0; x < 9; x++) + board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); + board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); + board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[4, 0].WhichPiece.Should().Be(WhichPiece.King); + board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[0, 1].Should().BeNull(); + board[1, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); + board[7, 1].WhichPiece.Should().Be(WhichPiece.Rook); + board[8, 1].Should().BeNull(); + for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + + // Assert empty locations. + for (var y = 3; y < 6; y++) + for (var x = 0; x < 9; x++) + board[x, y].Should().BeNull(); + + // Assert Player2. + for (var y = 6; y < 9; y++) + for (var x = 0; x < 9; x++) + board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); + board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[4, 8].WhichPiece.Should().Be(WhichPiece.King); + board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[0, 7].Should().BeNull(); + board[1, 7].WhichPiece.Should().Be(WhichPiece.Rook); + for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); + board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + board[8, 7].Should().BeNull(); + for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void InitializeBoardStateWithMoves() + { + var moves = new[] + { + new Move + { + // Pawn + From = new BoardVector(0, 2), + To = new BoardVector(0, 3) + } + }; + var shogi = new ShogiBoard(moves); + shogi.Board[0, 2].Should().BeNull(); + shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = new ShogiBoard(); + + // Act - P1 "moves" pawn to the position it already exists at. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(0, 2), To = new BoardVector(0, 2) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_MoveSet() + { + var invalidLanceMove = new Move + { + // Lance moving adjacent + From = new BoardVector(0, 0), + To = new BoardVector(1, 5) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidLanceMove); + + moveSuccess.Should().BeFalse(); + // Assert the Lance has not actually moved. + shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + } + + [TestMethod] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = new ShogiBoard(); + + // Act - Move Player2 Pawn when it's Player1 turn. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 6), To = new BoardVector(8, 5) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board[8, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[8, 5].Should().BeNull(); + } + + [TestMethod] + public void PreventInvalidMoves_MoveThroughAllies() + { + var invalidLanceMove = new Move + { + // Lance moving through the pawn before it. + From = new BoardVector(0, 0), + To = new BoardVector(0, 5) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidLanceMove); + + moveSuccess.Should().BeFalse(); + // Assert the Lance has not actually moved. + shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + } + + [TestMethod] + public void PreventInvalidMoves_CaptureAlly() + { + var invalidKnightMove = new Move + { + // Knight capturing allied Pawn + From = new BoardVector(1, 0), + To = new BoardVector(0, 2) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidKnightMove); + + moveSuccess.Should().BeFalse(); + // Assert the Knight has not actually moved or captured. + shogi.Board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + // P1 Bishop puts P2 in check + new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) } + }; + var shogi = new ShogiBoard(moves); + + // Prerequisit + shogi.InCheck.Should().Be(WhichPlayer.Player2); + + + // Act - P2 moves Lance while remaining in check. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 8), To = new BoardVector(8, 7) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board[8, 7].Should().BeNull(); + } + + [TestMethod] + public void Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + }; + var shogi = new ShogiBoard(moves); + + + // Act - P1 Bishop, check + shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) }); + + // Assert + shogi.InCheck.Should().Be(WhichPlayer.Player2); + } + + [TestMethod] + public void Capture() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 Bishop captures P2 Bishop + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(7, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board + .Cast() + .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) + .Should() + .Be(1); + shogi.Board[1, 1].Should().BeNull(); + shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Hands[WhichPlayer.Player1] + .Should() + .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); + + + // Act - P2 Silver captures P1 Bishop + moveSuccess = shogi.TryMove(new Move { From = new BoardVector(6, 8), To = new BoardVector(7, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board[6, 8].Should().BeNull(); + shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + shogi.Board + .Cast() + .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) + .Should().Be(0); + shogi.Hands[WhichPlayer.Player2] + .Should() + .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player2); + } + + [TestMethod] + public void Promote() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 moves across promote threshold. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6), IsPromotion = true }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board[1, 1].Should().BeNull(); + shogi.Board[6, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); + } + + [TestMethod] + public void CheckMate() + { + // Arrange + var moves = new[] + { + // P1 Rook + new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + // P2 Gold + new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + // P1 Pawn + new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + // P2 other Gold + new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + // P1 same Pawn + new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + // P2 Pawn + new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + // P1 Pawn takes P2 Pawn + new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + // P2 King + new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + // P1 Pawn promotes + new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + // P2 King retreat + new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 Pawn wins by checkmate. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.IsCheckmate.Should().BeTrue(); + + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj similarity index 100% rename from Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj rename to Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs similarity index 91% rename from Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs rename to Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs index d0c1961..f878da3 100644 --- a/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs @@ -2,7 +2,7 @@ using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Gameboard.ShogiUI.Sockets.UnitTests.Models +namespace Gameboard.ShogiUI.UnitTests.Sockets { [TestClass] public class CoordsModelShould From 640db4f4a2026524d3857c369d6c04e7e8b640aa Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 25 Feb 2021 19:55:43 -0600 Subject: [PATCH 04/27] before remove validation board --- Benchmarking/Benchmarks.cs | 15 ++- Gameboard.ShogiUI.BoardState/Array2D.cs | 51 ++++++++- Gameboard.ShogiUI.BoardState/Piece.cs | 8 ++ Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 124 ++++++++++++--------- Gameboard.ShogiUI.Sockets.sln | 8 +- PathFinding/IPlanarCollection.cs | 7 ++ PathFinding/PathFinder2D.cs | 10 ++ PathFinding/PathFinding.csproj | 7 ++ 8 files changed, 172 insertions(+), 58 deletions(-) create mode 100644 PathFinding/IPlanarCollection.cs create mode 100644 PathFinding/PathFinder2D.cs create mode 100644 PathFinding/PathFinding.csproj diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 8cb0685..208b962 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -7,7 +7,7 @@ namespace Benchmarking { public class Benchmarks { - private Move[] moves; + private readonly Move[] moves; public Benchmarks() { @@ -37,7 +37,7 @@ namespace Benchmarking } [Benchmark] - public void OnlyValidMoves_NewBoard() + public void One() { var board = new ShogiBoard(); foreach (var move in moves) @@ -45,6 +45,17 @@ namespace Benchmarking board.TryMove(move); } } + + [Benchmark] + public void Two() + { + var board = new ShogiBoard(); + foreach (var move in moves) + { + //board.TryMove2(move); + } + } + } public class Program diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs index f2d7e0c..623a536 100644 --- a/Gameboard.ShogiUI.BoardState/Array2D.cs +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -1,20 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.BoardState { - public class Array2D : IEnumerable + public class Array2D : IEnumerable { + /// False to stop iterating. + public delegate void ForEachDelegate(T element, int x, int y); private readonly T[] array; private readonly int width; + private readonly int height; public Array2D(int width, int height) { this.width = width; + this.height = height; array = new T[width * height]; } @@ -24,6 +25,48 @@ namespace Gameboard.ShogiUI.BoardState set => array[y * width + x] = value; } + public void ForEach(ForEachDelegate callback) + { + for (var x = 0; x < width; x++) + { + for (var y = 0; y < height; y++) + { + callback(this[x, y], x, y); + } + } + } + + public void ForEachNotNull(ForEachDelegate callback) + { + for (var x = 0; x < width; x++) + { + for (var y = 0; y < height; y++) + { + if (this[x, y] != null) + callback(this[x, y], x, y); + } + } + } + + // TODO: Figure out a better return type, or make this class specific to ShogiBoard. + public BoardVector IndexOf(Predicate predicate) + { + for (var x = 0; x < width; x++) + for (var y = 0; y < height; y++) + { + if (this[x, y] != null && predicate(this[x, y])) + { + return new BoardVector(x, y); + } + } + return null; + } + + public IEnumerator GetEnumerator() + { + foreach (var item in array) yield return item; + } + IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); } } diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Piece.cs index 9a7ff64..99b218d 100644 --- a/Gameboard.ShogiUI.BoardState/Piece.cs +++ b/Gameboard.ShogiUI.BoardState/Piece.cs @@ -33,6 +33,14 @@ namespace Gameboard.ShogiUI.BoardState _ => " ? ", }; + public bool IsRanged => WhichPiece switch + { + WhichPiece.Bishop => true, + WhichPiece.Rook => true, + WhichPiece.Lance => !IsPromoted, + _ => false, + }; + public void ToggleOwnership() { Owner = Owner == WhichPlayer.Player1 diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 0d45761..9b1ca78 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using System.Text; namespace Gameboard.ShogiUI.BoardState @@ -13,9 +14,10 @@ namespace Gameboard.ShogiUI.BoardState { private delegate void MoveSetCallback(Piece piece, BoardVector position); private ShogiBoard validationBoard; - + private Vector2 player1King; + private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } - public Piece[,] Board { get; } + public Array2D Board { get; } public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } @@ -23,13 +25,15 @@ namespace Gameboard.ShogiUI.BoardState public ShogiBoard() { - Board = new Piece[9, 9]; + Board = new Array2D(9, 9); MoveHistory = new List(20); Hands = new Dictionary> { { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; InitializeBoardState(); + player1King = new Vector2(4, 0); + player2King = new Vector2(4, 8); } public ShogiBoard(IList moves) : this() { @@ -45,7 +49,40 @@ namespace Gameboard.ShogiUI.BoardState /// /// Attempts a given move. Returns false if the move is illegal. /// - public bool TryMove(Move move) + //public bool TryMove2(Move move) + //{ + // // Try making the move in a "throw away" board. + // if (validationBoard == null) + // { + // validationBoard = new ShogiBoard(MoveHistory); + // } + // var isValid = move.PieceFromCaptured.HasValue + // ? validationBoard.PlaceFromHand(move) + // : validationBoard.PlaceFromBoard(move); + // if (!isValid) + // { + // // Invalidate the "throw away" board. + // validationBoard = null; + // return false; + // } + // // Assert that this move does not put the moving player in check. + // if (validationBoard.EvaluateCheck(WhoseTurn)) return false; + + // var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + // // The move is valid and legal; update board state. + // if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); + // else PlaceFromBoard(move); + + // // Evaluate check + // InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; + // if (InCheck.HasValue) + // { + // //IsCheckmate = EvaluateCheckmate(); + // } + // return true; + //} + + public bool TryMove(Move move, bool shouldEvaluateCheck = true) { // Try making the move in a "throw away" board. if (validationBoard == null) @@ -70,14 +107,18 @@ namespace Gameboard.ShogiUI.BoardState else PlaceFromBoard(move); // Evaluate check - InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; - if (InCheck.HasValue) + if (shouldEvaluateCheck) { - //IsCheckmate = EvaluateCheckmate(); + InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; + if (InCheck.HasValue) + { + IsCheckmate = EvaluateCheckmate(); + } } return true; } + private bool EvaluateCheckmate() { if (!InCheck.HasValue) return false; @@ -100,10 +141,9 @@ namespace Gameboard.ShogiUI.BoardState // And evaluate if any move gets the player out of check. foreach (var position in positionsToCheck) { - var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }, false); if (moveSuccess) { - Console.WriteLine($"Not check mate"); isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); validationBoard = null; } @@ -148,7 +188,7 @@ namespace Gameboard.ShogiUI.BoardState var fromPiece = Board[move.From.X, move.From.Y]; if (fromPiece == null) return false; // Invalid move if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. - if (ValidateMoveAgainstMoveSet(move) == false) return false; // Invalid move; move not part of move-set. + if (ValidateMoveAgainstMoveSet(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -172,6 +212,19 @@ namespace Gameboard.ShogiUI.BoardState } Board[move.To.X, move.To.Y] = fromPiece; Board[move.From.X, move.From.Y] = null; + if (fromPiece.WhichPiece == WhichPiece.King) + { + if (fromPiece.Owner == WhichPlayer.Player1) + { + player1King.X = move.To.X; + player1King.Y = move.To.Y; + } + else if (fromPiece.Owner == WhichPlayer.Player2) + { + player2King.X = move.To.X; + player2King.Y = move.To.Y; + } + } MoveHistory.Add(move); return true; } @@ -215,13 +268,16 @@ namespace Gameboard.ShogiUI.BoardState /// private bool EvaluateCheck(WhichPlayer whichPlayer) { + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; var inCheck = false; // Iterate every board piece... Board.ForEachNotNull((piece, x, y) => { - // ...that belongs to the opponent... - if (piece.Owner != whichPlayer) + var v = new Vector2(x, y); + // ...that belongs to the opponent within range... + if (piece.Owner != whichPlayer && (piece.IsRanged || Vector2.Distance(kingPosition, v) < 3)) { + Console.WriteLine($"Evaluating {piece.WhichPiece}"); IterateMoveSet(new BoardVector(x, y), (threatenedPiece, position) => { // ...and threatens the player's king. @@ -233,47 +289,13 @@ namespace Gameboard.ShogiUI.BoardState }); return inCheck; } - private bool EvaluateCheck2(WhichPlayer whichPlayer) - { - var inCheck = false; - MoveSetCallback checkKingThreat = (piece, position) => - { - inCheck |= - piece?.WhichPiece == WhichPiece.King - && piece?.Owner == whichPlayer; - }; - // Find interesting pieces - var longRangePiecePositions = new List(8); - Board.ForEachNotNull((piece, x, y) => - { - if (piece.Owner != whichPlayer) - { - switch (piece.WhichPiece) - { - case WhichPiece.Bishop: - case WhichPiece.Rook: - longRangePiecePositions.Add(new BoardVector(x, y)); - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) longRangePiecePositions.Add(new BoardVector(x, y)); - break; - } - } - }); - - foreach(var position in longRangePiecePositions) - { - IterateMoveSet(position, checkKingThreat); - } - - return inCheck; - } - private bool ValidateMoveAgainstMoveSet(Move move) + + private bool ValidateMoveAgainstMoveSet(BoardVector from, BoardVector to) { var isValid = false; - IterateMoveSet(move.From, (piece, position) => + IterateMoveSet(from, (piece, position) => { - if (piece?.Owner != WhoseTurn && position == move.To) + if (piece?.Owner != WhoseTurn && position == to) { isValid = true; } @@ -289,7 +311,7 @@ namespace Gameboard.ShogiUI.BoardState // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. var piece = Board[from.X, from.Y]; - switch (piece.WhichPiece) + switch (piece?.WhichPiece) { case WhichPiece.King: IterateKingMoveSet(from, callback); diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 38ccb7d..497525d 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -13,7 +13,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.BoardStat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,6 +43,10 @@ Global {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU + {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs new file mode 100644 index 0000000..8a2b046 --- /dev/null +++ b/PathFinding/IPlanarCollection.cs @@ -0,0 +1,7 @@ +namespace PathFinding +{ + interface IPlanarCollection + { + T this[int x, int y] { get; set; } + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs new file mode 100644 index 0000000..93b9bc0 --- /dev/null +++ b/PathFinding/PathFinder2D.cs @@ -0,0 +1,10 @@ + +namespace PathFinding +{ + public class PathFinder2D + { + public PathFinder2D() + { + } + } +} diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj new file mode 100644 index 0000000..f208d30 --- /dev/null +++ b/PathFinding/PathFinding.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + From 715b328ef5c6811a04eb70eee9fecef069548c9b Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 26 Feb 2021 17:11:08 -0600 Subject: [PATCH 05/27] Switch to Vector2 --- Benchmarking/Benchmarks.cs | 26 ++-- Gameboard.ShogiUI.BoardState/Array2D.cs | 11 +- Gameboard.ShogiUI.BoardState/BoardVector.cs | 65 +++------ Gameboard.ShogiUI.BoardState/Move.cs | 8 +- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 135 +++++++----------- .../ClientActionHandlers/LoadGameHandler.cs | 2 +- Gameboard.ShogiUI.Sockets/Models/Move.cs | 5 +- .../BoardState/BoardVectorShould.cs | 26 ---- .../BoardState/ShogiBoardShould.cs | 95 ++++++------ 9 files changed, 154 insertions(+), 219 deletions(-) delete mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 208b962..5a7947f 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -2,6 +2,7 @@ using BenchmarkDotNet.Running; using Gameboard.ShogiUI.BoardState; using System; +using System.Numerics; namespace Benchmarking { @@ -14,25 +15,25 @@ namespace Benchmarking moves = new[] { // P1 Rook - new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, // P2 Gold - new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, // P1 Pawn - new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, // P2 other Gold - new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, // P1 same Pawn - new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, // P2 Pawn - new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, // P1 Pawn takes P2 Pawn - new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, // P2 King - new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, // P1 Pawn promotes - new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, // P2 King retreat - new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; } @@ -42,7 +43,7 @@ namespace Benchmarking var board = new ShogiBoard(); foreach (var move in moves) { - board.TryMove(move); + board.Move(move); } } @@ -62,8 +63,9 @@ namespace Benchmarking { public static void Main(string[] args) { - BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); Console.WriteLine("Done"); + } } } diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs index 623a536..8e46f77 100644 --- a/Gameboard.ShogiUI.BoardState/Array2D.cs +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Numerics; namespace Gameboard.ShogiUI.BoardState { @@ -24,6 +25,11 @@ namespace Gameboard.ShogiUI.BoardState get => array[y * width + x]; set => array[y * width + x] = value; } + public T this[float x, float y] + { + get => array[(int)y * width + (int)x]; + set => array[(int)y * width + (int)x] = value; + } public void ForEach(ForEachDelegate callback) { @@ -48,15 +54,14 @@ namespace Gameboard.ShogiUI.BoardState } } - // TODO: Figure out a better return type, or make this class specific to ShogiBoard. - public BoardVector IndexOf(Predicate predicate) + public Vector2? IndexOf(Predicate predicate) { for (var x = 0; x < width; x++) for (var y = 0; y < height; y++) { if (this[x, y] != null && predicate(this[x, y])) { - return new BoardVector(x, y); + return new Vector2(x, y); } } return null; diff --git a/Gameboard.ShogiUI.BoardState/BoardVector.cs b/Gameboard.ShogiUI.BoardState/BoardVector.cs index 5568ac5..8f550d3 100644 --- a/Gameboard.ShogiUI.BoardState/BoardVector.cs +++ b/Gameboard.ShogiUI.BoardState/BoardVector.cs @@ -1,21 +1,21 @@ -using System.Diagnostics; +using System.Numerics; namespace Gameboard.ShogiUI.BoardState { /// - /// Provides normalized BoardVectors relative to player. + /// Provides normalized Vector2s relative to player. /// "Up" for player 1 is "Down" for player 2; that sort of thing. /// public class Direction { - private static readonly BoardVector PositiveX = new BoardVector(1, 0); - private static readonly BoardVector NegativeX = new BoardVector(-1, 0); - private static readonly BoardVector PositiveY = new BoardVector(0, 1); - private static readonly BoardVector NegativeY = new BoardVector(0, -1); - private static readonly BoardVector PositiveYX = new BoardVector(1, 1); - private static readonly BoardVector NegativeYX = new BoardVector(-1, -1); - private static readonly BoardVector NegativeYPositiveX = new BoardVector(1, -1); - private static readonly BoardVector PositiveYNegativeX = new BoardVector(-1, 1); + private static readonly Vector2 PositiveX = new Vector2(1, 0); + private static readonly Vector2 NegativeX = new Vector2(-1, 0); + private static readonly Vector2 PositiveY = new Vector2(0, 1); + private static readonly Vector2 NegativeY = new Vector2(0, -1); + private static readonly Vector2 PositiveYX = new Vector2(1, 1); + private static readonly Vector2 NegativeYX = new Vector2(-1, -1); + private static readonly Vector2 NegativeYPositiveX = new Vector2(1, -1); + private static readonly Vector2 PositiveYNegativeX = new Vector2(-1, 1); private readonly WhichPlayer whichPlayer; public Direction(WhichPlayer whichPlayer) @@ -23,40 +23,15 @@ namespace Gameboard.ShogiUI.BoardState this.whichPlayer = whichPlayer; } - public BoardVector Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; - public BoardVector Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; - public BoardVector Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; - public BoardVector Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; - public BoardVector UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; - public BoardVector UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; - public BoardVector DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; - public BoardVector DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; - public BoardVector KnightLeft => whichPlayer == WhichPlayer.Player1 ? new BoardVector(-1, 2) : new BoardVector(1, -2); - public BoardVector KnightRight => whichPlayer == WhichPlayer.Player1 ? new BoardVector(1, 2) : new BoardVector(-1, -2); - - } - - [DebuggerDisplay("[{X}, {Y}]")] - public class BoardVector - { - public int X { get; set; } - public int Y { get; set; } - public bool IsValidBoardPosition => X > -1 && X < 9 && Y > -1 && Y < 9; - public bool IsHand => X < 0 && Y < 0; // TODO: Find a better way to distinguish positions vs hand. - public BoardVector(int x, int y) - { - X = x; - Y = y; - } - - public BoardVector Add(BoardVector other) => new BoardVector(X + other.X, Y + other.Y); - public override bool Equals(object obj) => (obj is BoardVector other) && other.X == X && other.Y == Y; - public override int GetHashCode() - { - // [0,3] should hash different than [3,0] - return X.GetHashCode() * 3 + Y.GetHashCode() * 5; - } - public static bool operator ==(BoardVector a, BoardVector b) => a.Equals(b); - public static bool operator !=(BoardVector a, BoardVector b) => !a.Equals(b); + public Vector2 Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; + public Vector2 Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; + public Vector2 Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; + public Vector2 Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; + public Vector2 UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; + public Vector2 UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; + public Vector2 DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; + public Vector2 DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; + public Vector2 KnightLeft => whichPlayer == WhichPlayer.Player1 ? new Vector2(-1, 2) : new Vector2(1, -2); + public Vector2 KnightRight => whichPlayer == WhichPlayer.Player1 ? new Vector2(1, 2) : new Vector2(-1, -2); } } diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs index f87ebad..9df0f89 100644 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -1,10 +1,12 @@ -namespace Gameboard.ShogiUI.BoardState +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState { public class Move { public WhichPiece? PieceFromCaptured { get; set; } - public BoardVector From { get; set; } - public BoardVector To { get; set; } + public Vector2 From { get; set; } + public Vector2 To { get; set; } public bool IsPromotion { get; set; } } } diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 9b1ca78..0bd9819 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -12,7 +12,7 @@ namespace Gameboard.ShogiUI.BoardState /// public class ShogiBoard { - private delegate void MoveSetCallback(Piece piece, BoardVector position); + private delegate void MoveSetCallback(Piece piece, Vector2 position); private ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; @@ -35,59 +35,31 @@ namespace Gameboard.ShogiUI.BoardState player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); } - public ShogiBoard(IList moves) : this() - { - for (var i = 0; i < moves.Count; i++) - { - if (!TryMove(moves[i])) - { - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); - } - } - } + public bool Move(Move move) + { + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + var moveSuccess = TryMove(move); + + if (!moveSuccess) return false; + + // Evaluate check + InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; + if (InCheck.HasValue) + { + //IsCheckmate = EvaluateCheckmate(); + } + return true; + } /// /// Attempts a given move. Returns false if the move is illegal. /// - //public bool TryMove2(Move move) - //{ - // // Try making the move in a "throw away" board. - // if (validationBoard == null) - // { - // validationBoard = new ShogiBoard(MoveHistory); - // } - // var isValid = move.PieceFromCaptured.HasValue - // ? validationBoard.PlaceFromHand(move) - // : validationBoard.PlaceFromBoard(move); - // if (!isValid) - // { - // // Invalidate the "throw away" board. - // validationBoard = null; - // return false; - // } - // // Assert that this move does not put the moving player in check. - // if (validationBoard.EvaluateCheck(WhoseTurn)) return false; - - // var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - // // The move is valid and legal; update board state. - // if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); - // else PlaceFromBoard(move); - - // // Evaluate check - // InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; - // if (InCheck.HasValue) - // { - // //IsCheckmate = EvaluateCheckmate(); - // } - // return true; - //} - - public bool TryMove(Move move, bool shouldEvaluateCheck = true) + private bool TryMove(Move move) { // Try making the move in a "throw away" board. if (validationBoard == null) { - validationBoard = new ShogiBoard(MoveHistory); + validationBoard = ConstructWithMoves(MoveHistory); } var isValid = move.PieceFromCaptured.HasValue ? validationBoard.PlaceFromHand(move) @@ -101,24 +73,11 @@ namespace Gameboard.ShogiUI.BoardState // Assert that this move does not put the moving player in check. if (validationBoard.EvaluateCheck(WhoseTurn)) return false; - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); - - // Evaluate check - if (shouldEvaluateCheck) - { - InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; - if (InCheck.HasValue) - { - IsCheckmate = EvaluateCheckmate(); - } - } return true; } - - private bool EvaluateCheckmate() { if (!InCheck.HasValue) return false; @@ -129,10 +88,10 @@ namespace Gameboard.ShogiUI.BoardState { if (!isCheckmate) return; // Short circuit - var from = new BoardVector(x, y); + var from = new Vector2(x, y); if (piece.Owner == InCheck) // Owned by the player in check... { - var positionsToCheck = new List(10); + var positionsToCheck = new List(10); IterateMoveSet(from, (innerPiece, position) => { if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... @@ -141,7 +100,8 @@ namespace Gameboard.ShogiUI.BoardState // And evaluate if any move gets the player out of check. foreach (var position in positionsToCheck) { - var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }, false); + if (validationBoard == null) validationBoard = ConstructWithMoves(MoveHistory); + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); if (moveSuccess) { isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); @@ -277,8 +237,7 @@ namespace Gameboard.ShogiUI.BoardState // ...that belongs to the opponent within range... if (piece.Owner != whichPlayer && (piece.IsRanged || Vector2.Distance(kingPosition, v) < 3)) { - Console.WriteLine($"Evaluating {piece.WhichPiece}"); - IterateMoveSet(new BoardVector(x, y), (threatenedPiece, position) => + IterateMoveSet(new Vector2(x, y), (threatenedPiece, position) => { // ...and threatens the player's king. inCheck |= @@ -289,8 +248,7 @@ namespace Gameboard.ShogiUI.BoardState }); return inCheck; } - - private bool ValidateMoveAgainstMoveSet(BoardVector from, BoardVector to) + private bool ValidateMoveAgainstMoveSet(Vector2 from, Vector2 to) { var isValid = false; IterateMoveSet(from, (piece, position) => @@ -306,7 +264,7 @@ namespace Gameboard.ShogiUI.BoardState /// /// Iterate through the possible moves of a piece at a given position. /// - private void IterateMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateMoveSet(Vector2 from, MoveSetCallback callback) { // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. @@ -339,7 +297,7 @@ namespace Gameboard.ShogiUI.BoardState break; } } - private void IterateKingMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateKingMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); @@ -352,7 +310,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.Left, callback); BoardStep(from, direction.Right, callback); } - private void IterateGoldenGeneralMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateGoldenGeneralMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); @@ -363,7 +321,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.Left, callback); BoardStep(from, direction.Right, callback); } - private void IterateSilverGeneralMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateSilverGeneralMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); @@ -380,7 +338,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.DownRight, callback); } } - private void IterateBishopMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateBishopMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); @@ -396,7 +354,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.Down, callback); } } - private void IterateRookMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateRookMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); @@ -412,7 +370,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.DownRight, callback); } } - private void IterateKnightMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateKnightMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece.IsPromoted) @@ -426,7 +384,7 @@ namespace Gameboard.ShogiUI.BoardState BoardStep(from, direction.KnightRight, callback); } } - private void IterateLanceMoveSet(BoardVector from, MoveSetCallback callback) + private void IterateLanceMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece.IsPromoted) @@ -439,7 +397,7 @@ namespace Gameboard.ShogiUI.BoardState BoardWalk(from, direction.Up, callback); } } - private void IteratePawnMoveSet(BoardVector from, MoveSetCallback callback) + private void IteratePawnMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece?.WhichPiece == WhichPiece.Pawn) @@ -459,15 +417,15 @@ namespace Gameboard.ShogiUI.BoardState /// Useful for iterating the board for pieces that move many spaces. /// /// A function that returns true if walking should continue. - private void BoardWalk(BoardVector from, BoardVector direction, MoveSetCallback callback) + private void BoardWalk(Vector2 from, Vector2 direction, MoveSetCallback callback) { var foundAnotherPiece = false; - var to = from.Add(direction); - while (to.IsValidBoardPosition && !foundAnotherPiece) + var to = Vector2.Add(from, direction); + while (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9 && !foundAnotherPiece) { var piece = Board[to.X, to.Y]; callback(piece, to); - to = to.Add(direction); + to = Vector2.Add(to, direction); foundAnotherPiece = piece != null; } } @@ -475,10 +433,10 @@ namespace Gameboard.ShogiUI.BoardState /// /// Useful for iterating the board for pieces that move only one space. /// - private void BoardStep(BoardVector from, BoardVector direction, MoveSetCallback callback) + private void BoardStep(Vector2 from, Vector2 direction, MoveSetCallback callback) { - var to = from.Add(direction); - if (to.IsValidBoardPosition) + var to = Vector2.Add(from, direction); + if (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9) { callback(Board[to.X, to.Y], to); } @@ -540,5 +498,18 @@ namespace Gameboard.ShogiUI.BoardState ResetRearRow(WhichPlayer.Player2); } #endregion + + public static ShogiBoard ConstructWithMoves(IList moves) + { + var s = new ShogiBoard(); + for (var i = 0; i < moves.Count; i++) + { + if (!s.Move(moves[i])) + { + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + } + } + return s; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index db15bfa..1ec37d1 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -52,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers communicationManager.SubscribeToGame(sessionModel, userName); var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); + boardManager.Add(getGameResponse.Session.Name, ShogiBoard.ConstructWithMoves(boardMoves)); var response = new LoadGameResponse(ClientAction.LoadGame) { diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index fcb7274..b61d8d7 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,6 +1,7 @@ using Gameboard.ShogiUI.BoardState; using Microsoft.FSharp.Core; using System; +using System.Numerics; using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Models @@ -77,10 +78,10 @@ namespace Gameboard.ShogiUI.Sockets.Models { return new BoardState.Move { - From = new BoardVector(From.X, From.Y), + From = new Vector2(From.X, From.Y), IsPromotion = IsPromotion, PieceFromCaptured = Enum.TryParse(PieceFromCaptured, out var whichPiece) ? whichPiece : null, - To = new BoardVector(To.X, To.Y) + To = new Vector2(To.X, To.Y) }; } } diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs deleted file mode 100644 index 85d9b99..0000000 --- a/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentAssertions; -using Gameboard.ShogiUI.BoardState; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Gameboard.ShogiUI.UnitTests.BoardState -{ - [TestClass] - public class BoardVectorShould - { - [TestMethod] - public void BeEqualWhenPropertiesAreEqual() - { - var a = new BoardVector(3, 2); - var b = new BoardVector(3, 2); - a.Should().Be(b); - a.GetHashCode().Should().Be(b.GetHashCode()); - (a == b).Should().BeTrue(); - - // Properties should not be transitively equal. - b = new BoardVector(2, 3); - a.Should().NotBe(b); - a.GetHashCode().Should().NotBe(b.GetHashCode()); - (a == b).Should().BeFalse(); - } - } -} diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 8d4a22d..2933154 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -1,8 +1,9 @@ using FluentAssertions; using Gameboard.ShogiUI.BoardState; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Linq; +using System.Numerics; + namespace Gameboard.ShogiUI.UnitTests.BoardState { [TestClass] @@ -70,11 +71,11 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState new Move { // Pawn - From = new BoardVector(0, 2), - To = new BoardVector(0, 3) + From = new Vector2(0, 2), + To = new Vector2(0, 3) } }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); shogi.Board[0, 2].Should().BeNull(); shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); } @@ -86,7 +87,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var shogi = new ShogiBoard(); // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(0, 2), To = new BoardVector(0, 2) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 2), To = new Vector2(0, 2) }); // Assert moveSuccess.Should().BeFalse(); @@ -99,12 +100,12 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var invalidLanceMove = new Move { // Lance moving adjacent - From = new BoardVector(0, 0), - To = new BoardVector(1, 5) + From = new Vector2(0, 0), + To = new Vector2(1, 5) }; var shogi = new ShogiBoard(); - var moveSuccess = shogi.TryMove(invalidLanceMove); + var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); // Assert the Lance has not actually moved. @@ -118,7 +119,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var shogi = new ShogiBoard(); // Act - Move Player2 Pawn when it's Player1 turn. - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 6), To = new BoardVector(8, 5) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 6), To = new Vector2(8, 5) }); // Assert moveSuccess.Should().BeFalse(); @@ -132,12 +133,12 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var invalidLanceMove = new Move { // Lance moving through the pawn before it. - From = new BoardVector(0, 0), - To = new BoardVector(0, 5) + From = new Vector2(0, 0), + To = new Vector2(0, 5) }; var shogi = new ShogiBoard(); - var moveSuccess = shogi.TryMove(invalidLanceMove); + var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); // Assert the Lance has not actually moved. @@ -150,12 +151,12 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var invalidKnightMove = new Move { // Knight capturing allied Pawn - From = new BoardVector(1, 0), - To = new BoardVector(0, 2) + From = new Vector2(1, 0), + To = new Vector2(0, 2) }; var shogi = new ShogiBoard(); - var moveSuccess = shogi.TryMove(invalidKnightMove); + var moveSuccess = shogi.Move(invalidKnightMove); moveSuccess.Should().BeFalse(); // Assert the Knight has not actually moved or captured. @@ -170,20 +171,24 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, // P2 Pawn - new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, // P1 Bishop puts P2 in check - new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) } + new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) } }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); + //foreach(var m in moves) + //{ + // shogi.Move(m); + //} // Prerequisit shogi.InCheck.Should().Be(WhichPlayer.Player2); // Act - P2 moves Lance while remaining in check. - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 8), To = new BoardVector(8, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 8), To = new Vector2(8, 7) }); // Assert moveSuccess.Should().BeFalse(); @@ -199,15 +204,15 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, // P2 Pawn - new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); // Act - P1 Bishop, check - shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) }); + shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }); // Assert shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -220,14 +225,14 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, // P2 Pawn - new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(7, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(7, 7) }); // Assert moveSuccess.Should().BeTrue(); @@ -244,7 +249,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // Act - P2 Silver captures P1 Bishop - moveSuccess = shogi.TryMove(new Move { From = new BoardVector(6, 8), To = new BoardVector(7, 7) }); + moveSuccess = shogi.Move(new Move { From = new Vector2(6, 8), To = new Vector2(7, 7) }); // Assert moveSuccess.Should().BeTrue(); @@ -266,14 +271,14 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, // P2 Pawn - new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); // Act - P1 moves across promote threshold. - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6), IsPromotion = true }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6), IsPromotion = true }); // Assert moveSuccess.Should().BeTrue(); @@ -288,30 +293,30 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Rook - new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, // P2 Gold - new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, // P1 Pawn - new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, // P2 other Gold - new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, // P1 same Pawn - new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, // P2 Pawn - new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, // P1 Pawn takes P2 Pawn - new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, // P2 King - new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, // P1 Pawn promotes - new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, // P2 King retreat - new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; - var shogi = new ShogiBoard(moves); + var shogi = ShogiBoard.ConstructWithMoves(moves); // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); // Assert moveSuccess.Should().BeTrue(); From f55716d2ec906164c95ef6bde76104eea79cc7dd Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 28 Feb 2021 07:52:28 -0600 Subject: [PATCH 06/27] UTests pass with Pathfinder2D --- Benchmarking/Benchmarks.cs | 50 ++- Gameboard.ShogiUI.BoardState/Array2D.cs | 12 +- Gameboard.ShogiUI.BoardState/BoardVector.cs | 37 -- Gameboard.ShogiUI.BoardState/Direction.cs | 18 + .../Gameboard.ShogiUI.BoardState.csproj | 4 + Gameboard.ShogiUI.BoardState/Move.cs | 4 +- Gameboard.ShogiUI.BoardState/Piece.cs | 14 +- Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs | 47 +++ .../Pieces/GoldGeneral.cs | 34 ++ Gameboard.ShogiUI.BoardState/Pieces/King.cs | 35 ++ Gameboard.ShogiUI.BoardState/Pieces/Knight.cs | 33 ++ Gameboard.ShogiUI.BoardState/Pieces/Lance.cs | 32 ++ Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs | 32 ++ Gameboard.ShogiUI.BoardState/Pieces/Rook.cs | 44 +++ .../Pieces/SilverGeneral.cs | 35 ++ Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 318 +++++------------- .../ClientActionHandlers/LoadGameHandler.cs | 2 +- .../BoardState/ShogiBoardShould.cs | 25 +- .../PathFinding/PathFinder2DShould.cs | 46 +++ PathFinding/Enums.cs | 19 ++ PathFinding/IPlanarCollection.cs | 7 +- PathFinding/IPlanarElement.cs | 12 + PathFinding/Path.cs | 15 + PathFinding/PathFinder2D.cs | 106 +++++- 24 files changed, 687 insertions(+), 294 deletions(-) delete mode 100644 Gameboard.ShogiUI.BoardState/BoardVector.cs create mode 100644 Gameboard.ShogiUI.BoardState/Direction.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/King.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Knight.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Lance.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Rook.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs create mode 100644 Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs create mode 100644 PathFinding/Enums.cs create mode 100644 PathFinding/IPlanarElement.cs create mode 100644 PathFinding/Path.cs diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 5a7947f..a972747 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -1,7 +1,9 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; using Gameboard.ShogiUI.BoardState; using System; +using System.Linq; using System.Numerics; namespace Benchmarking @@ -9,6 +11,8 @@ namespace Benchmarking public class Benchmarks { private readonly Move[] moves; + private readonly Vector2[] directions; + private readonly Consumer consumer = new Consumer(); public Benchmarks() { @@ -35,9 +39,13 @@ namespace Benchmarking // P2 King retreat new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; + var rand = new Random(); + + directions = new Vector2[10]; + for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); } - [Benchmark] + //[Benchmark] public void One() { var board = new ShogiBoard(); @@ -47,7 +55,7 @@ namespace Benchmarking } } - [Benchmark] + //[Benchmark] public void Two() { var board = new ShogiBoard(); @@ -57,15 +65,49 @@ namespace Benchmarking } } + + + public Vector2 FindDirection(Vector2[] directions, Vector2 destination) + { + var smallerDistance = float.MaxValue; + Vector2 found = Vector2.Zero; + foreach (var d in directions) + { + var distance = Vector2.Distance(d, destination); + if (distance < smallerDistance) + { + smallerDistance = distance; + found = d; + } + } + return found; + } + + + public Vector2 FindDirectionLinq(Vector2[] directions, Vector2 destination) => + directions.Aggregate((a, b) => Vector2.Distance(destination, a) < Vector2.Distance(destination, b) ? a : b); + + + + + [Benchmark] + public void Directions_A() + { + FindDirection(directions, new Vector2(8, 7)); + } + [Benchmark] + public void Directions_B() + { + FindDirectionLinq(directions, new Vector2(8, 7)); + } } public class Program { public static void Main(string[] args) { - //BenchmarkRunner.Run(); + BenchmarkRunner.Run(); Console.WriteLine("Done"); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs index 8e46f77..6487646 100644 --- a/Gameboard.ShogiUI.BoardState/Array2D.cs +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -1,11 +1,12 @@ -using System; +using PathFinding; +using System; using System.Collections; using System.Collections.Generic; using System.Numerics; namespace Gameboard.ShogiUI.BoardState { - public class Array2D : IEnumerable + public class Array2D : IPlanarCollection, IEnumerable { /// False to stop iterating. public delegate void ForEachDelegate(T element, int x, int y); @@ -31,6 +32,13 @@ namespace Gameboard.ShogiUI.BoardState set => array[(int)y * width + (int)x] = value; } + public int GetLength(int dimension) => dimension switch + { + 0 => width, + 1 => height, + _ => throw new IndexOutOfRangeException() + }; + public void ForEach(ForEachDelegate callback) { for (var x = 0; x < width; x++) diff --git a/Gameboard.ShogiUI.BoardState/BoardVector.cs b/Gameboard.ShogiUI.BoardState/BoardVector.cs deleted file mode 100644 index 8f550d3..0000000 --- a/Gameboard.ShogiUI.BoardState/BoardVector.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Numerics; - -namespace Gameboard.ShogiUI.BoardState -{ - /// - /// Provides normalized Vector2s relative to player. - /// "Up" for player 1 is "Down" for player 2; that sort of thing. - /// - public class Direction - { - private static readonly Vector2 PositiveX = new Vector2(1, 0); - private static readonly Vector2 NegativeX = new Vector2(-1, 0); - private static readonly Vector2 PositiveY = new Vector2(0, 1); - private static readonly Vector2 NegativeY = new Vector2(0, -1); - private static readonly Vector2 PositiveYX = new Vector2(1, 1); - private static readonly Vector2 NegativeYX = new Vector2(-1, -1); - private static readonly Vector2 NegativeYPositiveX = new Vector2(1, -1); - private static readonly Vector2 PositiveYNegativeX = new Vector2(-1, 1); - - private readonly WhichPlayer whichPlayer; - public Direction(WhichPlayer whichPlayer) - { - this.whichPlayer = whichPlayer; - } - - public Vector2 Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; - public Vector2 Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; - public Vector2 Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; - public Vector2 Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; - public Vector2 UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; - public Vector2 UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; - public Vector2 DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; - public Vector2 DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; - public Vector2 KnightLeft => whichPlayer == WhichPlayer.Player1 ? new Vector2(-1, 2) : new Vector2(1, -2); - public Vector2 KnightRight => whichPlayer == WhichPlayer.Player1 ? new Vector2(1, 2) : new Vector2(-1, -2); - } -} diff --git a/Gameboard.ShogiUI.BoardState/Direction.cs b/Gameboard.ShogiUI.BoardState/Direction.cs new file mode 100644 index 0000000..5f1307c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Direction.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState +{ + public static class Direction + { + public static readonly Vector2 Up = new Vector2(0, 1); + public static readonly Vector2 Down = new Vector2(0, -1); + public static readonly Vector2 Left = new Vector2(-1, 0); + public static readonly Vector2 Right = new Vector2(1, 0); + public static readonly Vector2 UpLeft = new Vector2(1, 1); + public static readonly Vector2 UpRight = new Vector2(-1, 1); + public static readonly Vector2 DownLeft = new Vector2(-1, -1); + public static readonly Vector2 DownRight = new Vector2(1, -1); + public static readonly Vector2 KnightLeft = new Vector2(-1, 2); + public static readonly Vector2 KnightRight = new Vector2(1, 2); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj index f208d30..e1f728c 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs index 9df0f89..fc8f860 100644 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -1,7 +1,9 @@ -using System.Numerics; +using System.Diagnostics; +using System.Numerics; namespace Gameboard.ShogiUI.BoardState { + [DebuggerDisplay("{From} - {To}")] public class Move { public WhichPiece? PieceFromCaptured { get; set; } diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Piece.cs index 99b218d..99bca18 100644 --- a/Gameboard.ShogiUI.BoardState/Piece.cs +++ b/Gameboard.ShogiUI.BoardState/Piece.cs @@ -1,9 +1,11 @@ -using System.Diagnostics; +using PathFinding; +using System.Collections.Generic; +using System.Diagnostics; namespace Gameboard.ShogiUI.BoardState { [DebuggerDisplay("{WhichPiece} {Owner}")] - public class Piece + public abstract class Piece : IPlanarElement { public WhichPiece WhichPiece { get; } public WhichPlayer Owner { get; private set; } @@ -48,7 +50,7 @@ namespace Gameboard.ShogiUI.BoardState : WhichPlayer.Player1; } - public void Promote() => IsPromoted = true; + public void Promote() => IsPromoted = CanPromote; public void Demote() => IsPromoted = false; @@ -57,5 +59,11 @@ namespace Gameboard.ShogiUI.BoardState ToggleOwnership(); Demote(); } + + public abstract ICollection GetPaths(); + + public abstract Piece DeepClone(); + + public bool IsUpsideDown => Owner == WhichPlayer.Player2; } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs new file mode 100644 index 0000000..cf73954 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs @@ -0,0 +1,47 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Bishop : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }; + private static readonly List PromotedMoveSet = new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }; + public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) + { + // TODO: If this strat works out, we can do away with the Direction class entirely. + PromotedMoveSet.AddRange(MoveSet); + } + + public override Piece DeepClone() + { + var clone = new Bishop(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs new file mode 100644 index 0000000..cd22d62 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs @@ -0,0 +1,34 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class GoldenGeneral : Piece + { + public static readonly List MoveSet = new List(6) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down) + }; + public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) + { + } + + public override Piece DeepClone() + { + var clone = new GoldenGeneral(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() => Owner == WhichPlayer.Player1 + ? MoveSet + : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.BoardState/Pieces/King.cs new file mode 100644 index 0000000..76251e4 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/King.cs @@ -0,0 +1,35 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class King : Piece + { + private static readonly List MoveSet = new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public King(WhichPlayer owner) : base(WhichPiece.King, owner) + { + } + + public override Piece DeepClone() + { + var clone = new King(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + public override ICollection GetPaths() => Owner == WhichPlayer.Player1 + ? MoveSet + : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs new file mode 100644 index 0000000..7e4a4df --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs @@ -0,0 +1,33 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Knight : Piece + { + private static readonly List MoveSet = new List(2) + { + new Path(Direction.KnightLeft), + new Path(Direction.KnightRight) + }; + + public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Knight(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs new file mode 100644 index 0000000..a9f2df7 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs @@ -0,0 +1,32 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Lance : Piece + { + private static readonly List MoveSet = new List(1) + { + new Path(Direction.Up, Distance.MultiStep), + }; + + public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Lance(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs new file mode 100644 index 0000000..bce9f14 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs @@ -0,0 +1,32 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Pawn : Piece + { + private static readonly List MoveSet = new List(1) + { + new Path(Direction.Up) + }; + + public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Pawn(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs new file mode 100644 index 0000000..dfedd94 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs @@ -0,0 +1,44 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Rook : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep) + }; + private static readonly List PromotedMoveSet = new List(8) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Rook(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs new file mode 100644 index 0000000..ef0bcbd --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs @@ -0,0 +1,35 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class SilverGeneral : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) + { + } + + public override Piece DeepClone() + { + var clone = new SilverGeneral(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 0bd9819..f11c980 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -1,4 +1,6 @@ -using System; +using Gameboard.ShogiUI.BoardState.Pieces; +using PathFinding; +using System; using System.Collections.Generic; using System.Numerics; using System.Text; @@ -16,6 +18,7 @@ namespace Gameboard.ShogiUI.BoardState private ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; + private PathFinder2D pathFinder; public IReadOnlyDictionary> Hands { get; } public Array2D Board { get; } public List MoveHistory { get; } @@ -31,17 +34,51 @@ namespace Gameboard.ShogiUI.BoardState { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; + pathFinder = new PathFinder2D(Board); InitializeBoardState(); player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); } + public ShogiBoard(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!Move(moves[i])) + { + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + } + } + } + + private ShogiBoard(ShogiBoard toCopy) + { + Board = new Array2D(9, 9); + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + Board[x, y] = toCopy.Board[x, y]?.DeepClone(); + + pathFinder = new PathFinder2D(Board); + MoveHistory = new List(toCopy.MoveHistory); + Hands = new Dictionary> + { + { WhichPlayer.Player1, new List(toCopy.Hands[WhichPlayer.Player1]) }, + { WhichPlayer.Player2, new List(toCopy.Hands[WhichPlayer.Player2]) } + }; + player1King = toCopy.player1King; + player2King = toCopy.player2King; + } + public bool Move(Move move) { + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); - if (!moveSuccess) return false; + if (!moveSuccess) + { + return false; + } // Evaluate check InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; @@ -59,8 +96,9 @@ namespace Gameboard.ShogiUI.BoardState // Try making the move in a "throw away" board. if (validationBoard == null) { - validationBoard = ConstructWithMoves(MoveHistory); + validationBoard = new ShogiBoard(this); } + var isValid = move.PieceFromCaptured.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); @@ -92,15 +130,15 @@ namespace Gameboard.ShogiUI.BoardState if (piece.Owner == InCheck) // Owned by the player in check... { var positionsToCheck = new List(10); - IterateMoveSet(from, (innerPiece, position) => - { - if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... - }); + //IterateMoveSet(from, (innerPiece, position) => + //{ + // if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... + //}); // And evaluate if any move gets the player out of check. foreach (var position in positionsToCheck) { - if (validationBoard == null) validationBoard = ConstructWithMoves(MoveHistory); + if (validationBoard == null) validationBoard = new ShogiBoard(this); var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); if (moveSuccess) { @@ -148,7 +186,7 @@ namespace Gameboard.ShogiUI.BoardState var fromPiece = Board[move.From.X, move.From.Y]; if (fromPiece == null) return false; // Invalid move if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. - if (ValidateMoveAgainstMoveSet(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. + if (IsPathable(move.From, move.To, fromPiece) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -188,6 +226,17 @@ namespace Gameboard.ShogiUI.BoardState MoveHistory.Add(move); return true; } + + private bool IsPathable(Vector2 from, Vector2 to, Piece piece) + { + var isObstructed = false; + var isPathable = pathFinder.PathTo(piece, from, to, (other, position) => + { + if (other.Owner == piece.Owner) isObstructed = true; + }); + return !isObstructed && isPathable; + } + public void PrintStateAsAscii() { var builder = new StringBuilder(); @@ -228,219 +277,34 @@ namespace Gameboard.ShogiUI.BoardState /// private bool EvaluateCheck(WhichPlayer whichPlayer) { - var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; + var destination = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; var inCheck = false; // Iterate every board piece... Board.ForEachNotNull((piece, x, y) => { - var v = new Vector2(x, y); + var origin = new Vector2(x, y); // ...that belongs to the opponent within range... - if (piece.Owner != whichPlayer && (piece.IsRanged || Vector2.Distance(kingPosition, v) < 3)) + if (piece.Owner != whichPlayer && pathFinder.IsPathable(origin, destination, piece)) { - IterateMoveSet(new Vector2(x, y), (threatenedPiece, position) => + pathFinder.PathTo(piece, origin, destination, (threatenedPiece, position) => { // ...and threatens the player's king. inCheck |= - threatenedPiece?.WhichPiece == WhichPiece.King - && threatenedPiece?.Owner == whichPlayer; + threatenedPiece.WhichPiece == WhichPiece.King + && threatenedPiece.Owner == whichPlayer; }); } }); return inCheck; } - private bool ValidateMoveAgainstMoveSet(Vector2 from, Vector2 to) - { - var isValid = false; - IterateMoveSet(from, (piece, position) => - { - if (piece?.Owner != WhoseTurn && position == to) - { - isValid = true; - } - }); - - return isValid; - } /// /// Iterate through the possible moves of a piece at a given position. /// - private void IterateMoveSet(Vector2 from, MoveSetCallback callback) - { - // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. - // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. - var piece = Board[from.X, from.Y]; - switch (piece?.WhichPiece) - { - case WhichPiece.King: - IterateKingMoveSet(from, callback); - break; - case WhichPiece.GoldenGeneral: - IterateGoldenGeneralMoveSet(from, callback); - break; - case WhichPiece.SilverGeneral: - IterateSilverGeneralMoveSet(from, callback); - break; - case WhichPiece.Bishop: - IterateBishopMoveSet(from, callback); - break; - case WhichPiece.Rook: - IterateRookMoveSet(from, callback); - break; - case WhichPiece.Knight: - IterateKnightMoveSet(from, callback); - break; - case WhichPiece.Lance: - IterateLanceMoveSet(from, callback); - break; - case WhichPiece.Pawn: - IteratePawnMoveSet(from, callback); - break; - } - } - private void IterateKingMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.Down, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - } - private void IterateGoldenGeneralMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.Down, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - } - private void IterateSilverGeneralMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - } - } - private void IterateBishopMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.UpLeft, callback); - BoardWalk(from, direction.UpRight, callback); - BoardWalk(from, direction.DownLeft, callback); - BoardWalk(from, direction.DownRight, callback); - if (piece.IsPromoted) - { - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - BoardStep(from, direction.Down, callback); - } - } - private void IterateRookMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.Up, callback); - BoardWalk(from, direction.Left, callback); - BoardWalk(from, direction.Right, callback); - BoardWalk(from, direction.Down, callback); - if (piece.IsPromoted) - { - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - } - } - private void IterateKnightMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardStep(from, direction.KnightLeft, callback); - BoardStep(from, direction.KnightRight, callback); - } - } - private void IterateLanceMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.Up, callback); - } - } - private void IteratePawnMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece?.WhichPiece == WhichPiece.Pawn) - { - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - } - } - } + /// /// Useful for iterating the board for pieces that move many spaces. /// /// A function that returns true if walking should continue. - private void BoardWalk(Vector2 from, Vector2 direction, MoveSetCallback callback) - { - var foundAnotherPiece = false; - var to = Vector2.Add(from, direction); - while (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9 && !foundAnotherPiece) - { - var piece = Board[to.X, to.Y]; - callback(piece, to); - to = Vector2.Add(to, direction); - foundAnotherPiece = piece != null; - } - } - - /// - /// Useful for iterating the board for pieces that move only one space. - /// - private void BoardStep(Vector2 from, Vector2 direction, MoveSetCallback callback) - { - var to = Vector2.Add(from, direction); - if (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9) - { - callback(Board[to.X, to.Y], to); - } - } #endregion #region Initialize @@ -453,7 +317,7 @@ namespace Gameboard.ShogiUI.BoardState private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 2 : 6; - for (int x = 0; x < 9; x++) Board[x, y] = new Piece(WhichPiece.Pawn, player); + for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); } private void ResetMiddleRow(WhichPlayer player) { @@ -464,28 +328,28 @@ namespace Gameboard.ShogiUI.BoardState Board[8, y] = null; if (player == WhichPlayer.Player1) { - Board[1, y] = new Piece(WhichPiece.Bishop, player); - Board[7, y] = new Piece(WhichPiece.Rook, player); + Board[1, y] = new Bishop(player); + Board[7, y] = new Rook(player); } else { - Board[1, y] = new Piece(WhichPiece.Rook, player); - Board[7, y] = new Piece(WhichPiece.Bishop, player); + Board[1, y] = new Rook(player); + Board[7, y] = new Bishop(player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 0 : 8; - Board[0, y] = new Piece(WhichPiece.Lance, player); - Board[1, y] = new Piece(WhichPiece.Knight, player); - Board[2, y] = new Piece(WhichPiece.SilverGeneral, player); - Board[3, y] = new Piece(WhichPiece.GoldenGeneral, player); - Board[4, y] = new Piece(WhichPiece.King, player); - Board[5, y] = new Piece(WhichPiece.GoldenGeneral, player); - Board[6, y] = new Piece(WhichPiece.SilverGeneral, player); - Board[7, y] = new Piece(WhichPiece.Knight, player); - Board[8, y] = new Piece(WhichPiece.Lance, player); + Board[0, y] = new Lance(player); + Board[1, y] = new Knight(player); + Board[2, y] = new SilverGeneral(player); + Board[3, y] = new GoldenGeneral(player); + Board[4, y] = new King(player); + Board[5, y] = new GoldenGeneral(player); + Board[6, y] = new SilverGeneral(player); + Board[7, y] = new Knight(player); + Board[8, y] = new Lance(player); } private void InitializeBoardState() { @@ -499,17 +363,17 @@ namespace Gameboard.ShogiUI.BoardState } #endregion - public static ShogiBoard ConstructWithMoves(IList moves) - { - var s = new ShogiBoard(); - for (var i = 0; i < moves.Count; i++) - { - if (!s.Move(moves[i])) - { - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); - } - } - return s; - } + //public static ShogiBoard ConstructWithMoves(IList moves) + //{ + // var s = new ShogiBoard(); + // for (var i = 0; i < moves.Count; i++) + // { + // if (!s.Move(moves[i])) + // { + // throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + // } + // } + // return s; + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 1ec37d1..db15bfa 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -52,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers communicationManager.SubscribeToGame(sessionModel, userName); var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - boardManager.Add(getGameResponse.Session.Name, ShogiBoard.ConstructWithMoves(boardMoves)); + boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); var response = new LoadGameResponse(ClientAction.LoadGame) { diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 2933154..4eaf634 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -75,7 +75,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState To = new Vector2(0, 3) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); shogi.Board[0, 2].Should().BeNull(); shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); } @@ -99,9 +99,9 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState { var invalidLanceMove = new Move { - // Lance moving adjacent - From = new Vector2(0, 0), - To = new Vector2(1, 5) + // Bishop moving lateral + From = new Vector2(1, 1), + To = new Vector2(2, 1) }; var shogi = new ShogiBoard(); @@ -177,16 +177,12 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P1 Bishop puts P2 in check new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); - //foreach(var m in moves) - //{ - // shogi.Move(m); - //} + //var shogi = new ShogiBoard(moves); + var shogi = new ShogiBoard(moves); // Prerequisit shogi.InCheck.Should().Be(WhichPlayer.Player2); - // Act - P2 moves Lance while remaining in check. var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 8), To = new Vector2(8, 7) }); @@ -208,7 +204,8 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); + shogi.PrintStateAsAscii(); // Act - P1 Bishop, check @@ -229,7 +226,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 Bishop captures P2 Bishop var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(7, 7) }); @@ -275,7 +272,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 moves across promote threshold. var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6), IsPromotion = true }); @@ -313,7 +310,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 King retreat new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 Pawn wins by checkmate. var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs new file mode 100644 index 0000000..98782e5 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PathFinding; +using System.Collections.Generic; +using System.Numerics; + +namespace Gameboard.ShogiUI.UnitTests.PathFinding +{ + [TestClass] + public class PathFinder2DShould + { + class TestElement : IPlanarElement + { + public ICollection GetPaths() => throw new System.NotImplementedException(); + public bool IsUpsideDown => false; + } + + [TestMethod] + public void Maths() + { + var finder = new PathFinder2D(new Array2D(5, 5)); + + var result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(3, 3) + ); + result.Should().BeTrue(); + + result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(2, 2) + ); + result.Should().BeFalse(); + + result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(-1, 1) + ); + result.Should().BeFalse(); + } + } +} diff --git a/PathFinding/Enums.cs b/PathFinding/Enums.cs new file mode 100644 index 0000000..fab3f18 --- /dev/null +++ b/PathFinding/Enums.cs @@ -0,0 +1,19 @@ +namespace PathFinding +{ + public enum HaltCondition + { + /// + /// Do not stop until you reach the collection boundary. + /// + None, + /// + /// Halt after encountering a non-null element. + /// + AfterCollide + } +} +public enum Distance +{ + OneStep, + MultiStep +} diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 8a2b046..3ccf800 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -1,7 +1,10 @@ -namespace PathFinding +using System.Collections.Generic; + +namespace PathFinding { - interface IPlanarCollection + public interface IPlanarCollection : IEnumerable { T this[int x, int y] { get; set; } + int GetLength(int dimension); } } diff --git a/PathFinding/IPlanarElement.cs b/PathFinding/IPlanarElement.cs new file mode 100644 index 0000000..4983bb4 --- /dev/null +++ b/PathFinding/IPlanarElement.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; + +namespace PathFinding +{ + public interface IPlanarElement + { + ICollection GetPaths(); + + bool IsUpsideDown { get; } + } +} diff --git a/PathFinding/Path.cs b/PathFinding/Path.cs new file mode 100644 index 0000000..dd3edaf --- /dev/null +++ b/PathFinding/Path.cs @@ -0,0 +1,15 @@ +using System.Numerics; + +namespace PathFinding +{ + public class Path + { + public Vector2 Direction { get; } + public Distance Distance { get; } + public Path(Vector2 direction, Distance distance = Distance.OneStep) + { + Direction = direction; + Distance = distance; + } + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 93b9bc0..9aa51e7 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,10 +1,110 @@ - +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + namespace PathFinding { - public class PathFinder2D + public class PathFinder2D where T : IPlanarElement { - public PathFinder2D() + /// + /// + /// Guaranteed to be non-null. + /// + public delegate void Callback(T collider, Vector2 position); + + + private readonly IPlanarCollection collection; + private readonly int width; + private readonly int height; + public PathFinder2D(IPlanarCollection collection) { + this.collection = collection; + width = collection.GetLength(0); + height = collection.GetLength(1); + } + + /// + /// Navigate the collection such that each "step" is always towards the destination. + /// + /// The pathing element. + /// The starting location. + /// The destination. + /// Do cool stuff here. + /// True if the element reached the destination. + public bool PathTo(T element, Vector2 origin, Vector2 destination, Callback callback) + { + if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) + { + return false; + } + + var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); + var next = Vector2.Add(origin, path.Direction); + + if (!IsPathable(origin, destination, next)) + { + // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. + return false; + } + + var shouldPath = true; + while (shouldPath) + { + var collider = collection[(int)next.X, (int)next.Y]; + if (collider != null) callback(collider, next); + if (next == destination) return true; + if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + next = Vector2.Add(next, path.Direction); + } + return true; + } + + public void PathEvery(IPlanarElement element, Vector2 from, Callback callback) + { + foreach (var path in element.GetPaths()) + { + var shouldPath = true; + var next = Vector2.Add(from, path.Direction); + while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) + { + var collider = collection[(int)next.X, (int)next.Y]; + if (collider != null) + { + callback(collider, next); + } + next = Vector2.Add(from, path.Direction); + if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + } + } + } + + public Path FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => + paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); + + + public bool IsPathable(Vector2 origin, Vector2 destination, T element) + { + var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); + var next = Vector2.Add(origin, path.Direction); + return IsPathable(origin, destination, next); + } + public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 next) + { + if (Vector2.Distance(next, destination) < Vector2.Distance(origin, destination)) + { + // y = mx + b + // b = -mx + y + var slope = (destination.Y - origin.Y) / (destination.X - origin.X); + var yIntercept = -(slope * origin.X) + origin.Y; + return float.IsInfinity(slope) || next.Y == slope * next.X + yIntercept; + } + return false; } } } From 3da187be178c1bc41b092ce889c2191bcdf12638 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 1 Mar 2021 20:55:37 -0600 Subject: [PATCH 07/27] yep --- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 183 ++++++++++-------- .../BoardState/ShogiBoardShould.cs | 1 - .../PathFinding/PathFinder2DShould.cs | 4 +- PathFinding/IPlanarCollection.cs | 2 +- PathFinding/PathFinder2D.cs | 73 ++++--- 5 files changed, 154 insertions(+), 109 deletions(-) diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index f11c980..9c16fd8 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -44,11 +44,15 @@ namespace Gameboard.ShogiUI.BoardState { for (var i = 0; i < moves.Count; i++) { - if (!Move(moves[i])) + if (!TryMove(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } + if (EvaluateCheckAfterMove(WhoseTurn)) + { + InCheck = WhoseTurn; + } } private ShogiBoard(ShogiBoard toCopy) @@ -72,7 +76,6 @@ namespace Gameboard.ShogiUI.BoardState public bool Move(Move move) { - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) @@ -81,10 +84,13 @@ namespace Gameboard.ShogiUI.BoardState } // Evaluate check - InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; - if (InCheck.HasValue) + if (EvaluateCheckAfterMove(WhoseTurn)) { - //IsCheckmate = EvaluateCheckmate(); + InCheck = WhoseTurn; + if (InCheck.HasValue) + { + //IsCheckmate = EvaluateCheckmate(); + } } return true; } @@ -109,47 +115,17 @@ namespace Gameboard.ShogiUI.BoardState return false; } // Assert that this move does not put the moving player in check. - if (validationBoard.EvaluateCheck(WhoseTurn)) return false; + if (validationBoard.EvaluateCheckAfterMove(WhoseTurn)) + { + // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + return false; + } // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } - private bool EvaluateCheckmate() - { - if (!InCheck.HasValue) return false; - - // Assume true and try to disprove. - var isCheckmate = true; - Board.ForEachNotNull((piece, x, y) => // For each piece... - { - if (!isCheckmate) return; // Short circuit - - var from = new Vector2(x, y); - if (piece.Owner == InCheck) // Owned by the player in check... - { - var positionsToCheck = new List(10); - //IterateMoveSet(from, (innerPiece, position) => - //{ - // if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... - //}); - - // And evaluate if any move gets the player out of check. - foreach (var position in positionsToCheck) - { - if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); - if (moveSuccess) - { - isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); - validationBoard = null; - } - } - } - }); - return isCheckmate; - } /// True if the move was successful. private bool PlaceFromHand(Move move) { @@ -186,7 +162,7 @@ namespace Gameboard.ShogiUI.BoardState var fromPiece = Board[move.From.X, move.From.Y]; if (fromPiece == null) return false; // Invalid move if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. - if (IsPathable(move.From, move.To, fromPiece) == false) return false; // Invalid move; move not part of move-set. + if (IsPathable(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -227,10 +203,13 @@ namespace Gameboard.ShogiUI.BoardState return true; } - private bool IsPathable(Vector2 from, Vector2 to, Piece piece) + private bool IsPathable(Vector2 from, Vector2 to) { + var piece = Board[from.X, from.Y]; + if (piece == null) return false; + var isObstructed = false; - var isPathable = pathFinder.PathTo(piece, from, to, (other, position) => + var isPathable = pathFinder.PathTo(from, to, (other, position) => { if (other.Owner == piece.Owner) isObstructed = true; }); @@ -272,39 +251,92 @@ namespace Gameboard.ShogiUI.BoardState Console.WriteLine(builder.ToString()); } #region Rules Validation - /// - /// Evaluate if a player is in check given the current board state. - /// - private bool EvaluateCheck(WhichPlayer whichPlayer) + private bool EvaluateCheckAfterMove(WhichPlayer whichPlayer) { - var destination = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; - var inCheck = false; - // Iterate every board piece... - Board.ForEachNotNull((piece, x, y) => + var isCheck = false; + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; + + // Get last move. + var move = MoveHistory[^1]; + // Check if the move put the king in check. + if (pathFinder.PathTo(move.To, kingPosition)) return true; + + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, move.From); + var slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) { - var origin = new Vector2(x, y); - // ...that belongs to the opponent within range... - if (piece.Owner != whichPlayer && pathFinder.IsPathable(origin, destination, piece)) + // if slope of the move is also infinity...can skip this? + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { - pathFinder.PathTo(piece, origin, destination, (threatenedPiece, position) => + if (piece.Owner != whichPlayer) { - // ...and threatens the player's king. - inCheck |= - threatenedPiece.WhichPiece == WhichPiece.King - && threatenedPiece.Owner == whichPlayer; - }); + switch (piece.WhichPiece) + { + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; + } + } + }); + } + else if (slope == 1) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); + } + + return isCheck; + } + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; + + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, x, y) => // For each piece... + { + // Short circuit + if (!isCheckmate) return; + + var from = new Vector2(x, y); + if (piece.Owner == InCheck) // ...owned by the player in check... + { + // ...evaluate if any move gets the player out of check. + pathFinder.PathEvery(from, (other, position) => + { + if (validationBoard == null) validationBoard = new ShogiBoard(this); + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + if (moveSuccess) + { + isCheckmate = false; + } + }); } }); - return inCheck; + return isCheckmate; } - /// - /// Iterate through the possible moves of a piece at a given position. - /// - - /// - /// Useful for iterating the board for pieces that move many spaces. - /// - /// A function that returns true if walking should continue. #endregion #region Initialize @@ -362,18 +394,5 @@ namespace Gameboard.ShogiUI.BoardState ResetRearRow(WhichPlayer.Player2); } #endregion - - //public static ShogiBoard ConstructWithMoves(IList moves) - //{ - // var s = new ShogiBoard(); - // for (var i = 0; i < moves.Count; i++) - // { - // if (!s.Move(moves[i])) - // { - // throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); - // } - // } - // return s; - //} } } diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 4eaf634..ae234f1 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -177,7 +177,6 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P1 Bishop puts P2 in check new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) } }; - //var shogi = new ShogiBoard(moves); var shogi = new ShogiBoard(moves); // Prerequisit diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs index 98782e5..935051e 100644 --- a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -24,14 +24,14 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding var result = finder.IsPathable( new Vector2(2, 2), new Vector2(7, 7), - new Vector2(3, 3) + new Vector2(1, 1) ); result.Should().BeTrue(); result = finder.IsPathable( new Vector2(2, 2), new Vector2(7, 7), - new Vector2(2, 2) + new Vector2(0, 0) ); result.Should().BeFalse(); diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 3ccf800..95506e1 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -4,7 +4,7 @@ namespace PathFinding { public interface IPlanarCollection : IEnumerable { - T this[int x, int y] { get; set; } + T this[float x, float y] { get; set; } int GetLength(int dimension); } } diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 9aa51e7..5b5c1e3 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -24,46 +25,50 @@ namespace PathFinding } /// - /// Navigate the collection such that each "step" is always towards the destination. + /// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin. /// /// The pathing element. /// The starting location. /// The destination. /// Do cool stuff here. /// True if the element reached the destination. - public bool PathTo(T element, Vector2 origin, Vector2 destination, Callback callback) + public bool PathTo(Vector2 origin, Vector2 destination, Callback callback = null) { if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) { return false; } - + var element = collection[origin.X, origin.Y]; var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); - var next = Vector2.Add(origin, path.Direction); - if (!IsPathable(origin, destination, next)) + if (!IsPathable(origin, destination, path.Direction)) { // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. return false; } var shouldPath = true; - while (shouldPath) + var next = origin; + while (shouldPath && next != destination) { + next = Vector2.Add(next, path.Direction); var collider = collection[(int)next.X, (int)next.Y]; - if (collider != null) callback(collider, next); - if (next == destination) return true; - if (path.Distance == Distance.OneStep) + if (collider != null) + { + callback?.Invoke(collider, next); + shouldPath = false; + } + else if (path.Distance == Distance.OneStep) { shouldPath = false; } - next = Vector2.Add(next, path.Direction); } - return true; + return next == destination; } - public void PathEvery(IPlanarElement element, Vector2 from, Callback callback) + public void PathEvery(Vector2 from, Callback callback) { + var element = collection[from.X, from.Y]; foreach (var path in element.GetPaths()) { var shouldPath = true; @@ -84,6 +89,22 @@ namespace PathFinding } } + /// + /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. + /// + public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback) + { + direction = Vector2.Normalize(direction); + + var next = Vector2.Add(origin, direction); + while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) + { + var element = collection[next.X, next.Y]; + if (element != null) callback(element, next); + next = Vector2.Add(next, direction); + } + } + public Path FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); @@ -91,20 +112,26 @@ namespace PathFinding public bool IsPathable(Vector2 origin, Vector2 destination, T element) { var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); - var next = Vector2.Add(origin, path.Direction); - return IsPathable(origin, destination, next); + return IsPathable(origin, destination, path.Direction); } - public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 next) + public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) { - if (Vector2.Distance(next, destination) < Vector2.Distance(origin, destination)) + direction = Vector2.Normalize(direction); + var next = Vector2.Add(origin, direction); + if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; + + var slope = (destination.Y - origin.Y) / (destination.X - origin.X); + if (float.IsInfinity(slope)) { - // y = mx + b - // b = -mx + y - var slope = (destination.Y - origin.Y) / (destination.X - origin.X); - var yIntercept = -(slope * origin.X) + origin.Y; - return float.IsInfinity(slope) || next.Y == slope * next.X + yIntercept; + return next.X == destination.X; + } + else + { + // b = -mx + y + var yIntercept = -slope * origin.X + origin.Y; + // y = mx + b + return next.Y == slope * next.X + yIntercept; } - return false; } } } From 2e976c01e9ade54e747c25b2f8502f66465e83d3 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 2 Mar 2021 19:08:10 -0600 Subject: [PATCH 08/27] it fricken works! --- Gameboard.ShogiUI.BoardState/Direction.cs | 4 +- Gameboard.ShogiUI.BoardState/Pieces/King.cs | 5 +-- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 45 +++++++++---------- .../BoardState/ShogiBoardShould.cs | 10 ++++- PathFinding/Path.cs | 4 +- PathFinding/PathFinder2D.cs | 5 ++- 6 files changed, 41 insertions(+), 32 deletions(-) diff --git a/Gameboard.ShogiUI.BoardState/Direction.cs b/Gameboard.ShogiUI.BoardState/Direction.cs index 5f1307c..d474f79 100644 --- a/Gameboard.ShogiUI.BoardState/Direction.cs +++ b/Gameboard.ShogiUI.BoardState/Direction.cs @@ -8,8 +8,8 @@ namespace Gameboard.ShogiUI.BoardState public static readonly Vector2 Down = new Vector2(0, -1); public static readonly Vector2 Left = new Vector2(-1, 0); public static readonly Vector2 Right = new Vector2(1, 0); - public static readonly Vector2 UpLeft = new Vector2(1, 1); - public static readonly Vector2 UpRight = new Vector2(-1, 1); + public static readonly Vector2 UpLeft = new Vector2(-1, 1); + public static readonly Vector2 UpRight = new Vector2(1, 1); public static readonly Vector2 DownLeft = new Vector2(-1, -1); public static readonly Vector2 DownRight = new Vector2(1, -1); public static readonly Vector2 KnightLeft = new Vector2(-1, 2); diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.BoardState/Pieces/King.cs index 76251e4..e2d4ebd 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/King.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/King.cs @@ -28,8 +28,7 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - public override ICollection GetPaths() => Owner == WhichPlayer.Player1 - ? MoveSet - : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + // The move set for a King is the same regardless of orientation. + public override ICollection GetPaths() => MoveSet; } } diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 9c16fd8..43da70b 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -15,10 +15,10 @@ namespace Gameboard.ShogiUI.BoardState public class ShogiBoard { private delegate void MoveSetCallback(Piece piece, Vector2 position); - private ShogiBoard validationBoard; + private readonly PathFinder2D pathFinder; + public ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; - private PathFinder2D pathFinder; public IReadOnlyDictionary> Hands { get; } public Array2D Board { get; } public List MoveHistory { get; } @@ -44,15 +44,11 @@ namespace Gameboard.ShogiUI.BoardState { for (var i = 0; i < moves.Count; i++) { - if (!TryMove(moves[i])) + if (!Move(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } - if (EvaluateCheckAfterMove(WhoseTurn)) - { - InCheck = WhoseTurn; - } } private ShogiBoard(ShogiBoard toCopy) @@ -75,7 +71,7 @@ namespace Gameboard.ShogiUI.BoardState public bool Move(Move move) { - + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) @@ -84,13 +80,10 @@ namespace Gameboard.ShogiUI.BoardState } // Evaluate check - if (EvaluateCheckAfterMove(WhoseTurn)) + if (EvaluateCheckAfterMove(move, otherPlayer)) { - InCheck = WhoseTurn; - if (InCheck.HasValue) - { - //IsCheckmate = EvaluateCheckmate(); - } + InCheck = otherPlayer; + IsCheckmate = EvaluateCheckmate(); } return true; } @@ -114,11 +107,14 @@ namespace Gameboard.ShogiUI.BoardState validationBoard = null; return false; } - // Assert that this move does not put the moving player in check. - if (validationBoard.EvaluateCheckAfterMove(WhoseTurn)) + // If already in check, assert the move that resulted in check no longer results in check. + if (InCheck == WhoseTurn) { - // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - return false; + if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) + { + // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + return false; + } } // The move is valid and legal; update board state. @@ -251,13 +247,11 @@ namespace Gameboard.ShogiUI.BoardState Console.WriteLine(builder.ToString()); } #region Rules Validation - private bool EvaluateCheckAfterMove(WhichPlayer whichPlayer) + private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) { var isCheck = false; var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; - // Get last move. - var move = MoveHistory[^1]; // Check if the move put the king in check. if (pathFinder.PathTo(move.To, kingPosition)) return true; @@ -327,10 +321,15 @@ namespace Gameboard.ShogiUI.BoardState pathFinder.PathEvery(from, (other, position) => { if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + var moveToTry = new Move { From = from, To = position }; + var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { - isCheckmate = false; + validationBoard = null; + if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) + { + isCheckmate = false; + } } }); } diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index ae234f1..21eed69 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Gameboard.ShogiUI.BoardState; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Linq; using System.Numerics; @@ -304,15 +305,22 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, // P2 King new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, - // P1 Pawn promotes + // P1 Pawn promotes, threatens P2 King new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, // P2 King retreat new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; var shogi = new ShogiBoard(moves); + Console.WriteLine("Prereq"); + shogi.PrintStateAsAscii(); // Act - P1 Pawn wins by checkmate. var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); + Console.WriteLine("Checkmate"); + shogi.PrintStateAsAscii(); + + shogi.Move(new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }); + shogi.PrintStateAsAscii(); // Assert moveSuccess.Should().BeTrue(); diff --git a/PathFinding/Path.cs b/PathFinding/Path.cs index dd3edaf..4b961bb 100644 --- a/PathFinding/Path.cs +++ b/PathFinding/Path.cs @@ -1,7 +1,9 @@ -using System.Numerics; +using System.Diagnostics; +using System.Numerics; namespace PathFinding { + [DebuggerDisplay("{Direction} - {Distance}")] public class Path { public Vector2 Direction { get; } diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 5b5c1e3..539ae7e 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -72,19 +72,20 @@ namespace PathFinding foreach (var path in element.GetPaths()) { var shouldPath = true; - var next = Vector2.Add(from, path.Direction); + var next = Vector2.Add(from, path.Direction); ; while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) { var collider = collection[(int)next.X, (int)next.Y]; if (collider != null) { callback(collider, next); + shouldPath = false; } - next = Vector2.Add(from, path.Direction); if (path.Distance == Distance.OneStep) { shouldPath = false; } + next = Vector2.Add(next, path.Direction); } } } From e64f75e3cc8047233e9fb90b4e6d23a779e78964 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 3 Mar 2021 07:33:12 -0600 Subject: [PATCH 09/27] tests --- Benchmarking/Benchmarking.csproj | 1 + .../Gameboard.ShogiUI.BoardState.csproj | 1 + Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 11 +-- ...board.ShogiUI.Sockets.ServiceModels.csproj | 1 + .../Gameboard.ShogiUI.Sockets.csproj | 1 + .../BoardState/ShogiBoardShould.cs | 86 +++++++++++++++++-- PathFinding/PathFinding.csproj | 1 + 7 files changed, 90 insertions(+), 12 deletions(-) diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index 78da0b4..470d7e6 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -2,6 +2,7 @@ net5.0 + true Exe diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj index e1f728c..23d6a53 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -2,6 +2,7 @@ net5.0 + true diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 43da70b..bb1cb62 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -16,7 +16,7 @@ namespace Gameboard.ShogiUI.BoardState { private delegate void MoveSetCallback(Piece piece, Vector2 position); private readonly PathFinder2D pathFinder; - public ShogiBoard validationBoard; + private ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } @@ -46,6 +46,7 @@ namespace Gameboard.ShogiUI.BoardState { if (!Move(moves[i])) { + // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } @@ -135,16 +136,16 @@ namespace Gameboard.ShogiUI.BoardState { case WhichPiece.Knight: // Knight cannot be placed onto the farthest two ranks from the hand. - minimumY = WhoseTurn == WhichPlayer.Player1 ? 2 : 6; + minimumY = WhoseTurn == WhichPlayer.Player1 ? 6 : 2; break; case WhichPiece.Lance: case WhichPiece.Pawn: // Lance and Pawn cannot be placed onto the farthest rank from the hand. - minimumY = WhoseTurn == WhichPlayer.Player1 ? 1 : 7; + minimumY = WhoseTurn == WhichPlayer.Player1 ? 7 : 1; break; } - if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; - if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; + if (WhoseTurn == WhichPlayer.Player1 && move.To.Y > minimumY) return false; + if (WhoseTurn == WhichPlayer.Player2 && move.To.Y < minimumY) return false; // Mutate the board. Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj index f208d30..423f013 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -2,6 +2,7 @@ net5.0 + true diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 50c2ac9..053dcd9 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -2,6 +2,7 @@ net5.0 + true diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 21eed69..e466a58 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -81,6 +81,24 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); } + + [TestMethod] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = new ShogiBoard(); + // Prerequisit + shogi.Board[4, 4].Should().BeNull(); + + // Act + var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board[4, 4].Should().BeNull(); + shogi.Board[4, 5].Should().BeNull(); + } + [TestMethod] public void PreventInvalidMoves_MoveToCurrentPosition() { @@ -193,6 +211,66 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState shogi.Board[8, 7].Should().BeNull(); } + [TestMethod] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + // P2 Pawn + new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, + // P1 Bishop takes P2 Pawn + new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + // P2 Gold, block check from P1 Bishop. + new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + new Move { From = new Vector2(6, 6), To = new Vector2(7, 7), IsPromotion = true }, + // P2 Pawn again + new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, + // P1 Bishop takes P2 Knight + new Move { From = new Vector2(7, 7), To = new Vector2(7, 8) }, + // P2 Pawn again + new Move { From = new Vector2(0, 4), To = new Vector2(0, 3) }, + // P1 Bishop takes P2 Lance + new Move { From = new Vector2(7, 8), To = new Vector2(8, 8) }, + // P2 Lance (move to make room for attempted P1 Pawn placement) + new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }, + // P1 arbitrary move + new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, + // P2 Pawn again, takes P1 Pawn + new Move { From = new Vector2(0, 3), To = new Vector2(0, 2) }, + }; + var shogi = new ShogiBoard(moves); + shogi.PrintStateAsAscii(); + + // Prerequisite + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + + // Act | Assert - It is P1 turn + // try illegally placing Knight from the hand. + shogi.Board[7, 8].Should().BeNull(); + var moveSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) }); + shogi.PrintStateAsAscii(); + moveSuccess.Should().BeFalse(); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Board[7, 8].Should().BeNull(); + + // Assert + //var pawnDropSuccess = shogi.Move(new Move) + + // Assert + } + + [TestMethod] + public void PreventInvalidDrop_Check() + { + + } + [TestMethod] public void Check() { @@ -316,16 +394,10 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // Act - P1 Pawn wins by checkmate. var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); - Console.WriteLine("Checkmate"); - shogi.PrintStateAsAscii(); - shogi.Move(new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }); - shogi.PrintStateAsAscii(); - - // Assert + // Assert - checkmate moveSuccess.Should().BeTrue(); shogi.IsCheckmate.Should().BeTrue(); - } } } diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index f208d30..423f013 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -2,6 +2,7 @@ net5.0 + true From 7ed771d467d71c56a4e7d1beb4935c0e870f4d6a Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 4 Mar 2021 20:35:23 -0600 Subject: [PATCH 10/27] Code smells --- Benchmarking/Benchmarks.cs | 2 +- Gameboard.ShogiUI.BoardState/Direction.cs | 18 ---- Gameboard.ShogiUI.BoardState/Extensions.cs | 19 ---- .../Gameboard.ShogiUI.BoardState.csproj | 1 + Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs | 40 +++----- .../Pieces/GoldGeneral.cs | 22 ++--- Gameboard.ShogiUI.BoardState/Pieces/King.cs | 24 +++-- Gameboard.ShogiUI.BoardState/Pieces/Knight.cs | 14 +-- Gameboard.ShogiUI.BoardState/Pieces/Lance.cs | 14 +-- Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs | 14 +-- .../{ => Pieces}/Piece.cs | 36 ++----- Gameboard.ShogiUI.BoardState/Pieces/Rook.cs | 37 +++---- .../Pieces/SilverGeneral.cs | 22 ++--- .../{Array2D.cs => PlanarCollection.cs} | 30 +----- Gameboard.ShogiUI.BoardState/Position.cs | 35 ------- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 40 +------- ...board.ShogiUI.Sockets.ServiceModels.csproj | 1 + .../Gameboard.ShogiUI.Sockets.csproj | 1 + Gameboard.ShogiUI.Sockets/Models/Move.cs | 2 +- .../BoardState/BoardStateExtensions.cs | 65 ++++++++++++ .../BoardState/ShogiBoardShould.cs | 98 +++++++++++++++++-- .../PathFinding/PathFinder2DShould.cs | 16 +-- PathFinding/Direction.cs | 18 ++++ PathFinding/Distance.cs | 8 ++ PathFinding/Enums.cs | 19 ---- PathFinding/IPlanarCollection.cs | 2 +- PathFinding/IPlanarElement.cs | 5 +- PathFinding/{Path.cs => Move.cs} | 4 +- PathFinding/MoveSet.cs | 23 +++++ PathFinding/PathFinder2D.cs | 18 ++-- PathFinding/PathFinding.csproj | 1 + 31 files changed, 310 insertions(+), 339 deletions(-) delete mode 100644 Gameboard.ShogiUI.BoardState/Direction.cs delete mode 100644 Gameboard.ShogiUI.BoardState/Extensions.cs rename Gameboard.ShogiUI.BoardState/{ => Pieces}/Piece.cs (55%) rename Gameboard.ShogiUI.BoardState/{Array2D.cs => PlanarCollection.cs} (64%) delete mode 100644 Gameboard.ShogiUI.BoardState/Position.cs create mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs create mode 100644 PathFinding/Direction.cs create mode 100644 PathFinding/Distance.cs delete mode 100644 PathFinding/Enums.cs rename PathFinding/{Path.cs => Move.cs} (77%) create mode 100644 PathFinding/MoveSet.cs diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index a972747..9969cb8 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -12,7 +12,7 @@ namespace Benchmarking { private readonly Move[] moves; private readonly Vector2[] directions; - private readonly Consumer consumer = new Consumer(); + private readonly Consumer consumer = new(); public Benchmarks() { diff --git a/Gameboard.ShogiUI.BoardState/Direction.cs b/Gameboard.ShogiUI.BoardState/Direction.cs deleted file mode 100644 index d474f79..0000000 --- a/Gameboard.ShogiUI.BoardState/Direction.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Numerics; - -namespace Gameboard.ShogiUI.BoardState -{ - public static class Direction - { - public static readonly Vector2 Up = new Vector2(0, 1); - public static readonly Vector2 Down = new Vector2(0, -1); - public static readonly Vector2 Left = new Vector2(-1, 0); - public static readonly Vector2 Right = new Vector2(1, 0); - public static readonly Vector2 UpLeft = new Vector2(-1, 1); - public static readonly Vector2 UpRight = new Vector2(1, 1); - public static readonly Vector2 DownLeft = new Vector2(-1, -1); - public static readonly Vector2 DownRight = new Vector2(1, -1); - public static readonly Vector2 KnightLeft = new Vector2(-1, 2); - public static readonly Vector2 KnightRight = new Vector2(1, 2); - } -} diff --git a/Gameboard.ShogiUI.BoardState/Extensions.cs b/Gameboard.ShogiUI.BoardState/Extensions.cs deleted file mode 100644 index 19dd810..0000000 --- a/Gameboard.ShogiUI.BoardState/Extensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -namespace Gameboard.ShogiUI.BoardState -{ - public static class Extensions - { - public static void ForEachNotNull(this Piece[,] array, Action action) - { - for (var x = 0; x < array.GetLength(0); x++) - for (var y = 0; y < array.GetLength(1); y++) - { - var piece = array[x, y]; - if (piece != null) - { - action(piece, x, y); - } - } - } - } -} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj index 23d6a53..1a2ca2c 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -3,6 +3,7 @@ net5.0 true + 5 diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs index cf73954..f91e6ea 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs @@ -1,34 +1,32 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class Bishop : Piece { - private static readonly List MoveSet = new List(4) + private static readonly List Moves = new(4) { - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) }; - private static readonly List PromotedMoveSet = new List(8) + private static readonly List PromotedMoves = new(8) { - new Path(Direction.Up), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) }; public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) { - // TODO: If this strat works out, we can do away with the Direction class entirely. - PromotedMoveSet.AddRange(MoveSet); + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, PromotedMoves); } public override Piece DeepClone() @@ -37,11 +35,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs index cd22d62..121903d 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs @@ -1,23 +1,23 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class GoldenGeneral : Piece { - public static readonly List MoveSet = new List(6) + public static readonly List Moves = new(6) { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down) + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down) }; public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, Moves); } public override Piece DeepClone() @@ -26,9 +26,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() => Owner == WhichPlayer.Player1 - ? MoveSet - : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.BoardState/Pieces/King.cs index e2d4ebd..b799a06 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/King.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/King.cs @@ -1,25 +1,25 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class King : Piece { - private static readonly List MoveSet = new List(8) + private static readonly List Moves = new(8) { - new Path(Direction.Up), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) }; public King(WhichPlayer owner) : base(WhichPiece.King, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, Moves); } public override Piece DeepClone() @@ -28,7 +28,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - // The move set for a King is the same regardless of orientation. - public override ICollection GetPaths() => MoveSet; } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs index 7e4a4df..efab3aa 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs @@ -7,14 +7,16 @@ namespace Gameboard.ShogiUI.BoardState.Pieces { public class Knight : Piece { - private static readonly List MoveSet = new List(2) + private static readonly List Moves = new(2) { - new Path(Direction.KnightLeft), - new Path(Direction.KnightRight) + new PathFinding.Move(Direction.KnightLeft), + new PathFinding.Move(Direction.KnightRight) }; public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); } public override Piece DeepClone() @@ -23,11 +25,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs index a9f2df7..0329ee1 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs @@ -1,19 +1,19 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class Lance : Piece { - private static readonly List MoveSet = new List(1) + private static readonly List Moves = new(1) { - new Path(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Up, Distance.MultiStep), }; public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); } public override Piece DeepClone() @@ -22,11 +22,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs index bce9f14..4710bc3 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs @@ -1,19 +1,19 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class Pawn : Piece { - private static readonly List MoveSet = new List(1) + private static readonly List Moves = new(1) { - new Path(Direction.Up) + new PathFinding.Move(Direction.Up) }; public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); } public override Piece DeepClone() @@ -22,11 +22,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs similarity index 55% rename from Gameboard.ShogiUI.BoardState/Piece.cs rename to Gameboard.ShogiUI.BoardState/Pieces/Piece.cs index 99bca18..071d301 100644 --- a/Gameboard.ShogiUI.BoardState/Piece.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs @@ -1,15 +1,20 @@ using PathFinding; -using System.Collections.Generic; using System.Diagnostics; -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.BoardState.Pieces { [DebuggerDisplay("{WhichPiece} {Owner}")] public abstract class Piece : IPlanarElement { + protected MoveSet promotedMoveSet; + protected MoveSet moveSet; + + public MoveSet MoveSet => IsPromoted ? promotedMoveSet : moveSet; + public abstract Piece DeepClone(); public WhichPiece WhichPiece { get; } public WhichPlayer Owner { get; private set; } public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; public Piece(WhichPiece piece, WhichPlayer owner) { @@ -22,27 +27,6 @@ namespace Gameboard.ShogiUI.BoardState && WhichPiece != WhichPiece.King && WhichPiece != WhichPiece.GoldenGeneral; - public string ShortName => WhichPiece switch - { - WhichPiece.King => " K ", - WhichPiece.GoldenGeneral => " G ", - WhichPiece.SilverGeneral => IsPromoted ? "^S^" : " S ", - WhichPiece.Bishop => IsPromoted ? "^B^" : " B ", - WhichPiece.Rook => IsPromoted ? "^R^" : " R ", - WhichPiece.Knight => IsPromoted ? "^k^" : " k ", - WhichPiece.Lance => IsPromoted ? "^L^" : " L ", - WhichPiece.Pawn => IsPromoted ? "^P^" : " P ", - _ => " ? ", - }; - - public bool IsRanged => WhichPiece switch - { - WhichPiece.Bishop => true, - WhichPiece.Rook => true, - WhichPiece.Lance => !IsPromoted, - _ => false, - }; - public void ToggleOwnership() { Owner = Owner == WhichPlayer.Player1 @@ -59,11 +43,5 @@ namespace Gameboard.ShogiUI.BoardState ToggleOwnership(); Demote(); } - - public abstract ICollection GetPaths(); - - public abstract Piece DeepClone(); - - public bool IsUpsideDown => Owner == WhichPlayer.Player2; } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs index dfedd94..709c82f 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs @@ -1,32 +1,32 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class Rook : Piece { - private static readonly List MoveSet = new List(4) + private static readonly List Moves = new(4) { - new Path(Direction.Up, Distance.MultiStep), - new Path(Direction.Left, Distance.MultiStep), - new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep) + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep) }; - private static readonly List PromotedMoveSet = new List(8) + private static readonly List PromotedMoves = new(8) { - new Path(Direction.Up, Distance.MultiStep), - new Path(Direction.Left, Distance.MultiStep), - new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) }; public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, PromotedMoves); } public override Piece DeepClone() @@ -35,10 +35,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs index ef0bcbd..557c3b0 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs @@ -1,22 +1,22 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { public class SilverGeneral : Piece { - private static readonly List MoveSet = new List(4) + private static readonly List Moves = new(4) { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) }; public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) { + moveSet = new MoveSet(this, Moves); + promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); } public override Piece DeepClone() @@ -25,11 +25,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces if (IsPromoted) clone.Promote(); return clone; } - - public override ICollection GetPaths() - { - var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; - return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/PlanarCollection.cs similarity index 64% rename from Gameboard.ShogiUI.BoardState/Array2D.cs rename to Gameboard.ShogiUI.BoardState/PlanarCollection.cs index 6487646..f093035 100644 --- a/Gameboard.ShogiUI.BoardState/Array2D.cs +++ b/Gameboard.ShogiUI.BoardState/PlanarCollection.cs @@ -2,19 +2,17 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState { - public class Array2D : IPlanarCollection, IEnumerable + public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { - /// False to stop iterating. public delegate void ForEachDelegate(T element, int x, int y); private readonly T[] array; private readonly int width; private readonly int height; - public Array2D(int width, int height) + public PlanarCollection(int width, int height) { this.width = width; this.height = height; @@ -39,17 +37,6 @@ namespace Gameboard.ShogiUI.BoardState _ => throw new IndexOutOfRangeException() }; - public void ForEach(ForEachDelegate callback) - { - for (var x = 0; x < width; x++) - { - for (var y = 0; y < height; y++) - { - callback(this[x, y], x, y); - } - } - } - public void ForEachNotNull(ForEachDelegate callback) { for (var x = 0; x < width; x++) @@ -62,19 +49,6 @@ namespace Gameboard.ShogiUI.BoardState } } - public Vector2? IndexOf(Predicate predicate) - { - for (var x = 0; x < width; x++) - for (var y = 0; y < height; y++) - { - if (this[x, y] != null && predicate(this[x, y])) - { - return new Vector2(x, y); - } - } - return null; - } - public IEnumerator GetEnumerator() { foreach (var item in array) yield return item; diff --git a/Gameboard.ShogiUI.BoardState/Position.cs b/Gameboard.ShogiUI.BoardState/Position.cs deleted file mode 100644 index 71552d7..0000000 --- a/Gameboard.ShogiUI.BoardState/Position.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Gameboard.ShogiUI.BoardState -{ - public class Position - { - private int x; - private int y; - - public int X - { - get => x; - set { - if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); - x = value; - } - } - - public int Y - { - get => y; - set - { - if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); - y = value; - } - } - - public Position(int x, int y) - { - X = x; - Y = y; - } - } -} diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index bb1cb62..6242196 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -20,7 +20,7 @@ namespace Gameboard.ShogiUI.BoardState private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } - public Array2D Board { get; } + public PlanarCollection Board { get; } public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } @@ -28,7 +28,7 @@ namespace Gameboard.ShogiUI.BoardState public ShogiBoard() { - Board = new Array2D(9, 9); + Board = new PlanarCollection(9, 9); MoveHistory = new List(20); Hands = new Dictionary> { { WhichPlayer.Player1, new List()}, @@ -54,7 +54,7 @@ namespace Gameboard.ShogiUI.BoardState private ShogiBoard(ShogiBoard toCopy) { - Board = new Array2D(9, 9); + Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) Board[x, y] = toCopy.Board[x, y]?.DeepClone(); @@ -213,40 +213,6 @@ namespace Gameboard.ShogiUI.BoardState return !isObstructed && isPathable; } - public void PrintStateAsAscii() - { - var builder = new StringBuilder(); - builder.Append(" Player 2"); - builder.AppendLine(); - for (var y = 8; y > -1; y--) - { - builder.Append("- "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append('|'); - for (var x = 0; x < 9; x++) - { - var piece = Board[x, y]; - if (piece == null) - { - builder.Append(" "); - } - else - { - builder.AppendFormat("{0}", piece.ShortName); - } - builder.Append('|'); - } - builder.AppendLine(); - } - builder.Append("- "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append(" Player 1"); - Console.WriteLine(builder.ToString()); - } #region Rules Validation private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj index 423f013..ee29921 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -3,6 +3,7 @@ net5.0 true + 5 diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 053dcd9..3b3a6f8 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -3,6 +3,7 @@ net5.0 true + 5 diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index b61d8d7..66fda38 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -44,7 +44,7 @@ namespace Gameboard.ShogiUI.Sockets.Models IsPromotion = move.IsPromotion; PieceFromCaptured = pieceFromCaptured; } - public ServiceModels.Socket.Types.Move ToServiceModel() => new ServiceModels.Socket.Types.Move + public ServiceModels.Socket.Types.Move ToServiceModel() => new() { From = From.ToBoardNotation(), IsPromotion = IsPromotion, diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs b/Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs new file mode 100644 index 0000000..9eac527 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs @@ -0,0 +1,65 @@ +using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.BoardState.Pieces; +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace Gameboard.ShogiUI.UnitTests.BoardState +{ + public static class BoardStateExtensions + { + public static string GetShortName(this Piece self) + { + var name = self.WhichPiece switch + { + WhichPiece.King => " K ", + WhichPiece.GoldenGeneral => " G ", + WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ", + WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ", + WhichPiece.Rook => self.IsPromoted ? "^R " : " R ", + WhichPiece.Knight => self.IsPromoted ? "^k " : " k ", + WhichPiece.Lance => self.IsPromoted ? "^L " : " L ", + WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ", + _ => " ? ", + }; + if (self.Owner == WhichPlayer.Player2) + name = Regex.Replace(name, @"([^\s]+)\s", "$1."); + return name; + } + + public static void PrintStateAsAscii(this ShogiBoard self) + { + var builder = new StringBuilder(); + builder.Append(" Player 2(.)"); + builder.AppendLine(); + for (var y = 8; y > -1; y--) + { + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append('|'); + for (var x = 0; x < 9; x++) + { + var piece = self.Board[x, y]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", piece.GetShortName()); + } + builder.Append('|'); + } + builder.AppendLine(); + } + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" Player 1"); + Console.WriteLine(builder.ToString()); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index e466a58..1178c55 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.BoardState.Pieces; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; @@ -81,7 +82,6 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); } - [TestMethod] public void PreventInvalidMoves_MoveFromEmptyPosition() { @@ -245,30 +245,110 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var shogi = new ShogiBoard(moves); shogi.PrintStateAsAscii(); - // Prerequisite + // Prerequisites shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); // Act | Assert - It is P1 turn - // try illegally placing Knight from the hand. + /// try illegally placing Knight from the hand. shogi.Board[7, 8].Should().BeNull(); - var moveSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) }); - shogi.PrintStateAsAscii(); - moveSuccess.Should().BeFalse(); + var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) }); + dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 8].Should().BeNull(); + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 7) }); + dropSuccess.Should().BeFalse(); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Board[7, 7].Should().BeNull(); - // Assert - //var pawnDropSuccess = shogi.Move(new Move) + /// try illegally placing Pawn from the hand + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 8) }); + dropSuccess.Should().BeFalse(); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + shogi.Board[7, 8].Should().BeNull(); - // Assert + /// try illegally placing Lance from the hand + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 8) }); + dropSuccess.Should().BeFalse(); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Board[7, 8].Should().BeNull(); } [TestMethod] public void PreventInvalidDrop_Check() { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + // P2 Pawn + new Move { From = new Vector2(8, 6), To = new Vector2(8, 5) }, + // P1 Bishop, check + new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + // P2 Gold, block check + new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + // P1 arbitrary move + new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, + // P2 Bishop + new Move { From = new Vector2(7, 7), To = new Vector2(8, 6) }, + // P1 Bishop takes P2 Lance + new Move { From = new Vector2(6, 6), To = new Vector2(8, 8) }, + // P2 Bishop + new Move { From = new Vector2(8, 6), To = new Vector2(7, 7) }, + // P1 arbitrary move + new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, + // P2 Bishop, check + new Move { From = new Vector2(7, 7), To = new Vector2(2, 2) }, + }; + var shogi = new ShogiBoard(moves); + // Prerequisites + shogi.InCheck.Should().Be(WhichPlayer.Player1); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + + // Act - P1 tries to place a Lance while in check. + var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(4, 4) }); + + // Assert + dropSuccess.Should().BeFalse(); + shogi.Board[4, 4].Should().BeNull(); + shogi.InCheck.Should().Be(WhichPlayer.Player1); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + } + + [TestMethod] + public void PreventInvalidDrop_Capture() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + // P2 Pawn + new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, + // P1 Bishop, capture P2 Pawn, check + new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + // P2 Gold, block check + new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + // P1 Bishop capture P2 Bishop + new Move { From = new Vector2(6, 6), To = new Vector2(7, 7) }, + // P2 arbitrary move + new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }, + }; + var shogi = new ShogiBoard(moves); + + // Prerequisites + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + + // Act - P1 tries to place Bishop from hand to an already-occupied position + var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) }); + + // Assert + dropSuccess.Should().BeFalse(); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board[4, 0].WhichPiece.Should().Be(WhichPiece.King); } [TestMethod] diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs index 935051e..089d142 100644 --- a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -1,8 +1,7 @@ using FluentAssertions; -using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.BoardState.Pieces; using Microsoft.VisualStudio.TestTools.UnitTesting; using PathFinding; -using System.Collections.Generic; using System.Numerics; namespace Gameboard.ShogiUI.UnitTests.PathFinding @@ -10,32 +9,25 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding [TestClass] public class PathFinder2DShould { - class TestElement : IPlanarElement - { - public ICollection GetPaths() => throw new System.NotImplementedException(); - public bool IsUpsideDown => false; - } - [TestMethod] public void Maths() { - var finder = new PathFinder2D(new Array2D(5, 5)); - var result = finder.IsPathable( + var result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(1, 1) ); result.Should().BeTrue(); - result = finder.IsPathable( + result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(0, 0) ); result.Should().BeFalse(); - result = finder.IsPathable( + result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(-1, 1) diff --git a/PathFinding/Direction.cs b/PathFinding/Direction.cs new file mode 100644 index 0000000..2ee825d --- /dev/null +++ b/PathFinding/Direction.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace PathFinding +{ + public static class Direction + { + public static readonly Vector2 Up = new(0, 1); + public static readonly Vector2 Down = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 UpLeft = new(-1, 1); + public static readonly Vector2 UpRight = new(1, 1); + public static readonly Vector2 DownLeft = new(-1, -1); + public static readonly Vector2 DownRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); + } +} diff --git a/PathFinding/Distance.cs b/PathFinding/Distance.cs new file mode 100644 index 0000000..339296d --- /dev/null +++ b/PathFinding/Distance.cs @@ -0,0 +1,8 @@ +namespace PathFinding +{ + public enum Distance + { + OneStep, + MultiStep + } +} \ No newline at end of file diff --git a/PathFinding/Enums.cs b/PathFinding/Enums.cs deleted file mode 100644 index fab3f18..0000000 --- a/PathFinding/Enums.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace PathFinding -{ - public enum HaltCondition - { - /// - /// Do not stop until you reach the collection boundary. - /// - None, - /// - /// Halt after encountering a non-null element. - /// - AfterCollide - } -} -public enum Distance -{ - OneStep, - MultiStep -} diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 95506e1..d276b12 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -2,7 +2,7 @@ namespace PathFinding { - public interface IPlanarCollection : IEnumerable + public interface IPlanarCollection : IEnumerable where T : IPlanarElement { T this[float x, float y] { get; set; } int GetLength(int dimension); diff --git a/PathFinding/IPlanarElement.cs b/PathFinding/IPlanarElement.cs index 4983bb4..5d3fde4 100644 --- a/PathFinding/IPlanarElement.cs +++ b/PathFinding/IPlanarElement.cs @@ -1,12 +1,9 @@  -using System.Collections.Generic; - namespace PathFinding { public interface IPlanarElement { - ICollection GetPaths(); - + MoveSet MoveSet { get; } bool IsUpsideDown { get; } } } diff --git a/PathFinding/Path.cs b/PathFinding/Move.cs similarity index 77% rename from PathFinding/Path.cs rename to PathFinding/Move.cs index 4b961bb..5ff2c8e 100644 --- a/PathFinding/Path.cs +++ b/PathFinding/Move.cs @@ -4,11 +4,11 @@ using System.Numerics; namespace PathFinding { [DebuggerDisplay("{Direction} - {Distance}")] - public class Path + public class Move { public Vector2 Direction { get; } public Distance Distance { get; } - public Path(Vector2 direction, Distance distance = Distance.OneStep) + public Move(Vector2 direction, Distance distance = Distance.OneStep) { Direction = direction; Distance = distance; diff --git a/PathFinding/MoveSet.cs b/PathFinding/MoveSet.cs new file mode 100644 index 0000000..f6c7260 --- /dev/null +++ b/PathFinding/MoveSet.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace PathFinding +{ + public class MoveSet + { + private readonly IPlanarElement element; + private readonly ICollection moves; + private readonly ICollection upsidedownMoves; + + public MoveSet(IPlanarElement element, ICollection moves) + { + this.element = element; + this.moves = moves; + upsidedownMoves = moves.Select(_ => new Move(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + + public ICollection GetMoves() => element.IsUpsideDown ? upsidedownMoves : moves; + + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 539ae7e..ae32b6b 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -13,7 +12,6 @@ namespace PathFinding /// public delegate void Callback(T collider, Vector2 position); - private readonly IPlanarCollection collection; private readonly int width; private readonly int height; @@ -39,8 +37,9 @@ namespace PathFinding return false; } var element = collection[origin.X, origin.Y]; - var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); + if (element == null) return false; + var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); if (!IsPathable(origin, destination, path.Direction)) { // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. @@ -69,7 +68,7 @@ namespace PathFinding public void PathEvery(Vector2 from, Callback callback) { var element = collection[from.X, from.Y]; - foreach (var path in element.GetPaths()) + foreach (var path in element.MoveSet.GetMoves()) { var shouldPath = true; var next = Vector2.Add(from, path.Direction); ; @@ -106,16 +105,15 @@ namespace PathFinding } } - public Path FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => + public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); - - public bool IsPathable(Vector2 origin, Vector2 destination, T element) + public static bool IsPathable(Vector2 origin, Vector2 destination, T element) { - var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); + var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); return IsPathable(origin, destination, path.Direction); } - public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) + public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) { direction = Vector2.Normalize(direction); var next = Vector2.Add(origin, direction); diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index 423f013..ee29921 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -3,6 +3,7 @@ net5.0 true + 5 From 97ccff1fae7814fa8295b1a14e122d4e5484ed38 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 4 Mar 2021 20:40:53 -0600 Subject: [PATCH 11/27] code cleanup --- Gameboard.ShogiUI.BoardState/Pieces/Knight.cs | 2 -- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 1 - .../Api/Messages/GetToken.cs | 14 ++++++------- .../Socket/Interfaces/IRequest.cs | 8 ++++---- .../Socket/Interfaces/IResponse.cs | 10 +++++----- .../Socket/Types/ClientActionEnum.cs | 20 +++++++++---------- .../ClientActionHandlers/IActionHandler.cs | 1 - .../Managers/SocketCommunicationManager.cs | 1 - .../Utility/AuthenticatedHttpClient.cs | 8 ++++---- Gameboard.ShogiUI.Sockets/Startup.cs | 12 +++++------ 10 files changed, 36 insertions(+), 41 deletions(-) diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs index efab3aa..5a6cdd1 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs @@ -1,7 +1,5 @@ using PathFinding; using System.Collections.Generic; -using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.BoardState.Pieces { diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 6242196..8c3653b 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -3,7 +3,6 @@ using PathFinding; using System; using System.Collections.Generic; using System.Numerics; -using System.Text; namespace Gameboard.ShogiUI.BoardState { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs index e2f3c98..eb59291 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs @@ -2,13 +2,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages { - public class GetTokenResponse - { - public Guid OneTimeToken { get; } + public class GetTokenResponse + { + public Guid OneTimeToken { get; } - public GetTokenResponse(Guid token) - { - OneTimeToken = token; - } + public GetTokenResponse(Guid token) + { + OneTimeToken = token; } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs index ce8c0a4..79ec262 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs @@ -2,8 +2,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces { - public interface IRequest - { - ClientAction Action { get; set; } - } + public interface IRequest + { + ClientAction Action { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs index 8c1bed8..746d3ab 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs @@ -1,8 +1,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces { - public interface IResponse - { - string Action { get; } - string Error { get; set; } - } + public interface IResponse + { + string Action { get; } + string Error { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs index 9e0952e..7b91157 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs @@ -1,13 +1,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { - public enum ClientAction - { - ListGames, - CreateGame, - JoinGame, - JoinByCode, - LoadGame, - Move, - KeepAlive - } + public enum ClientAction + { + ListGames, + CreateGame, + JoinGame, + JoinByCode, + LoadGame, + Move, + KeepAlive + } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs index 20f98ab..24cf9d2 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 3269493..33ec4d2 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -9,7 +9,6 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Net.WebSockets; using System.Threading.Tasks; diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs index b9b7ba0..3b4067d 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs @@ -58,7 +58,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility this.SetBearerToken(tokenResponse.AccessToken); } - public async new Task GetAsync(string requestUri) + public new async Task GetAsync(string requestUri) { var response = await base.GetAsync(requestUri); if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -73,7 +73,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility await response.Content.ReadAsStringAsync()); return response; } - public async new Task PostAsync(string requestUri, HttpContent content) + public new async Task PostAsync(string requestUri, HttpContent content) { var response = await base.PostAsync(requestUri, content); if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -90,7 +90,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility await response.Content.ReadAsStringAsync()); return response; } - public async new Task PutAsync(string requestUri, HttpContent content) + public new async Task PutAsync(string requestUri, HttpContent content) { var response = await base.PutAsync(requestUri, content); if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -107,7 +107,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.Utility await response.Content.ReadAsStringAsync()); return response; } - public async new Task DeleteAsync(string requestUri) + public new async Task DeleteAsync(string requestUri) { var response = await base.DeleteAsync(requestUri); if (response.StatusCode == HttpStatusCode.Unauthorized) diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 09805be..8431c68 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -1,5 +1,10 @@ -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; +using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; +using Gameboard.ShogiUI.Sockets.Repositories.Utility; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -12,11 +17,6 @@ using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; -using Gameboard.ShogiUI.Sockets.Managers; -using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; namespace Gameboard.ShogiUI.Sockets { From 2d5c6b20b94e5a8a9c70faa4a66b57ba38c61d39 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 4 Mar 2021 21:09:14 -0600 Subject: [PATCH 12/27] yep --- .../PathFinding/PathFinder2DShould.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs index 089d142..983ec6f 100644 --- a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using Gameboard.ShogiUI.BoardState.Pieces; using Microsoft.VisualStudio.TestTools.UnitTesting; using PathFinding; using System.Numerics; @@ -12,22 +11,21 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding [TestMethod] public void Maths() { - - var result = PathFinder2D.IsPathable( + var result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(1, 1) ); result.Should().BeTrue(); - result = PathFinder2D.IsPathable( + result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(0, 0) ); result.Should().BeFalse(); - result = PathFinder2D.IsPathable( + result = PathFinder2D.IsPathable( new Vector2(2, 2), new Vector2(7, 7), new Vector2(-1, 1) From 05a9c71499e3f1025e8c7d2769a20a852741754b Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 6 Apr 2021 19:52:02 -0500 Subject: [PATCH 13/27] Fixed accidentally building the board from player2 perspective. --- Benchmarking/Benchmarking.csproj | 2 +- Benchmarking/Benchmarks.cs | 2 +- ....csproj => Gameboard.ShogiUI.Rules.csproj} | 0 Gameboard.ShogiUI.BoardState/Move.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs | 2 +- .../Pieces/GoldGeneral.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/King.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Knight.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Lance.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Piece.cs | 2 +- Gameboard.ShogiUI.BoardState/Pieces/Rook.cs | 2 +- .../Pieces/SilverGeneral.cs | 2 +- .../PlanarCollection.cs | 2 +- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 61 ++++-- Gameboard.ShogiUI.BoardState/WhichPiece.cs | 2 +- Gameboard.ShogiUI.BoardState/WhichPlayer.cs | 2 +- Gameboard.ShogiUI.Domain/Entities/Board.cs | 6 + Gameboard.ShogiUI.Domain/Entities/Match.cs | 30 +++ .../Gameboard.ShogiUI.Domain.csproj | 7 + .../ValueObjects/Class1.cs | 15 ++ .../Socket/Messages/LoadGame.cs | 2 +- .../Socket/Messages/Move.cs | 2 +- .../Socket/Types/Piece.cs | 5 - Gameboard.ShogiUI.Sockets.sln | 10 +- .../Controllers/GameController.cs | 4 +- .../Controllers/SocketController.cs | 11 +- .../Gameboard.ShogiUI.Sockets.csproj | 2 +- .../Managers/BoardManager.cs | 7 +- .../ClientActionHandlers/CreateGameHandler.cs | 9 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 38 ++-- .../ClientActionHandlers/JoinGameHandler.cs | 4 +- .../ClientActionHandlers/LoadGameHandler.cs | 15 +- .../ClientActionHandlers/MoveHandler.cs | 54 +++-- .../Models/BoardState.cs | 40 ++++ Gameboard.ShogiUI.Sockets/Models/Move.cs | 8 +- Gameboard.ShogiUI.Sockets/Models/Piece.cs | 27 +++ Gameboard.ShogiUI.Sockets/Models/Player.cs | 12 ++ .../Properties/launchSettings.json | 2 +- .../Repositories/GameboardRepository.cs | 79 +++++--- .../GameboardRepositoryManager.cs | 15 +- .../BoardStateExtensions.cs | 8 +- .../{BoardState => Rules}/ShogiBoardShould.cs | 190 +++++++++--------- PathFinding/Direction.cs | 16 +- PathFinding/PathFinder2D.cs | 8 - 45 files changed, 441 insertions(+), 276 deletions(-) rename Gameboard.ShogiUI.BoardState/{Gameboard.ShogiUI.BoardState.csproj => Gameboard.ShogiUI.Rules.csproj} (100%) create mode 100644 Gameboard.ShogiUI.Domain/Entities/Board.cs create mode 100644 Gameboard.ShogiUI.Domain/Entities/Match.cs create mode 100644 Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj create mode 100644 Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/Piece.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/Player.cs rename Gameboard.ShogiUI.UnitTests/{BoardState => Rules}/BoardStateExtensions.cs (91%) rename Gameboard.ShogiUI.UnitTests/{BoardState => Rules}/ShogiBoardShould.cs (74%) diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index 470d7e6..ec8dd8e 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -11,7 +11,7 @@ - + diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 9969cb8..b5785ed 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; -using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.Rules; using System; using System.Linq; using System.Numerics; diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj similarity index 100% rename from Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj rename to Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs index fc8f860..693c954 100644 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Numerics; -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.Rules { [DebuggerDisplay("{From} - {To}")] public class Move diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs index f91e6ea..3c433cc 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class Bishop : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs index 121903d..055b779 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class GoldenGeneral : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.BoardState/Pieces/King.cs index b799a06..ab4d9c4 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/King.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/King.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class King : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs index 5a6cdd1..7091ceb 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class Knight : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs index 0329ee1..48be6a2 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class Lance : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs index 4710bc3..1005c6f 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class Pawn : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs b/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs index 071d301..1f019b2 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Diagnostics; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { [DebuggerDisplay("{WhichPiece} {Owner}")] public abstract class Piece : IPlanarElement diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs index 709c82f..3a990a4 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class Rook : Piece { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs index 557c3b0..3287604 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs +++ b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs @@ -1,7 +1,7 @@ using PathFinding; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState.Pieces +namespace Gameboard.ShogiUI.Rules.Pieces { public class SilverGeneral : Piece { diff --git a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs b/Gameboard.ShogiUI.BoardState/PlanarCollection.cs index f093035..4baf867 100644 --- a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs +++ b/Gameboard.ShogiUI.BoardState/PlanarCollection.cs @@ -3,7 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.Rules { public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 8c3653b..0449668 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -1,10 +1,10 @@ -using Gameboard.ShogiUI.BoardState.Pieces; +using Gameboard.ShogiUI.Rules.Pieces; using PathFinding; using System; using System.Collections.Generic; using System.Numerics; -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.Rules { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. @@ -14,16 +14,20 @@ namespace Gameboard.ShogiUI.BoardState public class ShogiBoard { private delegate void MoveSetCallback(Piece piece, Vector2 position); + private readonly bool isValidationBoard; private readonly PathFinder2D pathFinder; private ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } - public PlanarCollection Board { get; } + public PlanarCollection Board { get; } //TODO: Hide this being a getter method public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } public bool IsCheckmate { get; private set; } + + + public string Error { get; private set; } public ShogiBoard() { @@ -35,8 +39,8 @@ namespace Gameboard.ShogiUI.BoardState }; pathFinder = new PathFinder2D(Board); InitializeBoardState(); - player1King = new Vector2(4, 0); - player2King = new Vector2(4, 8); + player1King = new Vector2(4, 8); + player2King = new Vector2(4, 0); } public ShogiBoard(IList moves) : this() @@ -46,13 +50,14 @@ namespace Gameboard.ShogiUI.BoardState if (!Move(moves[i])) { // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); } } } private ShogiBoard(ShogiBoard toCopy) { + isValidationBoard = true; Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) @@ -143,8 +148,8 @@ namespace Gameboard.ShogiUI.BoardState minimumY = WhoseTurn == WhichPlayer.Player1 ? 7 : 1; break; } - if (WhoseTurn == WhichPlayer.Player1 && move.To.Y > minimumY) return false; - if (WhoseTurn == WhichPlayer.Player2 && move.To.Y < minimumY) return false; + if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; + if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; // Mutate the board. Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; @@ -156,9 +161,21 @@ namespace Gameboard.ShogiUI.BoardState private bool PlaceFromBoard(Move move) { var fromPiece = Board[move.From.X, move.From.Y]; - if (fromPiece == null) return false; // Invalid move - if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. - if (IsPathable(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. + if (fromPiece == null) + { + Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; + return false; // Invalid move + } + if (fromPiece.Owner != WhoseTurn) + { + Error = "Not allowed to move the opponents piece"; + return false; // Invalid move; cannot move other players pieces. + } + if (IsPathable(move.From, move.To) == false) + { + Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; + return false; // Invalid move; move not part of move-set. + } var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -171,11 +188,11 @@ namespace Gameboard.ShogiUI.BoardState //Mutate the board. if (move.IsPromotion) { - if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Y > 5)) + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Y < 3)) { fromPiece.Promote(); } - else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Y < 3)) + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Y > 5)) { fromPiece.Promote(); } @@ -313,12 +330,12 @@ namespace Gameboard.ShogiUI.BoardState } private void ResetFrontRow(WhichPlayer player) { - int y = player == WhichPlayer.Player1 ? 2 : 6; + int y = player == WhichPlayer.Player1 ? 6 : 2; for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); } private void ResetMiddleRow(WhichPlayer player) { - int y = player == WhichPlayer.Player1 ? 1 : 7; + int y = player == WhichPlayer.Player1 ? 7 : 1; Board[0, y] = null; for (int x = 2; x < 7; x++) Board[x, y] = null; @@ -336,7 +353,7 @@ namespace Gameboard.ShogiUI.BoardState } private void ResetRearRow(WhichPlayer player) { - int y = player == WhichPlayer.Player1 ? 0 : 8; + int y = player == WhichPlayer.Player1 ? 8 : 0; Board[0, y] = new Lance(player); Board[1, y] = new Knight(player); @@ -350,13 +367,13 @@ namespace Gameboard.ShogiUI.BoardState } private void InitializeBoardState() { - ResetRearRow(WhichPlayer.Player1); - ResetMiddleRow(WhichPlayer.Player1); - ResetFrontRow(WhichPlayer.Player1); - ResetEmptyRows(); - ResetFrontRow(WhichPlayer.Player2); - ResetMiddleRow(WhichPlayer.Player2); ResetRearRow(WhichPlayer.Player2); + ResetMiddleRow(WhichPlayer.Player2); + ResetFrontRow(WhichPlayer.Player2); + ResetEmptyRows(); + ResetFrontRow(WhichPlayer.Player1); + ResetMiddleRow(WhichPlayer.Player1); + ResetRearRow(WhichPlayer.Player1); } #endregion } diff --git a/Gameboard.ShogiUI.BoardState/WhichPiece.cs b/Gameboard.ShogiUI.BoardState/WhichPiece.cs index a0dd88c..ec4fd4d 100644 --- a/Gameboard.ShogiUI.BoardState/WhichPiece.cs +++ b/Gameboard.ShogiUI.BoardState/WhichPiece.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.Rules { public enum WhichPiece { diff --git a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs index 1e8de13..4b8b8f2 100644 --- a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs +++ b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.BoardState +namespace Gameboard.ShogiUI.Rules { public enum WhichPlayer { diff --git a/Gameboard.ShogiUI.Domain/Entities/Board.cs b/Gameboard.ShogiUI.Domain/Entities/Board.cs new file mode 100644 index 0000000..9533d8d --- /dev/null +++ b/Gameboard.ShogiUI.Domain/Entities/Board.cs @@ -0,0 +1,6 @@ +namespace Gameboard.ShogiUI.Domain +{ + public class Board + { + } +} diff --git a/Gameboard.ShogiUI.Domain/Entities/Match.cs b/Gameboard.ShogiUI.Domain/Entities/Match.cs new file mode 100644 index 0000000..2d54945 --- /dev/null +++ b/Gameboard.ShogiUI.Domain/Entities/Match.cs @@ -0,0 +1,30 @@ +namespace Gameboard.ShogiUI.Domain +{ + public class Match + { + public string Name { get; } + public string Player1 { get; } + public string Player2 { get; } + + /// + /// Initialize pre-existing Match. + /// + public Match(MatchMeta meta, Board board) + { + Name = meta.Name; + Player1 = meta.Player1; + Player2 = meta.Player2; + } + + /// + /// Create a new Match. + /// + public Match(string name, string player1) + { + Name = name; + Player1 = player1; + } + + + } +} diff --git a/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj b/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj new file mode 100644 index 0000000..f208d30 --- /dev/null +++ b/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + diff --git a/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs b/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs new file mode 100644 index 0000000..6939def --- /dev/null +++ b/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Domain +{ + public class MatchMeta + { + public string Name { get; } + public string Player1 { get; } + public string Player2 { get; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index c457791..981ebf6 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; private set; } public Game Game { get; set; } - public IReadOnlyList Moves { get; set; } + public BoardState BoardState { get; set; } public string Error { get; set; } public LoadGameResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs index 5039196..e46f9a0 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs @@ -15,7 +15,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public string Action { get; } public string Error { get; set; } public string GameName { get; set; } - public Move Move { get; set; } + public BoardState BoardState { get; set; } public string PlayerName { get; set; } public MoveResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs index bb5ef62..8f9fd23 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs @@ -4,11 +4,6 @@ { public WhichPiece WhichPiece { get; set; } - /// - /// True if this piece is controlled by you. - /// - public bool IsControlledByMe { get; set; } - public bool IsPromoted { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 497525d..76d0058 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -9,13 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.BoardState", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.BoardState.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Domain", "Gameboard.ShogiUI.Domain\Gameboard.ShogiUI.Domain.csproj", "{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,6 +49,10 @@ Global {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU + {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 5d0becd..72a1fa8 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -31,7 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers var isPlayer1 = await manager.IsPlayer1(request.SessionName, userName); if (isPlayer1) { - var code = (await repository.PostJoinCode(request.SessionName, userName)).JoinCode; + var code = await repository.PostJoinCode(request.SessionName, userName); return new CreatedResult("", new PostGameInvitationResponse(code)); } else @@ -49,7 +49,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers var isPlayer1 = manager.IsPlayer1(request.SessionName, request.GuestId); if (isGuest && await isPlayer1) { - var code = (await repository.PostJoinCode(request.SessionName, request.GuestId)).JoinCode; + var code = await repository.PostJoinCode(request.SessionName, request.GuestId); return new CreatedResult("", new PostGameInvitationResponse(code)); } else diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index a729cdb..4891963 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.Managers; -using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; @@ -15,16 +14,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers public class SocketController : ControllerBase { private readonly ISocketTokenManager tokenManager; - private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepositoryManager gameboardManager; public SocketController( ISocketTokenManager tokenManager, - IGameboardRepository gameboardRepository, IGameboardRepositoryManager gameboardManager) { this.tokenManager = tokenManager; - this.gameboardRepository = gameboardRepository; this.gameboardManager = gameboardManager; } @@ -48,11 +44,10 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } else { - var response = await gameboardRepository.GetPlayer(request.ClientId); - if (response != null && response.Player != null) + if (await gameboardManager.PlayerExists(request.ClientId)) { - var token = tokenManager.GenerateToken(response.Player.Name); - return new JsonResult(new GetGuestTokenResponse(response.Player.Name, token)); + var token = tokenManager.GenerateToken(request.ClientId); + return new JsonResult(new GetGuestTokenResponse(request.ClientId, token)); } } return new UnauthorizedResult(); diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 3b3a6f8..dd477c5 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -17,7 +17,7 @@ - + diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs index 494c738..813887e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.Rules; using System.Collections.Concurrent; namespace Gameboard.ShogiUI.Sockets.Managers @@ -26,10 +26,5 @@ namespace Gameboard.ShogiUI.Sockets.Managers return board; return null; } - - public string GetBoardState() - { - return string.Empty; - } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index d8244a7..bd93ab6 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -12,16 +12,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers // It can be an API route and still tell socket connections about the new session. public class CreateGameHandler : IActionHandler { - private readonly ILogger logger; private readonly IGameboardRepository repository; private readonly ISocketCommunicationManager communicationManager; public CreateGameHandler( - ILogger logger, ISocketCommunicationManager communicationManager, IGameboardRepository repository) { - this.logger = logger; this.repository = repository; this.communicationManager = communicationManager; } @@ -29,7 +26,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var postSessionResponse = await repository.PostSession(new PostSession + var sessionName = await repository.PostSession(new PostSession { SessionName = request.GameName, PlayerName = userName, @@ -41,12 +38,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, Game = new Game { - GameName = postSessionResponse.SessionName, + GameName = sessionName, Players = new[] { userName } } }; - if (string.IsNullOrWhiteSpace(postSessionResponse.SessionName)) + if (string.IsNullOrWhiteSpace(sessionName)) { response.Error = "Game already exists."; } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index f8edb5e..6f22279 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.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.Threading.Tasks; @@ -10,16 +9,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class JoinByCodeHandler : IActionHandler { - private readonly ILogger logger; private readonly IGameboardRepository repository; private readonly ISocketCommunicationManager communicationManager; public JoinByCodeHandler( - ILogger logger, ISocketCommunicationManager communicationManager, IGameboardRepository repository) { - this.logger = logger; this.repository = repository; this.communicationManager = communicationManager; } @@ -27,38 +23,38 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession + var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession { PlayerName = userName, JoinCode = request.JoinCode }); - if (joinGameResponse.JoinSucceeded) + if (sessionName == null) { - // Other members of the game see a regular JoinGame occur. - var response = new JoinGameResponse(ClientAction.JoinGame) + var response = new JoinGameResponse(ClientAction.JoinByCode) { PlayerName = userName, - GameName = joinGameResponse.SessionName - }; - // At this time, userName hasn't subscribed and won't receive this message. - await communicationManager.BroadcastToGame(joinGameResponse.SessionName, response); - - // The player joining sees the JoinByCode occur. - response = new JoinGameResponse(ClientAction.JoinByCode) - { - PlayerName = userName, - GameName = joinGameResponse.SessionName + GameName = sessionName, + Error = "Error joining game." }; await communicationManager.BroadcastToPlayers(response, userName); } else { - var response = new JoinGameResponse(ClientAction.JoinByCode) + // Other members of the game see a regular JoinGame occur. + var response = new JoinGameResponse(ClientAction.JoinGame) { PlayerName = userName, - GameName = joinGameResponse.SessionName, - Error = "Error joining game." + GameName = sessionName + }; + // At this time, userName hasn't subscribed and won't receive this message. + await communicationManager.BroadcastToGame(sessionName, response); + + // The player joining sees the JoinByCode occur. + response = new JoinGameResponse(ClientAction.JoinByCode) + { + PlayerName = userName, + GameName = sessionName }; await communicationManager.BroadcastToPlayers(response, userName); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index c00aa64..150a137 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -23,7 +23,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { var request = JsonConvert.DeserializeObject(json); - var joinGameResponse = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession + var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession { PlayerName = userName, SessionName = request.GameName @@ -34,7 +34,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, GameName = request.GameName }; - if (joinGameResponse.JoinSucceeded) + if (joinSucceeded) { await communicationManager.BroadcastToAll(response); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index db15bfa..9efc43b 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -37,9 +37,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers var gameTask = gameboardRepository.GetGame(request.GameName); var moveTask = gameboardRepository.GetMoves(request.GameName); - var getGameResponse = await gameTask; - var getMovesResponse = await moveTask; - if (getGameResponse == null || getMovesResponse == null) + var sessionModel = await gameTask; + if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; @@ -47,17 +46,17 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } else { - var sessionModel = new Models.Session(getGameResponse.Session); - var moveModels = getMovesResponse.Moves.Select(_ => new Models.Move(_)).ToList(); + var moveModels = await moveTask; communicationManager.SubscribeToGame(sessionModel, userName); var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); + var shogiBoard = new ShogiBoard(boardMoves); + boardManager.Add(sessionModel.Name, shogiBoard); var response = new LoadGameResponse(ClientAction.LoadGame) { Game = sessionModel.ToServiceModel(), - Moves = moveModels.Select(_ => _.ToServiceModel()).ToList(), + BoardState = new Models.BoardState(shogiBoard).ToServiceModel() }; await communicationManager.BroadcastToPlayers(response, userName); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 5fe8a11..3fa5bb5 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using System.Threading.Tasks; using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; + namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class MoveHandler : IActionHandler @@ -25,31 +26,40 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - // Basic move validation - if (request.Move.To.Equals(request.Move.From)) - { - var error = new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) - { - Error = "Error: moving piece from tile to the same tile." - }; - await communicationManager.BroadcastToPlayers(error, userName); - return; - } - var moveModel = new Move(request.Move); var board = boardManager.Get(request.GameName); - var boardMove = moveModel.ToBoardModel(); - //board.Move() - await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); - - - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + if (board == null) { - GameName = request.GameName, - PlayerName = userName, - Move = moveModel.ToServiceModel() - }; - await communicationManager.BroadcastToGame(request.GameName, response); + // TODO: Find a flow for this + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + { + Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." + }; + await communicationManager.BroadcastToPlayers(response, userName); + + } + var boardMove = moveModel.ToBoardModel(); + var moveSuccess = board.Move(boardMove); + if (moveSuccess) + { + await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); + var boardState = new BoardState(board); + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + { + GameName = request.GameName, + PlayerName = userName, + BoardState = boardState.ToServiceModel() + }; + await communicationManager.BroadcastToGame(request.GameName, response); + } + else + { + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + { + Error = "Invalid move." + }; + await communicationManager.BroadcastToPlayers(response, userName); + } } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs new file mode 100644 index 0000000..1d50469 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs @@ -0,0 +1,40 @@ +using Gameboard.ShogiUI.Rules; +using System.Collections.Generic; +using System.Linq; +using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class BoardState + { + public Piece[,] Board { get; set; } + public IReadOnlyCollection Player1Hand { get; set; } + public IReadOnlyCollection Player2Hand { get; set; } + + public BoardState(ShogiBoard shogi) + { + Board = new Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + Board[x, y] = new Piece(shogi.Board[x, y]); + + Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList(); + Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); + } + + public ServiceTypes.BoardState ToServiceModel() + { + var board = new ServiceTypes.Piece[9, 9]; + Board = new Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + board[x, y] = Board[x, y].ToServiceModel(); + return new ServiceTypes.BoardState + { + Board = board, + Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), + Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() + }; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 66fda38..a597493 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,7 +1,8 @@ -using Gameboard.ShogiUI.BoardState; +using Gameboard.ShogiUI.Rules; using Microsoft.FSharp.Core; using System; using System.Numerics; +using BoardStateMove = Gameboard.ShogiUI.Rules.Move; using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Models @@ -13,7 +14,6 @@ namespace Gameboard.ShogiUI.Sockets.Models public Coords To { get; set; } public bool IsPromotion { get; set; } - public Move() { } public Move(ServiceModels.Socket.Types.Move move) { From = Coords.FromBoardNotation(move.From); @@ -74,9 +74,9 @@ namespace Gameboard.ShogiUI.Sockets.Models }; return target; } - public BoardState.Move ToBoardModel() + public BoardStateMove ToBoardModel() { - return new BoardState.Move + return new BoardStateMove { From = new Vector2(From.X, From.Y), IsPromotion = IsPromotion, diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs new file mode 100644 index 0000000..8ce5124 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -0,0 +1,27 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using BoardStatePiece = Gameboard.ShogiUI.Rules.Pieces.Piece; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class Piece + { + public WhichPiece WhichPiece { get; set; } + + public bool IsPromoted { get; set; } + + public Piece(BoardStatePiece piece) + { + WhichPiece = (WhichPiece)piece.WhichPiece; + IsPromoted = piece.IsPromoted; + } + + public ServiceModels.Socket.Types.Piece ToServiceModel() + { + return new ServiceModels.Socket.Types.Piece + { + IsPromoted = IsPromoted, + WhichPiece = WhichPiece + }; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/Player.cs b/Gameboard.ShogiUI.Sockets/Models/Player.cs new file mode 100644 index 0000000..90fa28e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/Player.cs @@ -0,0 +1,12 @@ +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class Player + { + public string Name { get; } + + public Player(string name) + { + Name = name; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json index 20f6c84..4a087fc 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json +++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json @@ -16,7 +16,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "AspShogiSockets": { + "Kestrel": { "commandName": "Project", "launchUrl": "Socket/Token", "environmentVariables": { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 25583c9..bd483eb 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -1,7 +1,10 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories.Utility; using Newtonsoft.Json; using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -11,17 +14,17 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public interface IGameboardRepository { Task DeleteGame(string gameName); - Task GetGame(string gameName); + Task GetGame(string gameName); Task GetGames(); Task GetGames(string playerName); - Task GetMoves(string gameName); - Task PostSession(PostSession request); - Task PostJoinPrivateSession(PostJoinPrivateSession request); - Task PutJoinPublicSession(PutJoinPublicSession request); + Task> GetMoves(string gameName); + Task PostSession(PostSession request); + Task PostJoinPrivateSession(PostJoinPrivateSession request); + Task PutJoinPublicSession(PutJoinPublicSession request); Task PostMove(string gameName, PostMove request); - Task PostJoinCode(string gameName, string userName); - Task GetPlayer(string userName); - Task PostPlayer(PostPlayer request); + Task PostJoinCode(string gameName, string userName); + Task GetPlayer(string userName); + Task PostPlayer(PostPlayer request); } public class GameboardRepository : IGameboardRepository @@ -52,12 +55,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return JsonConvert.DeserializeObject(json); } - public async Task GetGame(string gameName) + public async Task GetGame(string gameName) { var uri = $"Session/{gameName}"; var response = await client.GetAsync(Uri.EscapeUriString(uri)); var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + return new Session(JsonConvert.DeserializeObject(json).Session); } public async Task DeleteGame(string gameName) @@ -66,36 +73,46 @@ namespace Gameboard.ShogiUI.Sockets.Repositories await client.DeleteAsync(Uri.EscapeUriString(uri)); } - public async Task PostSession(PostSession request) + public async Task PostSession(PostSession request) { 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); + return JsonConvert.DeserializeObject(json).SessionName; } - public async Task PutJoinPublicSession(PutJoinPublicSession request) + public async Task PutJoinPublicSession(PutJoinPublicSession request) { 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); + return JsonConvert.DeserializeObject(json).JoinSucceeded; } - public async Task PostJoinPrivateSession(PostJoinPrivateSession request) + public async Task PostJoinPrivateSession(PostJoinPrivateSession request) { 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); + var deserialized = JsonConvert.DeserializeObject(json); + if (deserialized.JoinSucceeded) + { + return deserialized.SessionName; + } + return null; } - public async Task GetMoves(string gameName) + public async Task> GetMoves(string gameName) { var uri = $"Session/{gameName}/Moves"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); + var get = await client.GetAsync(Uri.EscapeUriString(uri)); + var json = await get.Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + var response = JsonConvert.DeserializeObject(json); + return response.Moves.Select(m => new Move(m)).ToList(); } public async Task PostMove(string gameName, PostMove request) @@ -105,27 +122,33 @@ namespace Gameboard.ShogiUI.Sockets.Repositories await client.PostAsync(Uri.EscapeUriString(uri), content); } - public async Task PostJoinCode(string gameName, string userName) + public async Task PostJoinCode(string gameName, string userName) { var uri = $"JoinCode/{gameName}"; var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); var content = new StringContent(serialized, Encoding.UTF8, MediaType); var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); + return JsonConvert.DeserializeObject(json).JoinCode; } - public async Task GetPlayer(string playerName) + public async Task GetPlayer(string playerName) { var uri = $"Player/{playerName}"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); + var get = await client.GetAsync(Uri.EscapeUriString(uri)); + var content = await get.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(content)) + { + var response = JsonConvert.DeserializeObject(content); + return new Player(response.Player.Name); + } + return null; } - public async Task PostPlayer(PostPlayer request) + public async Task PostPlayer(PostPlayer request) { var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - return await client.PostAsync(PlayerRoute, content); + var response = await client.PostAsync(PlayerRoute, content); + return response.IsSuccessStatusCode; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs index 55ff15a..30fed61 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs @@ -9,6 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers Task CreateGuestUser(); Task IsPlayer1(string sessionName, string playerName); bool IsGuest(string playerName); + Task PlayerExists(string playerName); } public class GameboardRepositoryManager : IGameboardRepositoryManager @@ -33,8 +34,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers { PlayerName = clientId }; - var response = await repository.PostPlayer(request); - if (response.IsSuccessStatusCode) + var isCreated = await repository.PostPlayer(request); + if (isCreated) { return clientId; } @@ -45,19 +46,21 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers public async Task IsPlayer1(string sessionName, string playerName) { var session = await repository.GetGame(sessionName); - return session?.Session.Player1 == playerName; + return session?.Player1 == playerName; } public async Task CreateJoinCode(string sessionName, string playerName) { - var getGameResponse = await repository.GetGame(sessionName); - if (playerName == getGameResponse?.Session.Player1) + var session = await repository.GetGame(sessionName); + if (playerName == session?.Player1) { - return (await repository.PostJoinCode(sessionName, playerName)).JoinCode; + return await repository.PostJoinCode(sessionName, playerName); } return null; } public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); + + public async Task PlayerExists(string playerName) => await repository.GetPlayer(playerName) != null; } } diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs similarity index 91% rename from Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs rename to Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs index 9eac527..7145e5e 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/BoardStateExtensions.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs @@ -1,10 +1,10 @@ -using Gameboard.ShogiUI.BoardState; -using Gameboard.ShogiUI.BoardState.Pieces; +using Gameboard.ShogiUI.Rules; +using Gameboard.ShogiUI.Rules.Pieces; using System; using System.Text; using System.Text.RegularExpressions; -namespace Gameboard.ShogiUI.UnitTests.BoardState +namespace Gameboard.ShogiUI.UnitTests.Rules { public static class BoardStateExtensions { @@ -32,7 +32,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var builder = new StringBuilder(); builder.Append(" Player 2(.)"); builder.AppendLine(); - for (var y = 8; y > -1; y--) + for (var y = 0; y < 9; y++) { builder.Append("- "); for (var x = 0; x < 8; x++) builder.Append("- - "); diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs similarity index 74% rename from Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs rename to Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index 1178c55..fbe26b3 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -1,12 +1,12 @@ using FluentAssertions; -using Gameboard.ShogiUI.BoardState; -using Gameboard.ShogiUI.BoardState.Pieces; +using Gameboard.ShogiUI.Rules; +using Gameboard.ShogiUI.Rules.Pieces; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; using System.Numerics; -namespace Gameboard.ShogiUI.UnitTests.BoardState +namespace Gameboard.ShogiUI.UnitTests.Rules { [TestClass] public class ShogiBoardShould @@ -22,7 +22,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // Assert Player1. for (var y = 0; y < 3; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); + board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); @@ -33,9 +33,9 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[0, 1].Should().BeNull(); - board[1, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook); for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); - board[7, 1].WhichPiece.Should().Be(WhichPiece.Rook); + board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); board[8, 1].Should().BeNull(); for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); @@ -47,7 +47,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // Assert Player2. for (var y = 6; y < 9; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); + board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); @@ -58,9 +58,9 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[0, 7].Should().BeNull(); - board[1, 7].WhichPiece.Should().Be(WhichPiece.Rook); + board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); - board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook); board[8, 7].Should().BeNull(); for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); } @@ -73,13 +73,13 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState new Move { // Pawn - From = new Vector2(0, 2), - To = new Vector2(0, 3) + From = new Vector2(0, 6), + To = new Vector2(0, 5) } }; var shogi = new ShogiBoard(moves); - shogi.Board[0, 2].Should().BeNull(); - shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[0, 6].Should().BeNull(); + shogi.Board[0, 5].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -106,11 +106,11 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var shogi = new ShogiBoard(); // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 2), To = new Vector2(0, 2) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 6), To = new Vector2(0, 6) }); // Assert moveSuccess.Should().BeFalse(); - shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[0, 6].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -136,9 +136,11 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState { // Arrange var shogi = new ShogiBoard(); + shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + shogi.Board[8, 2].Owner.Should().Be(WhichPlayer.Player2); // Act - Move Player2 Pawn when it's Player1 turn. - var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 6), To = new Vector2(8, 5) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }); // Assert moveSuccess.Should().BeFalse(); @@ -152,8 +154,8 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var invalidLanceMove = new Move { // Lance moving through the pawn before it. - From = new Vector2(0, 0), - To = new Vector2(0, 5) + From = new Vector2(0, 8), + To = new Vector2(0, 4) }; var shogi = new ShogiBoard(); @@ -170,8 +172,8 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var invalidKnightMove = new Move { // Knight capturing allied Pawn - From = new Vector2(1, 0), - To = new Vector2(0, 2) + From = new Vector2(1, 8), + To = new Vector2(0, 6) }; var shogi = new ShogiBoard(); @@ -190,11 +192,11 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, + new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, // P1 Bishop puts P2 in check - new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) } + new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) } }; var shogi = new ShogiBoard(moves); @@ -202,7 +204,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState shogi.InCheck.Should().Be(WhichPlayer.Player2); // Act - P2 moves Lance while remaining in check. - var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 8), To = new Vector2(8, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }); // Assert moveSuccess.Should().BeFalse(); @@ -218,61 +220,62 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, + new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, // P1 Bishop takes P2 Pawn - new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, // P2 Gold, block check from P1 Bishop. - new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - new Move { From = new Vector2(6, 6), To = new Vector2(7, 7), IsPromotion = true }, + new Move { From = new Vector2(6, 2), To = new Vector2(7, 1), IsPromotion = true }, // P2 Pawn again - new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, + new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, // P1 Bishop takes P2 Knight - new Move { From = new Vector2(7, 7), To = new Vector2(7, 8) }, + new Move { From = new Vector2(7, 1), To = new Vector2(7, 0) }, // P2 Pawn again - new Move { From = new Vector2(0, 4), To = new Vector2(0, 3) }, + new Move { From = new Vector2(0, 4), To = new Vector2(0, 5) }, // P1 Bishop takes P2 Lance - new Move { From = new Vector2(7, 8), To = new Vector2(8, 8) }, + new Move { From = new Vector2(7, 0), To = new Vector2(8, 0) }, // P2 Lance (move to make room for attempted P1 Pawn placement) - new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }, + new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, // P1 arbitrary move - new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, + new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, // P2 Pawn again, takes P1 Pawn - new Move { From = new Vector2(0, 3), To = new Vector2(0, 2) }, + new Move { From = new Vector2(0, 5), To = new Vector2(0, 6) }, }; var shogi = new ShogiBoard(moves); - shogi.PrintStateAsAscii(); // Prerequisites + shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); // Act | Assert - It is P1 turn /// try illegally placing Knight from the hand. - shogi.Board[7, 8].Should().BeNull(); - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) }); + shogi.Board[7, 0].Should().BeNull(); + var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 8].Should().BeNull(); - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 7) }); + shogi.Board[7, 0].Should().BeNull(); + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 1) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 7].Should().BeNull(); + shogi.Board[7, 1].Should().BeNull(); /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 8) }); + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Board[7, 8].Should().BeNull(); + shogi.Board[7, 0].Should().BeNull(); /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 8) }); + dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 8].Should().BeNull(); + shogi.Board[7, 0].Should().BeNull(); } [TestMethod] @@ -282,25 +285,25 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(8, 6), To = new Vector2(8, 5) }, + new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }, // P1 Bishop, check - new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, // P2 Gold, block check - new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, // P1 arbitrary move - new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, + new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, // P2 Bishop - new Move { From = new Vector2(7, 7), To = new Vector2(8, 6) }, + new Move { From = new Vector2(7, 1), To = new Vector2(8, 2) }, // P1 Bishop takes P2 Lance - new Move { From = new Vector2(6, 6), To = new Vector2(8, 8) }, + new Move { From = new Vector2(6, 2), To = new Vector2(8, 0) }, // P2 Bishop - new Move { From = new Vector2(8, 6), To = new Vector2(7, 7) }, + new Move { From = new Vector2(8, 2), To = new Vector2(7, 1) }, // P1 arbitrary move - new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, + new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, // P2 Bishop, check - new Move { From = new Vector2(7, 7), To = new Vector2(2, 2) }, + new Move { From = new Vector2(7, 1), To = new Vector2(2, 6) }, }; var shogi = new ShogiBoard(moves); @@ -325,22 +328,23 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, + new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, // P1 Bishop, capture P2 Pawn, check - new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }, + new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, // P2 Gold, block check - new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) }, + new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, // P1 Bishop capture P2 Bishop - new Move { From = new Vector2(6, 6), To = new Vector2(7, 7) }, + new Move { From = new Vector2(6, 2), To = new Vector2(7, 1) }, // P2 arbitrary move - new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }, + new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, }; var shogi = new ShogiBoard(moves); // Prerequisites shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board[4, 0].Should().NotBeNull(); // Act - P1 tries to place Bishop from hand to an already-occupied position var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) }); @@ -358,16 +362,14 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, + new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, }; var shogi = new ShogiBoard(moves); - shogi.PrintStateAsAscii(); - // Act - P1 Bishop, check - shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) }); + shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }); // Assert shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -380,14 +382,14 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } + new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } }; var shogi = new ShogiBoard(moves); // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(7, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(7, 1) }); // Assert moveSuccess.Should().BeTrue(); @@ -396,20 +398,20 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) .Should() .Be(1); - shogi.Board[1, 1].Should().BeNull(); - shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board[1, 7].Should().BeNull(); + shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); shogi.Hands[WhichPlayer.Player1] .Should() .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); // Act - P2 Silver captures P1 Bishop - moveSuccess = shogi.Move(new Move { From = new Vector2(6, 8), To = new Vector2(7, 7) }); + moveSuccess = shogi.Move(new Move { From = new Vector2(6, 0), To = new Vector2(7, 1) }); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[6, 8].Should().BeNull(); - shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + shogi.Board[6, 0].Should().BeNull(); + shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.SilverGeneral); shogi.Board .Cast() .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) @@ -426,19 +428,19 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) }, + new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, // P2 Pawn - new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } + new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } }; var shogi = new ShogiBoard(moves); // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6), IsPromotion = true }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2), IsPromotion = true }); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[1, 1].Should().BeNull(); - shogi.Board[6, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); + shogi.Board[1, 7].Should().BeNull(); + shogi.Board[6, 2].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); } [TestMethod] @@ -448,32 +450,30 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState var moves = new[] { // P1 Rook - new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, + new Move { From = new Vector2(7, 7), To = new Vector2(4, 7) }, // P2 Gold - new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, + new Move { From = new Vector2(3, 0), To = new Vector2(2, 1) }, // P1 Pawn - new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, - // P2 other Gold - new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, - // P1 same Pawn - new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, - // P2 Pawn new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, + // P2 other Gold + new Move { From = new Vector2(5, 0), To = new Vector2(6, 1) }, + // P1 same Pawn + new Move { From = new Vector2(4, 5), To = new Vector2(4, 4) }, + // P2 Pawn + new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, // P1 Pawn takes P2 Pawn - new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, + new Move { From = new Vector2(4, 4), To = new Vector2(4, 3) }, // P2 King - new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, + new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, // P1 Pawn promotes, threatens P2 King - new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, + new Move { From = new Vector2(4, 3), To = new Vector2(4, 2), IsPromotion = true }, // P2 King retreat - new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, + new Move { From = new Vector2(4, 1), To = new Vector2(4, 0) }, }; var shogi = new ShogiBoard(moves); - Console.WriteLine("Prereq"); - shogi.PrintStateAsAscii(); // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); + var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 2), To = new Vector2(4, 1) }); // Assert - checkmate moveSuccess.Should().BeTrue(); diff --git a/PathFinding/Direction.cs b/PathFinding/Direction.cs index 2ee825d..8a199e4 100644 --- a/PathFinding/Direction.cs +++ b/PathFinding/Direction.cs @@ -4,15 +4,15 @@ namespace PathFinding { public static class Direction { - public static readonly Vector2 Up = new(0, 1); - public static readonly Vector2 Down = new(0, -1); + public static readonly Vector2 Up = new(0, -1); + public static readonly Vector2 Down = new(0, 1); public static readonly Vector2 Left = new(-1, 0); public static readonly Vector2 Right = new(1, 0); - public static readonly Vector2 UpLeft = new(-1, 1); - public static readonly Vector2 UpRight = new(1, 1); - public static readonly Vector2 DownLeft = new(-1, -1); - public static readonly Vector2 DownRight = new(1, -1); - public static readonly Vector2 KnightLeft = new(-1, 2); - public static readonly Vector2 KnightRight = new(1, 2); + public static readonly Vector2 UpLeft = new(-1, -1); + public static readonly Vector2 UpRight = new(1, -1); + public static readonly Vector2 DownLeft = new(-1, 1); + public static readonly Vector2 DownRight = new(1, 1); + public static readonly Vector2 KnightLeft = new(-1, -2); + public static readonly Vector2 KnightRight = new(1, -2); } } diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index ae32b6b..f2c9323 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -6,8 +6,6 @@ namespace PathFinding { public class PathFinder2D where T : IPlanarElement { - /// - /// /// Guaranteed to be non-null. /// public delegate void Callback(T collider, Vector2 position); @@ -108,14 +106,8 @@ namespace PathFinding public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); - public static bool IsPathable(Vector2 origin, Vector2 destination, T element) - { - var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); - return IsPathable(origin, destination, path.Direction); - } public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) { - direction = Vector2.Normalize(direction); var next = Vector2.Add(origin, direction); if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; From f8f779e84cc9f37c1b4d87f573b336312f3b3d21 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 8 May 2021 10:26:04 -0500 Subject: [PATCH 14/27] before deleting Rules --- Benchmarking/Benchmarking.csproj | 2 +- Benchmarking/Benchmarks.cs | 52 ++-- .../CouchDB.csproj | 4 + CouchDB/CouchDocument.cs | 16 ++ CouchDB/Selectors/CouchQuery.cs | 27 ++ CouchDB/Selectors/Equals.cs | 18 ++ Gameboard.ShogiUI.BoardState/Move.cs | 14 -- Gameboard.ShogiUI.Domain/Entities/Board.cs | 6 - Gameboard.ShogiUI.Domain/Entities/Match.cs | 30 --- .../ValueObjects/Class1.cs | 15 -- .../Gameboard.ShogiUI.Rules.csproj | 1 + Gameboard.ShogiUI.Rules/Move.cs | 27 ++ .../Pieces/Bishop.cs | 0 .../Pieces/GoldGeneral.cs | 2 +- .../Pieces/King.cs | 0 .../Pieces/Knight.cs | 0 .../Pieces/Lance.cs | 0 .../Pieces/Pawn.cs | 0 .../Pieces/Piece.cs | 2 +- .../Pieces/Rook.cs | 0 .../Pieces/SilverGeneral.cs | 0 .../PlanarCollection.cs | 6 +- .../ShogiBoard.cs | 39 +-- .../WhichPiece.cs | 2 +- .../WhichPlayer.cs | 0 .../Api/Messages/GetGuestToken.cs | 2 +- .../Api/Messages/PostSession.cs | 10 + ...board.ShogiUI.Sockets.ServiceModels.csproj | 1 + .../Socket/Interfaces/IRequest.cs | 2 +- .../Socket/Messages/CreateGame.cs | 7 +- .../Socket/Messages/ErrorResponse.cs | 16 -- .../Socket/Messages/JoinByCode.cs | 11 - .../Socket/Messages/JoinGame.cs | 8 +- .../Socket/Messages/ListGames.cs | 2 +- .../Socket/Messages/LoadGame.cs | 3 +- .../Socket/Messages/Move.cs | 8 +- .../Socket/Types/BoardState.cs | 9 +- .../Socket/Types/ClientActionEnum.cs | 3 +- .../Socket/Types/Game.cs | 7 +- .../Socket/Types/Move.cs | 6 +- .../Socket/Types/Piece.cs | 4 +- .../Socket/Types/WhichPlayer.cs | 8 + Gameboard.ShogiUI.Sockets.sln | 22 +- .../Controllers/GameController.cs | 33 ++- .../Controllers/SocketController.cs | 18 +- .../Gameboard.ShogiUI.Sockets.csproj | 13 +- .../Managers/BoardManager.cs | 6 +- .../ClientActionHandlers/CreateGameHandler.cs | 60 ++--- .../ClientActionHandlers/IActionHandler.cs | 15 -- .../ClientActionHandlers/JoinByCodeHandler.cs | 81 +++--- .../ClientActionHandlers/JoinGameHandler.cs | 53 ++-- .../ClientActionHandlers/ListGamesHandler.cs | 23 +- .../ClientActionHandlers/LoadGameHandler.cs | 52 ++-- .../ClientActionHandlers/MoveHandler.cs | 81 +++--- .../Managers/SocketCommunicationManager.cs | 90 ++----- .../Managers/SocketConnectionManager.cs | 105 +++++++- .../Managers/SocketTokenManager.cs | 25 +- .../Managers/Utility/Request.cs | 1 - .../Models/BoardState.cs | 38 ++- Gameboard.ShogiUI.Sockets/Models/Move.cs | 89 ++----- Gameboard.ShogiUI.Sockets/Models/Piece.cs | 20 +- Gameboard.ShogiUI.Sockets/Models/Session.cs | 19 +- .../Repositories/CouchModels/BoardState.cs | 75 ++++++ .../CouchModels/CouchCreatedResult.cs | 15 ++ .../Repositories/CouchModels/CouchDocument.cs | 25 ++ .../CouchModels/CouchFindResult.cs | 14 ++ .../Repositories/CouchModels/Move.cs | 45 ++++ .../Repositories/CouchModels/Piece.cs | 27 ++ .../Repositories/CouchModels/Readme.md | 4 + .../Repositories/CouchModels/Session.cs | 30 +++ .../Repositories/CouchModels/User.cs | 23 ++ .../Repositories/GameboardRepository.cs | 233 ++++++++++-------- .../GameboardRepositoryManager.cs | 37 +-- .../Utility/AuthenticatedHttpClient.cs | 122 --------- Gameboard.ShogiUI.Sockets/Startup.cs | 43 ++-- Gameboard.ShogiUI.Sockets/appsettings.json | 9 +- .../Rules/BoardStateExtensions.cs | 2 +- .../Rules/ShogiBoardShould.cs | 20 +- PathFinding/IPlanarCollection.cs | 2 +- PathFinding/PathFinding.csproj | 1 + 80 files changed, 1109 insertions(+), 832 deletions(-) rename Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj => CouchDB/CouchDB.csproj (67%) create mode 100644 CouchDB/CouchDocument.cs create mode 100644 CouchDB/Selectors/CouchQuery.cs create mode 100644 CouchDB/Selectors/Equals.cs delete mode 100644 Gameboard.ShogiUI.BoardState/Move.cs delete mode 100644 Gameboard.ShogiUI.Domain/Entities/Board.cs delete mode 100644 Gameboard.ShogiUI.Domain/Entities/Match.cs delete mode 100644 Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Gameboard.ShogiUI.Rules.csproj (90%) create mode 100644 Gameboard.ShogiUI.Rules/Move.cs rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Bishop.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/GoldGeneral.cs (97%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/King.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Knight.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Lance.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Pawn.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Piece.cs (95%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Rook.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/SilverGeneral.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/PlanarCollection.cs (92%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/ShogiBoard.cs (93%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/WhichPiece.cs (89%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/WhichPlayer.cs (100%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index ec8dd8e..d84d541 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -11,7 +11,7 @@ - + diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index b5785ed..31f6599 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -16,33 +16,33 @@ namespace Benchmarking public Benchmarks() { - moves = new[] - { - // P1 Rook - new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, - // P2 Gold - new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, - // P1 Pawn - new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, - // P2 other Gold - new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, - // P1 same Pawn - new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, - // P2 Pawn - new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, - // P1 Pawn takes P2 Pawn - new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, - // P2 King - new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, - // P1 Pawn promotes - new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, - // P2 King retreat - new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, - }; - var rand = new Random(); + //moves = new[] + //{ + // // P1 Rook + // new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, + // // P2 Gold + // new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, + // // P1 Pawn + // new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, + // // P2 other Gold + // new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, + // // P1 same Pawn + // new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, + // // P2 Pawn + // new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, + // // P1 Pawn takes P2 Pawn + // new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, + // // P2 King + // new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, + // // P1 Pawn promotes + // new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, + // // P2 King retreat + // new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, + //}; + //var rand = new Random(); - directions = new Vector2[10]; - for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); + //directions = new Vector2[10]; + //for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); } //[Benchmark] diff --git a/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj b/CouchDB/CouchDB.csproj similarity index 67% rename from Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj rename to CouchDB/CouchDB.csproj index f208d30..83f1b25 100644 --- a/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj +++ b/CouchDB/CouchDB.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/CouchDB/CouchDocument.cs b/CouchDB/CouchDocument.cs new file mode 100644 index 0000000..8f29f6c --- /dev/null +++ b/CouchDB/CouchDocument.cs @@ -0,0 +1,16 @@ +namespace CouchDB +{ + public class CouchDocument + { + public readonly string _id; + public readonly string type; + public readonly T model; + + public CouchDocument(string id, T model) + { + _id = id; + this.model = model; + type = nameof(T); + } + } +} diff --git a/CouchDB/Selectors/CouchQuery.cs b/CouchDB/Selectors/CouchQuery.cs new file mode 100644 index 0000000..7bd7b29 --- /dev/null +++ b/CouchDB/Selectors/CouchQuery.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace CouchDB.Selectors +{ + public class CouchQuery + { + public static CouchQuery Select => new(); + + private readonly List equals; + protected CouchQuery() + { + equals = new List(); + } + + public CouchQuery WithEqual(string key, string value) + { + equals.Add(new Equals(key, value)); + return this; + } + + public override string ToString() + { + var selector = string.Join(",", equals); + return $"{{ \"selector\": {selector}"; + } + } +} diff --git a/CouchDB/Selectors/Equals.cs b/CouchDB/Selectors/Equals.cs new file mode 100644 index 0000000..418f547 --- /dev/null +++ b/CouchDB/Selectors/Equals.cs @@ -0,0 +1,18 @@ +namespace CouchDB.Selectors +{ + public class Equals + { + private readonly string key; + private readonly string value; + internal Equals(string key, string value) + { + this.key = key; + this.value = value; + } + + public override string ToString() + { + return $"{{ \"{key}\": {{ \"$eq\": {value}}} }}"; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs deleted file mode 100644 index 693c954..0000000 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics; -using System.Numerics; - -namespace Gameboard.ShogiUI.Rules -{ - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public WhichPiece? PieceFromCaptured { get; set; } - public Vector2 From { get; set; } - public Vector2 To { get; set; } - public bool IsPromotion { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Domain/Entities/Board.cs b/Gameboard.ShogiUI.Domain/Entities/Board.cs deleted file mode 100644 index 9533d8d..0000000 --- a/Gameboard.ShogiUI.Domain/Entities/Board.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Gameboard.ShogiUI.Domain -{ - public class Board - { - } -} diff --git a/Gameboard.ShogiUI.Domain/Entities/Match.cs b/Gameboard.ShogiUI.Domain/Entities/Match.cs deleted file mode 100644 index 2d54945..0000000 --- a/Gameboard.ShogiUI.Domain/Entities/Match.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Gameboard.ShogiUI.Domain -{ - public class Match - { - public string Name { get; } - public string Player1 { get; } - public string Player2 { get; } - - /// - /// Initialize pre-existing Match. - /// - public Match(MatchMeta meta, Board board) - { - Name = meta.Name; - Player1 = meta.Player1; - Player2 = meta.Player2; - } - - /// - /// Create a new Match. - /// - public Match(string name, string player1) - { - Name = name; - Player1 = player1; - } - - - } -} diff --git a/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs b/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs deleted file mode 100644 index 6939def..0000000 --- a/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Domain -{ - public class MatchMeta - { - public string Name { get; } - public string Player1 { get; } - public string Player2 { get; } - } -} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj similarity index 90% rename from Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj rename to Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj index 1a2ca2c..5befd78 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj +++ b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable diff --git a/Gameboard.ShogiUI.Rules/Move.cs b/Gameboard.ShogiUI.Rules/Move.cs new file mode 100644 index 0000000..81286fe --- /dev/null +++ b/Gameboard.ShogiUI.Rules/Move.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Gameboard.ShogiUI.Rules +{ + [DebuggerDisplay("{From} - {To}")] + public class Move + { + public WhichPiece? PieceFromHand { get; } + public Vector2? From { get; } + public Vector2 To { get; } + public bool IsPromotion { get; } + + public Move(Vector2 from, Vector2 to, bool isPromotion) + { + From = from; + To = to; + IsPromotion = isPromotion; + } + + public Move(WhichPiece pieceFromHand, Vector2 to) + { + PieceFromHand = pieceFromHand; + To = to; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs rename to Gameboard.ShogiUI.Rules/Pieces/Bishop.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs similarity index 97% rename from Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs rename to Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs index 055b779..fa984ab 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs +++ b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs @@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces new PathFinding.Move(Direction.Right), new PathFinding.Move(Direction.Down) }; - public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) + public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner) { moveSet = new MoveSet(this, Moves); promotedMoveSet = new MoveSet(this, Moves); diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.Rules/Pieces/King.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/King.cs rename to Gameboard.ShogiUI.Rules/Pieces/King.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.Rules/Pieces/Knight.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Knight.cs rename to Gameboard.ShogiUI.Rules/Pieces/Knight.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.Rules/Pieces/Lance.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Lance.cs rename to Gameboard.ShogiUI.Rules/Pieces/Lance.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs rename to Gameboard.ShogiUI.Rules/Pieces/Pawn.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs similarity index 95% rename from Gameboard.ShogiUI.BoardState/Pieces/Piece.cs rename to Gameboard.ShogiUI.Rules/Pieces/Piece.cs index 1f019b2..b64584b 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs +++ b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs @@ -25,7 +25,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces public bool CanPromote => !IsPromoted && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldenGeneral; + && WhichPiece != WhichPiece.GoldGeneral; public void ToggleOwnership() { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.Rules/Pieces/Rook.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Rook.cs rename to Gameboard.ShogiUI.Rules/Pieces/Rook.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs rename to Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs diff --git a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs b/Gameboard.ShogiUI.Rules/PlanarCollection.cs similarity index 92% rename from Gameboard.ShogiUI.BoardState/PlanarCollection.cs rename to Gameboard.ShogiUI.Rules/PlanarCollection.cs index 4baf867..d606134 100644 --- a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs +++ b/Gameboard.ShogiUI.Rules/PlanarCollection.cs @@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Rules public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { public delegate void ForEachDelegate(T element, int x, int y); - private readonly T[] array; + private readonly T?[] array; private readonly int width; private readonly int height; @@ -19,12 +19,12 @@ namespace Gameboard.ShogiUI.Rules array = new T[width * height]; } - public T this[int x, int y] + public T? this[int x, int y] { get => array[y * width + x]; set => array[y * width + x] = value; } - public T this[float x, float y] + public T? this[float x, float y] { get => array[(int)y * width + (int)x]; set => array[(int)y * width + (int)x] = value; diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.Rules/ShogiBoard.cs similarity index 93% rename from Gameboard.ShogiUI.BoardState/ShogiBoard.cs rename to Gameboard.ShogiUI.Rules/ShogiBoard.cs index 0449668..e5f104e 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.Rules/ShogiBoard.cs @@ -14,9 +14,8 @@ namespace Gameboard.ShogiUI.Rules public class ShogiBoard { private delegate void MoveSetCallback(Piece piece, Vector2 position); - private readonly bool isValidationBoard; private readonly PathFinder2D pathFinder; - private ShogiBoard validationBoard; + private ShogiBoard? validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } @@ -25,7 +24,6 @@ namespace Gameboard.ShogiUI.Rules public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } public bool IsCheckmate { get; private set; } - public string Error { get; private set; } @@ -41,6 +39,7 @@ namespace Gameboard.ShogiUI.Rules InitializeBoardState(); player1King = new Vector2(4, 8); player2King = new Vector2(4, 0); + Error = string.Empty; } public ShogiBoard(IList moves) : this() @@ -57,11 +56,16 @@ namespace Gameboard.ShogiUI.Rules private ShogiBoard(ShogiBoard toCopy) { - isValidationBoard = true; Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - Board[x, y] = toCopy.Board[x, y]?.DeepClone(); + { + var piece = toCopy.Board[x, y]; + if (piece != null) + { + Board[x, y] = piece.DeepClone(); + } + } pathFinder = new PathFinder2D(Board); MoveHistory = new List(toCopy.MoveHistory); @@ -72,6 +76,7 @@ namespace Gameboard.ShogiUI.Rules }; player1King = toCopy.player1King; player2King = toCopy.player2King; + Error = toCopy.Error; } public bool Move(Move move) @@ -103,7 +108,7 @@ namespace Gameboard.ShogiUI.Rules validationBoard = new ShogiBoard(this); } - var isValid = move.PieceFromCaptured.HasValue + var isValid = move.PieceFromHand.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); if (!isValid) @@ -123,20 +128,20 @@ namespace Gameboard.ShogiUI.Rules } // The move is valid and legal; update board state. - if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); + if (move.PieceFromHand.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } /// True if the move was successful. private bool PlaceFromHand(Move move) { - if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move - var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); + if (move.PieceFromHand.HasValue == false) return false; //Invalid move + var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) return false; // Invalid move if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; - switch (move.PieceFromCaptured.Value) + switch (move.PieceFromHand.Value) { case WhichPiece.Knight: // Knight cannot be placed onto the farthest two ranks from the hand. @@ -160,7 +165,7 @@ namespace Gameboard.ShogiUI.Rules /// True if the move was successful. private bool PlaceFromBoard(Move move) { - var fromPiece = Board[move.From.X, move.From.Y]; + var fromPiece = Board[move.From.Value.X, move.From.Value.Y]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; @@ -171,7 +176,7 @@ namespace Gameboard.ShogiUI.Rules Error = "Not allowed to move the opponents piece"; return false; // Invalid move; cannot move other players pieces. } - if (IsPathable(move.From, move.To) == false) + if (IsPathable(move.From.Value, move.To) == false) { Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; return false; // Invalid move; move not part of move-set. @@ -188,17 +193,17 @@ namespace Gameboard.ShogiUI.Rules //Mutate the board. if (move.IsPromotion) { - if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Y < 3)) + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Value.Y < 3)) { fromPiece.Promote(); } - else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Y > 5)) + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5)) { fromPiece.Promote(); } } Board[move.To.X, move.To.Y] = fromPiece; - Board[move.From.X, move.From.Y] = null; + Board[move.From.Value.X, move.From.Value.Y] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) @@ -239,7 +244,7 @@ namespace Gameboard.ShogiUI.Rules if (pathFinder.PathTo(move.To, kingPosition)) return true; // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From); + var direction = Vector2.Subtract(kingPosition, move.From.Value); var slope = Math.Abs(direction.Y / direction.X); // If absolute slope is 45°, look for a bishop along the line. // If absolute slope is 0° or 90°, look for a rook along the line. @@ -304,7 +309,7 @@ namespace Gameboard.ShogiUI.Rules pathFinder.PathEvery(from, (other, position) => { if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveToTry = new Move { From = from, To = position }; + var moveToTry = new Move(from, position, false); var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { diff --git a/Gameboard.ShogiUI.BoardState/WhichPiece.cs b/Gameboard.ShogiUI.Rules/WhichPiece.cs similarity index 89% rename from Gameboard.ShogiUI.BoardState/WhichPiece.cs rename to Gameboard.ShogiUI.Rules/WhichPiece.cs index ec4fd4d..5e6356c 100644 --- a/Gameboard.ShogiUI.BoardState/WhichPiece.cs +++ b/Gameboard.ShogiUI.Rules/WhichPiece.cs @@ -3,7 +3,7 @@ public enum WhichPiece { King, - GoldenGeneral, + GoldGeneral, SilverGeneral, Bishop, Rook, diff --git a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs b/Gameboard.ShogiUI.Rules/WhichPlayer.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/WhichPlayer.cs rename to Gameboard.ShogiUI.Rules/WhichPlayer.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs index 6f6a751..6049f8e 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs @@ -4,7 +4,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages { public class GetGuestToken { - public string ClientId { get; set; } + public string? ClientId { get; set; } } public class GetGuestTokenResponse diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs new file mode 100644 index 0000000..8b7061a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs @@ -0,0 +1,10 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages +{ + public class PostSession + { + public string Name { get; set; } + public string Player1 { get; set; } + public string Player2 { get; set; } + public bool IsPrivate { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj index ee29921..4466c3d 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs index 79ec262..79f20e5 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs @@ -4,6 +4,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces { public interface IRequest { - ClientAction Action { get; set; } + ClientAction Action { get; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs index 09d7455..9fa701a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs @@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class CreateGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = string.Empty; public bool IsPrivate { get; set; } } public class CreateGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public Game Game { get; set; } public string PlayerName { get; set; } @@ -20,6 +20,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public CreateGameResponse(ClientAction action) { Action = action.ToString(); + Error = string.Empty; + Game = new Game(); + PlayerName = string.Empty; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs deleted file mode 100644 index 376ffc8..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages -{ - public class ErrorResponse : IResponse - { - public string Action { get; private set; } - public string Error { get; set; } - - public ErrorResponse(ClientAction action) - { - Action = action.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs deleted file mode 100644 index 51f2f8d..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages -{ - public class JoinByCode : IRequest - { - public ClientAction Action { get; set; } - public string JoinCode { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs index ef8842d..b662cb1 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs @@ -3,6 +3,12 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { + public class JoinByCodeRequest : IRequest + { + public ClientAction Action { get; set; } + public string JoinCode { get; set; } + } + public class JoinGameRequest : IRequest { public ClientAction Action { get; set; } @@ -11,7 +17,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class JoinGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public string GameName { get; set; } public string PlayerName { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index c45d51c..ab9d67f 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -11,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class ListGamesResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public ICollection Games { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index 981ebf6..a879953 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { @@ -12,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class LoadGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public Game Game { get; set; } public BoardState BoardState { get; set; } public string Error { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs index e46f9a0..3ce4c6e 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs @@ -6,8 +6,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class MoveRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } - public Move Move { get; set; } + public string GameName { get; set; } = string.Empty; + public Move Move { get; set; } = new Move(); } public class MoveResponse : IResponse @@ -21,6 +21,10 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public MoveResponse(ClientAction action) { Action = action.ToString(); + Error = string.Empty; + GameName = string.Empty; + BoardState = new BoardState(); + PlayerName = string.Empty; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs index b42c398..6ee3051 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class BoardState { - public Piece[,] Board { get; set; } - public IReadOnlyCollection Player1Hand { get; set; } - public IReadOnlyCollection Player2Hand { get; set; } + public Piece[,] Board { get; set; } = new Piece[0, 0]; + public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); + public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs index 7b91157..5215171 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs @@ -7,7 +7,6 @@ JoinGame, JoinByCode, LoadGame, - Move, - KeepAlive + Move } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index 3f5ddc6..8ff5fb1 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class Game { - public string GameName { get; set; } + public string GameName { get; set; } = string.Empty; /// /// Players[0] is the session owner, Players[1] is the other guy /// - public IReadOnlyList Players { get; set; } + public IReadOnlyList Players { get; set; } = Array.Empty(); } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs index 3ad3719..ef9723a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs @@ -2,11 +2,11 @@ { public class Move { - public string PieceFromCaptured { get; set; } + public WhichPiece? PieceFromCaptured { get; set; } /// Board position notation, like A3 or G1 - public string From { get; set; } + public string? From { get; set; } /// Board position notation, like A3 or G1 - public string To { get; set; } + public string To { get; set; } = string.Empty; public bool IsPromotion { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs index 8f9fd23..71d9dbc 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs @@ -2,8 +2,8 @@ { public class Piece { - public WhichPiece WhichPiece { get; set; } - public bool IsPromoted { get; set; } + public WhichPiece WhichPiece { get; set; } + public WhichPlayer Owner { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs new file mode 100644 index 0000000..835b8e6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 76d0058..a1576c2 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -9,15 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Domain", "Gameboard.ShogiUI.Domain\Gameboard.ShogiUI.Domain.csproj", "{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,10 +33,6 @@ Global {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.Build.0 = Release|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -49,10 +45,14 @@ Global {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.Build.0 = Release|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 72a1fa8..7900ed2 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,4 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; @@ -14,17 +15,20 @@ namespace Gameboard.ShogiUI.Sockets.Controllers public class GameController : ControllerBase { private readonly IGameboardRepositoryManager manager; + private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; public GameController( IGameboardRepository repository, - IGameboardRepositoryManager manager) + IGameboardRepositoryManager manager, + ISocketCommunicationManager communicationManager) { this.manager = manager; + this.communicationManager = communicationManager; this.repository = repository; } - [Route("JoinCode")] + [HttpPost("JoinCode")] public async Task PostGameInvitation([FromBody] PostGameInvitation request) { var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; @@ -41,7 +45,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } [AllowAnonymous] - [Route("GuestJoinCode")] + [HttpPost("GuestJoinCode")] public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) { @@ -57,5 +61,26 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return new UnauthorizedResult(); } } + + // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. + //[Route("")] + //public async Task PostSession([FromBody] PostSession request) + //{ + // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); + // var success = await repository.CreateSession(model); + // if (success) + // { + // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Socket.Types.ClientAction.CreateGame) + // { + // Game = model.ToServiceModel(), + // PlayerName = + // } + // var task = request.IsPrivate + // ? communicationManager.BroadcastToPlayers(response, userName) + // : communicationManager.BroadcastToAll(response); + // return new CreatedResult("", null); + // } + // return new ConflictResult(); + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 4891963..f00b5bf 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,8 +1,10 @@ using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using System.Linq; using System.Threading.Tasks; @@ -13,18 +15,24 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [ApiController] public class SocketController : ControllerBase { + private readonly ILogger logger; private readonly ISocketTokenManager tokenManager; private readonly IGameboardRepositoryManager gameboardManager; + private readonly IGameboardRepository gameboardRepository; public SocketController( - ISocketTokenManager tokenManager, - IGameboardRepositoryManager gameboardManager) + ILogger logger, + ISocketTokenManager tokenManager, + IGameboardRepositoryManager gameboardManager, + IGameboardRepository gameboardRepository) { + this.logger = logger; this.tokenManager = tokenManager; this.gameboardManager = gameboardManager; + this.gameboardRepository = gameboardRepository; } - [Route("Token")] + [HttpGet("Token")] public IActionResult GetToken() { var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; @@ -33,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } [AllowAnonymous] - [Route("GuestToken")] + [HttpGet("GuestToken")] public async Task GetGuestToken([FromQuery] GetGuestToken request) { if (request.ClientId == null) @@ -44,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } else { - if (await gameboardManager.PlayerExists(request.ClientId)) + if (await gameboardRepository.IsGuestUser(request.ClientId)) { var token = tokenManager.GenerateToken(request.ClientId); return new JsonResult(new GetGuestTokenResponse(request.ClientId, token)); diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index dd477c5..c558037 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -4,10 +4,18 @@ net5.0 true 5 + enable - + + + + + + + + @@ -17,7 +25,8 @@ - + + diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs index 813887e..f153527 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -6,7 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers public interface IBoardManager { void Add(string sessionName, ShogiBoard board); - ShogiBoard Get(string sessionName); + ShogiBoard? Get(string sessionName); } public class BoardManager : IBoardManager @@ -20,10 +20,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); - public ShogiBoard Get(string sessionName) + public ShogiBoard? Get(string sessionName) { if (Boards.TryGetValue(sessionName, out var board)) + { return board; + } return null; } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index bd93ab6..8ff9dd1 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,61 +1,55 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface ICreateGameHandler + { + Task Handle(CreateGameRequest request, string userName); + } + // TODO: This doesn't need to be a socket action. // It can be an API route and still tell socket connections about the new session. - public class CreateGameHandler : IActionHandler + public class CreateGameHandler : ICreateGameHandler { - private readonly IGameboardRepository repository; + private readonly IGameboardRepositoryManager manager; private readonly ISocketCommunicationManager communicationManager; public CreateGameHandler( ISocketCommunicationManager communicationManager, - IGameboardRepository repository) + IGameboardRepositoryManager manager) { - this.repository = repository; + this.manager = manager; this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(CreateGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var sessionName = await repository.PostSession(new PostSession + var model = new Session(request.GameName, request.IsPrivate, userName); + var success = await manager.CreateSession(model); + + if (!success) { - SessionName = request.GameName, - PlayerName = userName, - IsPrivate = request.IsPrivate - }); + var error = new CreateGameResponse(request.Action) + { + Error = "Unable to create game with this name." + }; + await communicationManager.BroadcastToPlayers(error, userName); + } var response = new CreateGameResponse(request.Action) { PlayerName = userName, - Game = new Game - { - GameName = sessionName, - Players = new[] { userName } - } + Game = model.ToServiceModel() }; - if (string.IsNullOrWhiteSpace(sessionName)) - { - response.Error = "Game already exists."; - } + var task = request.IsPrivate + ? communicationManager.BroadcastToPlayers(response, userName) + : communicationManager.BroadcastToAll(response); - if (request.IsPrivate) - { - await communicationManager.BroadcastToPlayers(response, userName); - } - else - { - await communicationManager.BroadcastToAll(response); - } + await task; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs deleted file mode 100644 index 24cf9d2..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IActionHandler - { - /// - /// Responsible for parsing json and handling the request. - /// - Task Handle(string json, string userName); - } - - public delegate IActionHandler ActionHandlerResolver(ClientAction action); -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 6f22279..9f3ae26 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -1,13 +1,14 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class JoinByCodeHandler : IActionHandler + public interface IJoinByCodeHandler + { + Task Handle(JoinByCodeRequest request, string userName); + } + public class JoinByCodeHandler : IJoinByCodeHandler { private readonly IGameboardRepository repository; private readonly ISocketCommunicationManager communicationManager; @@ -20,44 +21,44 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(JoinByCodeRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession - { - PlayerName = userName, - JoinCode = request.JoinCode - }); + //var request = JsonConvert.DeserializeObject(json); + //var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession + //{ + // PlayerName = userName, + // JoinCode = request.JoinCode + //}); - if (sessionName == null) - { - var response = new JoinGameResponse(ClientAction.JoinByCode) - { - PlayerName = userName, - GameName = sessionName, - Error = "Error joining game." - }; - await communicationManager.BroadcastToPlayers(response, userName); - } - else - { - // Other members of the game see a regular JoinGame occur. - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName, - GameName = sessionName - }; - // At this time, userName hasn't subscribed and won't receive this message. - await communicationManager.BroadcastToGame(sessionName, response); + //if (sessionName == null) + //{ + // var response = new JoinGameResponse(ClientAction.JoinByCode) + // { + // PlayerName = userName, + // GameName = sessionName, + // Error = "Error joining game." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} + //else + //{ + // // Other members of the game see a regular JoinGame occur. + // var response = new JoinGameResponse(ClientAction.JoinGame) + // { + // PlayerName = userName, + // GameName = sessionName + // }; + // // At this time, userName hasn't subscribed and won't receive this message. + // await communicationManager.BroadcastToGame(sessionName, response); - // The player joining sees the JoinByCode occur. - response = new JoinGameResponse(ClientAction.JoinByCode) - { - PlayerName = userName, - GameName = sessionName - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + // // The player joining sees the JoinByCode occur. + // response = new JoinGameResponse(ClientAction.JoinByCode) + // { + // PlayerName = userName, + // GameName = sessionName + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 150a137..a37cdfc 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -1,13 +1,14 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class JoinGameHandler : IActionHandler + public interface IJoinGameHandler + { + Task Handle(JoinGameRequest request, string userName); + } + public class JoinGameHandler : IJoinGameHandler { private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; @@ -19,30 +20,30 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(JoinGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); + //var request = JsonConvert.DeserializeObject(json); - var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession - { - PlayerName = userName, - SessionName = request.GameName - }); + //var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession + //{ + // PlayerName = userName, + // SessionName = request.GameName + //}); - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName, - GameName = request.GameName - }; - if (joinSucceeded) - { - await communicationManager.BroadcastToAll(response); - } - else - { - response.Error = "Game is full."; - await communicationManager.BroadcastToPlayers(response, userName); - } + //var response = new JoinGameResponse(ClientAction.JoinGame) + //{ + // PlayerName = userName, + // GameName = request.GameName + //}; + //if (joinSucceeded) + //{ + // await communicationManager.BroadcastToAll(response); + //} + //else + //{ + // response.Error = "Game is full."; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index d4379e3..41b5d5e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,16 +1,19 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Linq; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface IListGamesHandler + { + Task Handle(ListGamesRequest request, string userName); + } + // TODO: This doesn't need to be a socket action. // It can be an HTTP route. - public class ListGamesHandler : IActionHandler + public class ListGamesHandler : IListGamesHandler { private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; @@ -23,16 +26,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.repository = repository; } - public async Task Handle(string json, string userName) + public async Task Handle(ListGamesRequest _, string userName) { - var request = JsonConvert.DeserializeObject(json); - var getGamesResponse = string.IsNullOrWhiteSpace(userName) - ? await repository.GetGames() - : await repository.GetGames(userName); - - var games = getGamesResponse.Sessions - .OrderBy(s => s.Player1 == userName || s.Player2 == userName) - .Select(s => new Session(s).ToServiceModel()); // yuck + var sessions = await repository.ReadSessions(); + var games = sessions.Select(s => s.ToServiceModel()); // yuck var response = new ListGamesResponse(ClientAction.ListGames) { diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 9efc43b..9fcb72f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -3,16 +3,20 @@ 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.Linq; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface ILoadGameHandler + { + Task Handle(LoadGameRequest request, string userName); + } + /// /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. /// - public class LoadGameHandler : IActionHandler + public class LoadGameHandler : ILoadGameHandler { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; @@ -31,35 +35,35 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.boardManager = boardManager; } - public async Task Handle(string json, string userName) + public async Task Handle(LoadGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var gameTask = gameboardRepository.GetGame(request.GameName); - var moveTask = gameboardRepository.GetMoves(request.GameName); + var readSession = gameboardRepository.ReadSession(request.GameName); + var readStates = gameboardRepository.ReadBoardStates(request.GameName); - var sessionModel = await gameTask; + var sessionModel = await readSession; if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); - var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; - await communicationManager.BroadcastToPlayers(response, userName); + var error = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; + await communicationManager.BroadcastToPlayers(error, userName); + return; } - else + + communicationManager.SubscribeToGame(sessionModel, userName); + var boardStates = await readStates; + var moveModels = boardStates + .Where(_ => _.Move != null) + .Select(_ => _.Move!.ToRulesModel()) + .ToList(); + var shogiBoard = new ShogiBoard(moveModels); + boardManager.Add(sessionModel.Name, shogiBoard); + + var response = new LoadGameResponse(ClientAction.LoadGame) { - var moveModels = await moveTask; - - communicationManager.SubscribeToGame(sessionModel, userName); - var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - var shogiBoard = new ShogiBoard(boardMoves); - boardManager.Add(sessionModel.Name, shogiBoard); - - var response = new LoadGameResponse(ClientAction.LoadGame) - { - Game = sessionModel.ToServiceModel(), - BoardState = new Models.BoardState(shogiBoard).ToServiceModel() - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + Game = sessionModel.ToServiceModel(), + BoardState = new Models.BoardState(shogiBoard).ToServiceModel() + }; + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 3fa5bb5..74141d0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,14 +1,17 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Newtonsoft.Json; using System.Threading.Tasks; -using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class MoveHandler : IActionHandler + public interface IMoveHandler + { + Task Handle(MoveRequest request, string userName); + } + public class MoveHandler : IMoveHandler { private readonly IBoardManager boardManager; private readonly IGameboardRepository gameboardRepository; @@ -23,43 +26,43 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(MoveRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var moveModel = new Move(request.Move); - var board = boardManager.Get(request.GameName); - if (board == null) - { - // TODO: Find a flow for this - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." - }; - await communicationManager.BroadcastToPlayers(response, userName); + //var request = JsonConvert.DeserializeObject(json); + //var moveModel = new Move(request.Move); + //var board = boardManager.Get(request.GameName); + //if (board == null) + //{ + // // TODO: Find a flow for this + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); - } - var boardMove = moveModel.ToBoardModel(); - var moveSuccess = board.Move(boardMove); - if (moveSuccess) - { - await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); - var boardState = new BoardState(board); - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - GameName = request.GameName, - PlayerName = userName, - BoardState = boardState.ToServiceModel() - }; - await communicationManager.BroadcastToGame(request.GameName, response); - } - else - { - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - Error = "Invalid move." - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + //} + //var boardMove = moveModel.ToBoardModel(); + //var moveSuccess = board.Move(boardMove); + //if (moveSuccess) + //{ + // await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); + // var boardState = new BoardState(board); + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // GameName = request.GameName, + // PlayerName = userName, + // BoardState = boardState.ToServiceModel() + // }; + // await communicationManager.BroadcastToGame(request.GameName, response); + //} + //else + //{ + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // Error = "Invalid move." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 33ec4d2..c3dfc6a 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -3,6 +3,7 @@ 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.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -16,10 +17,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface ISocketCommunicationManager { - Task CommunicateWith(WebSocket w, string s); Task BroadcastToAll(IResponse response); - Task BroadcastToGame(string gameName, IResponse response); - Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + //Task BroadcastToGame(string gameName, IResponse response); + //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); void SubscribeToGame(Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); @@ -34,54 +34,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// Dictionary key is game name. private readonly ConcurrentDictionary sessions; private readonly ILogger logger; - private readonly ActionHandlerResolver handlerResolver; - public SocketCommunicationManager( - ILogger logger, - ActionHandlerResolver handlerResolver) + public SocketCommunicationManager(ILogger logger) { this.logger = logger; - this.handlerResolver = handlerResolver; connections = new ConcurrentDictionary(); sessions = new ConcurrentDictionary(); } - public async Task CommunicateWith(WebSocket socket, string userName) - { - SubscribeToBroadcast(socket, userName); - - while (!socket.CloseStatus.HasValue) - { - try - { - 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)) - { - await socket.SendTextAsync("Error: Action not recognized."); - } - else - { - var handler = handlerResolver(request.Action); - await handler.Handle(message, userName); - } - } - catch (OperationCanceledException ex) - { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); - } - } - UnsubscribeFromBroadcastAndGames(userName); - } - public void SubscribeToBroadcast(WebSocket socket, string playerName) { connections.TryAdd(playerName, socket); @@ -154,27 +114,27 @@ namespace Gameboard.ShogiUI.Sockets.Managers return Task.WhenAll(tasks); } - public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - { - if (sessions.TryGetValue(gameName, out var session)) - { - var serialized1 = JsonConvert.SerializeObject(forPlayer1); - var serialized2 = JsonConvert.SerializeObject(forPlayer2); - return Task.WhenAll( - session.SendToPlayer1(serialized1), - session.SendToPlayer2(serialized2)); - } - return Task.CompletedTask; - } + //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized1 = JsonConvert.SerializeObject(forPlayer1); + // var serialized2 = JsonConvert.SerializeObject(forPlayer2); + // return Task.WhenAll( + // session.SendToPlayer1(serialized1), + // session.SendToPlayer2(serialized2)); + // } + // return Task.CompletedTask; + //} - public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - { - if (sessions.TryGetValue(gameName, out var session)) - { - var serialized = JsonConvert.SerializeObject(messageForAllPlayers); - return session.Broadcast(serialized); - } - return Task.CompletedTask; - } + //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // 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 05374e3..5ae3ff0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,6 +1,14 @@ -using Microsoft.AspNetCore.Http; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; +using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using System; using System.Net; +using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers @@ -12,14 +20,36 @@ namespace Gameboard.ShogiUI.Sockets.Managers public class SocketConnectionManager : ISocketConnectionManager { + private readonly ILogger logger; private readonly ISocketCommunicationManager communicationManager; private readonly ISocketTokenManager tokenManager; + private readonly ICreateGameHandler createGameHandler; + private readonly IJoinByCodeHandler joinByCodeHandler; + private readonly IJoinGameHandler joinGameHandler; + private readonly IListGamesHandler listGamesHandler; + private readonly ILoadGameHandler loadGameHandler; + private readonly IMoveHandler moveHandler; - public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base() + public SocketConnectionManager( + ILogger logger, + ISocketCommunicationManager communicationManager, + ISocketTokenManager tokenManager, + ICreateGameHandler createGameHandler, + IJoinByCodeHandler joinByCodeHandler, + IJoinGameHandler joinGameHandler, + IListGamesHandler listGamesHandler, + ILoadGameHandler loadGameHandler, + IMoveHandler moveHandler) : base() { + this.logger = logger; this.communicationManager = communicationManager; this.tokenManager = tokenManager; - + this.createGameHandler = createGameHandler; + this.joinByCodeHandler = joinByCodeHandler; + this.joinGameHandler = joinGameHandler; + this.listGamesHandler = listGamesHandler; + this.loadGameHandler = loadGameHandler; + this.moveHandler = moveHandler; } public async Task HandleSocketRequest(HttpContext context) @@ -33,7 +63,74 @@ namespace Gameboard.ShogiUI.Sockets.Managers if (userName != null) { var socket = await context.WebSockets.AcceptWebSocketAsync(); - await communicationManager.CommunicateWith(socket, userName); + + communicationManager.SubscribeToBroadcast(socket, userName); + while (!socket.CloseStatus.HasValue) + { + try + { + 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)) + { + await socket.SendTextAsync("Error: Action not recognized."); + continue; + } + switch (request.Action) + { + case ClientAction.ListGames: + { + var req = JsonConvert.DeserializeObject(message); + await listGamesHandler.Handle(req, userName); + break; + } + case ClientAction.CreateGame: + { + var req = JsonConvert.DeserializeObject(message); + await createGameHandler.Handle(req, userName); + break; + } + case ClientAction.JoinGame: + { + var req = JsonConvert.DeserializeObject(message); + await joinGameHandler.Handle(req, userName); + break; + } + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + await joinByCodeHandler.Handle(req, userName); + break; + } + case ClientAction.LoadGame: + { + var req = JsonConvert.DeserializeObject(message); + await loadGameHandler.Handle(req, userName); + break; + } + case ClientAction.Move: + { + var req = JsonConvert.DeserializeObject(message); + await moveHandler.Handle(req, userName); + break; + } + } + } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } + } + communicationManager.UnsubscribeFromBroadcastAndGames(userName); + return; } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs index 971e438..b2e2f73 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -16,27 +17,24 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// /// Key is userName /// - private readonly Dictionary Tokens; + private readonly ConcurrentDictionary Tokens; public SocketTokenManager() { - Tokens = new Dictionary(); + Tokens = new ConcurrentDictionary(); } public Guid GenerateToken(string userName) { - var guid = Guid.NewGuid(); + Tokens.Remove(userName, out _); - if (Tokens.ContainsKey(userName)) - { - Tokens.Remove(userName); - } - Tokens.Add(userName, guid); + var guid = Guid.NewGuid(); + Tokens.TryAdd(userName, guid); _ = Task.Run(async () => { await Task.Delay(TimeSpan.FromMinutes(1)); - Tokens.Remove(userName); + Tokens.Remove(userName, out _); }); return guid; @@ -45,13 +43,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// User name associated to the guid or null. public string GetUsername(Guid guid) { - if (Tokens.ContainsValue(guid)) + var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key; + if (userName != null) { - var username = Tokens.First(kvp => kvp.Value == guid).Key; - Tokens.Remove(username); - return username; + Tokens.Remove(userName, out _); } - return null; + return userName; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs index df3f245..9d8903f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs @@ -6,6 +6,5 @@ namespace Gameboard.ShogiUI.Sockets.Managers.Utility public class Request : IRequest { public ClientAction Action { get; set; } - public string PlayerName { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs index 1d50469..df3d1f0 100644 --- a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs @@ -1,5 +1,6 @@ using Gameboard.ShogiUI.Rules; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -7,28 +8,53 @@ namespace Gameboard.ShogiUI.Sockets.Models { public class BoardState { - public Piece[,] Board { get; set; } - public IReadOnlyCollection Player1Hand { get; set; } - public IReadOnlyCollection Player2Hand { get; set; } + // TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity. + public Piece?[,] Board { get; } + public IReadOnlyCollection Player1Hand { get; } + public IReadOnlyCollection Player2Hand { get; } + /// + /// Move is null in the first BoardState of a Session, before any moves have been made. + /// + public Move? Move { get; } + + public BoardState() : this(new ShogiBoard()) { } + + public BoardState(Piece?[,] board, IList player1Hand, ICollection player2Hand, Move move) + { + Board = board; + Player1Hand = new ReadOnlyCollection(player1Hand); + } public BoardState(ShogiBoard shogi) { Board = new Piece[9, 9]; for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - Board[x, y] = new Piece(shogi.Board[x, y]); + { + var piece = shogi.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList(); Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); + Move = new Move(shogi.MoveHistory[^1]); } public ServiceTypes.BoardState ToServiceModel() { var board = new ServiceTypes.Piece[9, 9]; - Board = new Piece[9, 9]; for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - board[x, y] = Board[x, y].ToServiceModel(); + { + var piece = Board[x, y]; + if (piece != null) + { + board[x, y] = piece.ToServiceModel(); + } + } return new ServiceTypes.BoardState { Board = board, diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index a597493..3617d88 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,88 +1,41 @@ -using Gameboard.ShogiUI.Rules; -using Microsoft.FSharp.Core; -using System; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Numerics; -using BoardStateMove = Gameboard.ShogiUI.Rules.Move; -using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Models { public class Move { - public string PieceFromCaptured { get; set; } - public Coords From { get; set; } - public Coords To { get; set; } + public Coords? From { get; set; } public bool IsPromotion { get; set; } + public WhichPiece? PieceFromHand { get; set; } + public Coords To { get; set; } - public Move(ServiceModels.Socket.Types.Move move) + public Move(Coords from, Coords to, bool isPromotion) { - From = Coords.FromBoardNotation(move.From); - To = Coords.FromBoardNotation(move.To); - PieceFromCaptured = move.PieceFromCaptured; - IsPromotion = move.IsPromotion; + From = from; + To = to; + IsPromotion = isPromotion; } - public Move(ShogiApi.Move move) + + public Move(WhichPiece pieceFromHand, Coords to) { - string pieceFromCaptured = null; - if (move.PieceFromCaptured != null) - { - pieceFromCaptured = move.PieceFromCaptured.Value switch - { - ShogiApi.WhichPieceName.Bishop => "", - 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", - _ => "" - }; - } - From = new Coords(move.Origin.X, move.Origin.Y); - To = new Coords(move.Destination.X, move.Destination.Y); - IsPromotion = move.IsPromotion; - PieceFromCaptured = pieceFromCaptured; + PieceFromHand = pieceFromHand; + To = to; } + public ServiceModels.Socket.Types.Move ToServiceModel() => new() { - From = From.ToBoardNotation(), + From = From?.ToBoardNotation(), IsPromotion = IsPromotion, - PieceFromCaptured = PieceFromCaptured, - To = To.ToBoardNotation() + To = To.ToBoardNotation(), + PieceFromCaptured = PieceFromHand }; - public ShogiApi.Move ToApiModel() + + public Rules.Move ToRulesModel() { - var pieceFromCaptured = PieceFromCaptured switch - { - "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 ShogiApi.Move - { - Origin = new ShogiApi.BoardLocation { X = From.X, Y = From.Y }, - Destination = new ShogiApi.BoardLocation { X = To.X, Y = To.Y }, - IsPromotion = IsPromotion, - PieceFromCaptured = pieceFromCaptured - }; - return target; - } - public BoardStateMove ToBoardModel() - { - return new BoardStateMove - { - From = new Vector2(From.X, From.Y), - IsPromotion = IsPromotion, - PieceFromCaptured = Enum.TryParse(PieceFromCaptured, out var whichPiece) ? whichPiece : null, - To = new Vector2(To.X, To.Y) - }; + return PieceFromHand != null + ? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y)) + : new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion); } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index 8ce5124..d8bc5b9 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -1,18 +1,25 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using BoardStatePiece = Gameboard.ShogiUI.Rules.Pieces.Piece; namespace Gameboard.ShogiUI.Sockets.Models { public class Piece { - public WhichPiece WhichPiece { get; set; } + public bool IsPromoted { get; } + public WhichPlayer Owner { get; } + public WhichPiece WhichPiece { get; } - public bool IsPromoted { get; set; } - - public Piece(BoardStatePiece piece) + public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece) + { + IsPromoted = isPromoted; + Owner = owner; + WhichPiece = whichPiece; + } + + public Piece(Rules.Pieces.Piece piece) { - WhichPiece = (WhichPiece)piece.WhichPiece; IsPromoted = piece.IsPromoted; + Owner = (WhichPlayer)piece.Owner; + WhichPiece = (WhichPiece)piece.WhichPiece; } public ServiceModels.Socket.Types.Piece ToServiceModel() @@ -20,6 +27,7 @@ namespace Gameboard.ShogiUI.Sockets.Models return new ServiceModels.Socket.Types.Piece { IsPromoted = IsPromoted, + Owner = Owner, WhichPiece = WhichPiece }; } diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index 1e824f7..bf2af49 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -1,5 +1,6 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Newtonsoft.Json; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.WebSockets; @@ -9,18 +10,20 @@ namespace Gameboard.ShogiUI.Sockets.Models { public class Session { + [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } public string Name { get; } public string Player1 { get; } - public string Player2 { get; } + public string? Player2 { get; } + public bool IsPrivate { get; } - public ConcurrentDictionary Subscriptions { get; } - - public Session(Shogi.Api.ServiceModels.Types.Session session) + public Session(string name, bool isPrivate, string player1, string? player2 = null) { - Name = session.Name; - Player1 = session.Player1; - Player2 = session.Player2; Subscriptions = new ConcurrentDictionary(); + + Name = name; + Player1 = player1; + Player2 = player2; + IsPrivate = isPrivate; } public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); @@ -47,7 +50,7 @@ namespace Gameboard.ShogiUI.Sockets.Models public Task SendToPlayer2(string message) { - if (Subscriptions.TryGetValue(Player2, out var socket)) + if (Player2 != null && Subscriptions.TryGetValue(Player2, out var socket)) { return socket.SendTextAsync(message); } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs new file mode 100644 index 0000000..2658248 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class BoardState : CouchDocument + { + public string Name { get; set; } + public Piece?[,] Board { get; set; } + public Piece[] Player1Hand { get; set; } + public Piece[] Player2Hand { get; set; } + /// + /// Move is null for first BoardState of a session - before anybody has made moves. + /// + public Move? Move { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public BoardState() : base() + { + Name = string.Empty; + Board = new Piece[9, 9]; + Player1Hand = Array.Empty(); + Player2Hand = Array.Empty(); + } + + public BoardState(string sessionName, Models.BoardState boardState) : base($"{sessionName}-{DateTime.Now:O}", nameof(BoardState)) + { + Name = sessionName; + Board = new Piece[9, 9]; + + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = boardState.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } + + Player1Hand = boardState.Player1Hand.Select(model => new Piece(model)).ToArray(); + Player2Hand = boardState.Player2Hand.Select(model => new Piece(model)).ToArray(); + if (boardState.Move != null) + { + Move = new Move(boardState.Move); + } + } + + public Models.BoardState ToDomainModel() + { + /* + * Board = new Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = boardState.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } + + Player1Hand = boardState.Player1Hand.Select(_ => new Piece(_)).ToList(); + Player2Hand = boardState.Player2Hand.Select(_ => new Piece(_)).ToList(); + if (boardState.Move != null) + { + Move = new Move(boardState.Move); + } + */ + return null; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs new file mode 100644 index 0000000..3435e7b --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs @@ -0,0 +1,15 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class CouchCreateResult + { + public string Id { get; set; } + public bool Ok { get; set; } + public string Rev { get; set; } + + public CouchCreateResult() + { + Id = string.Empty; + Rev = string.Empty; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs new file mode 100644 index 0000000..bd1a793 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public abstract class CouchDocument + { + [JsonProperty("_id")] + public string Id { get; set; } + public string Type { get; set; } + public DateTimeOffset CreatedDate { get; set; } + + public CouchDocument() + { + Id = string.Empty; + Type = string.Empty; + CreatedDate = DateTimeOffset.UtcNow; + } + public CouchDocument(string id, string type) + { + Id = id; + Type = type; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs new file mode 100644 index 0000000..fd02af6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs @@ -0,0 +1,14 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + internal class CouchFindResult + { + public T[] docs; + + public CouchFindResult() + { + docs = Array.Empty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs new file mode 100644 index 0000000..a362f7d --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -0,0 +1,45 @@ +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Move + { + /// + /// A board coordinate, like A3 or G6. When null, look for PieceFromHand to exist. + /// + public string? From { get; set; } + + public bool IsPromotion { get; set; } + + /// + /// The piece placed from the player's hand. + /// + public WhichPiece? PieceFromHand { get; set; } + + /// + /// A board coordinate, like A3 or G6. + /// + public string To { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Move() + { + To = string.Empty; + } + + public Move(Models.Move move) + { + From = move.From?.ToBoardNotation(); + IsPromotion = move.IsPromotion; + To = move.To.ToBoardNotation(); + PieceFromHand = move.PieceFromHand; + } + + public Models.Move ToDomainModel() => PieceFromHand.HasValue + ? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To)) + : new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs new file mode 100644 index 0000000..68642f9 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -0,0 +1,27 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Piece + { + public bool IsPromoted { get; set; } + public WhichPlayer Owner { get; set; } + public WhichPiece WhichPiece { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Piece() + { + } + + public Piece(Models.Piece piece) + { + IsPromoted = piece.IsPromoted; + Owner = piece.Owner; + WhichPiece = piece.WhichPiece; + } + + public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md new file mode 100644 index 0000000..50c4380 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md @@ -0,0 +1,4 @@ +### Couch Models + +Couch models should accept domain models during construction and offer a ToDomainModel method which constructs a domain model. +In this way, domain models have the freedom to define their valid states. diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs new file mode 100644 index 0000000..a3dd34e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs @@ -0,0 +1,30 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Session : CouchDocument + { + public string Name { get; set; } + public string Player1 { get; set; } + public string? Player2 { get; set; } + public bool IsPrivate { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Session() : base() + { + Name = string.Empty; + Player1 = string.Empty; + Player2 = string.Empty; + } + + public Session(string id, Models.Session session) : base(id, nameof(Session)) + { + Name = session.Name; + Player1 = session.Player1; + Player2 = session.Player2; + IsPrivate = session.IsPrivate; + } + + public Models.Session ToDomainModel() => new(Name, IsPrivate, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs new file mode 100644 index 0000000..22d30c7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs @@ -0,0 +1,23 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class User : CouchDocument + { + public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; + + public enum LoginPlatform + { + Microsoft, + Guest + } + + public string Name { get; set; } + public LoginPlatform Platform { get; set; } + public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User)) + { + Name = name; + Platform = platform; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index bd483eb..162e2df 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -1,6 +1,5 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; +using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -13,141 +12,173 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task DeleteGame(string gameName); - Task GetGame(string gameName); - Task GetGames(); - Task GetGames(string playerName); - Task> GetMoves(string gameName); - Task PostSession(PostSession request); - Task PostJoinPrivateSession(PostJoinPrivateSession request); - Task PutJoinPublicSession(PutJoinPublicSession request); - Task PostMove(string gameName, PostMove request); + Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move); + Task CreateGuestUser(string userName); + Task CreateSession(Models.Session session); + Task> ReadSessions(); + Task IsGuestUser(string userName); Task PostJoinCode(string gameName, string userName); - Task GetPlayer(string userName); - Task PostPlayer(PostPlayer request); + Task ReadSession(string name); + Task> ReadBoardStates(string name); } 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) + private const string ApplicationJson = "application/json"; + private readonly HttpClient client; + private readonly ILogger logger; + + public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) { - this.client = client; + client = clientFactory.CreateClient("couchdb"); + this.logger = logger; } - public async Task GetGames() + public async Task> ReadSessions() { - var response = await client.GetAsync(GetSessionsRoute); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); - } + var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}"; + var query = $@"{{ ""selector"": {selector} }}"; + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync("_find", content); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); - public async Task GetGames(string playerName) - { - var uri = $"Sessions/{playerName}"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); - } - - public async Task GetGame(string gameName) - { - var uri = $"Session/{gameName}"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - if (string.IsNullOrWhiteSpace(json)) + if (result == null) { - return null; + logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); + return Array.Empty(); } - return new Session(JsonConvert.DeserializeObject(json).Session); + return result.docs + .Select(_ => _.ToDomainModel()) + .ToList(); } - public async Task DeleteGame(string gameName) + public async Task ReadSession(string name) { - var uri = $"Session/{gameName}"; - await client.DeleteAsync(Uri.EscapeUriString(uri)); + var response = await client.GetAsync(name); + var responseContent = await response.Content.ReadAsStringAsync(); + var couchModel = JsonConvert.DeserializeObject(responseContent); + return couchModel.ToDomainModel(); } - public async Task PostSession(PostSession request) + public async Task> ReadBoardStates(string name) { - 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).SessionName; - } + var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}"; + var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}"; + var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}"; + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync("_find", content); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); - public async Task PutJoinPublicSession(PutJoinPublicSession request) - { - 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).JoinSucceeded; - } - - public async Task PostJoinPrivateSession(PostJoinPrivateSession request) - { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PostAsync(JoinSessionRoute, content); - var json = await response.Content.ReadAsStringAsync(); - var deserialized = JsonConvert.DeserializeObject(json); - if (deserialized.JoinSucceeded) + if (result == null) { - return deserialized.SessionName; + logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); + return Array.Empty(); } - return null; + return result.docs + .Select(_ => new Models.BoardState(_)) + .ToList(); } - public async Task> GetMoves(string gameName) + //public async Task DeleteGame(string gameName) + //{ + // //var uri = $"Session/{gameName}"; + // //await client.DeleteAsync(Uri.EscapeUriString(uri)); + //} + + public async Task CreateSession(Models.Session session) { - var uri = $"Session/{gameName}/Moves"; - var get = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await get.Content.ReadAsStringAsync(); - if (string.IsNullOrWhiteSpace(json)) - { - return new List(); - } - var response = JsonConvert.DeserializeObject(json); - return response.Moves.Select(m => new Move(m)).ToList(); + var couchModel = new Session(session.Name, session); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } - public async Task PostMove(string gameName, PostMove request) + public async Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move) { - var uri = $"Session/{gameName}/Move"; - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - await client.PostAsync(Uri.EscapeUriString(uri), content); + var couchModel = new BoardState(sessionName, boardState, move); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } + //public async Task PutJoinPublicSession(PutJoinPublicSession request) + //{ + // 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).JoinSucceeded; + //} + + //public async Task PostJoinPrivateSession(PostJoinPrivateSession request) + //{ + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // var response = await client.PostAsync(JoinSessionRoute, content); + // var json = await response.Content.ReadAsStringAsync(); + // var deserialized = JsonConvert.DeserializeObject(json); + // if (deserialized.JoinSucceeded) + // { + // return deserialized.SessionName; + // } + // return null; + //} + + //public async Task> GetMoves(string gameName) + //{ + // var uri = $"Session/{gameName}/Moves"; + // var get = await client.GetAsync(Uri.EscapeUriString(uri)); + // var json = await get.Content.ReadAsStringAsync(); + // if (string.IsNullOrWhiteSpace(json)) + // { + // return new List(); + // } + // var response = JsonConvert.DeserializeObject(json); + // return response.Moves.Select(m => new Move(m)).ToList(); + //} + + //public async Task PostMove(string gameName, PostMove request) + //{ + // var uri = $"Session/{gameName}/Move"; + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // await client.PostAsync(Uri.EscapeUriString(uri), content); + //} + public async Task PostJoinCode(string gameName, string userName) { - var uri = $"JoinCode/{gameName}"; - var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); - var content = new StringContent(serialized, Encoding.UTF8, MediaType); - var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json).JoinCode; + // var uri = $"JoinCode/{gameName}"; + // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); + // var content = new StringContent(serialized, Encoding.UTF8, MediaType); + // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); + // return JsonConvert.DeserializeObject(json).JoinCode; + return string.Empty; } - public async Task GetPlayer(string playerName) + //public async Task GetPlayer(string playerName) + //{ + // var uri = $"Player/{playerName}"; + // var get = await client.GetAsync(Uri.EscapeUriString(uri)); + // var content = await get.Content.ReadAsStringAsync(); + // if (!string.IsNullOrWhiteSpace(content)) + // { + // var response = JsonConvert.DeserializeObject(content); + // return new Player(response.Player.Name); + // } + // return null; + //} + + public async Task CreateGuestUser(string userName) { - var uri = $"Player/{playerName}"; - var get = await client.GetAsync(Uri.EscapeUriString(uri)); - var content = await get.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) - { - var response = JsonConvert.DeserializeObject(content); - return new Player(response.Player.Name); - } - return null; + var couchModel = new User(userName, User.LoginPlatform.Guest); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } - public async Task PostPlayer(PostPlayer request) + public async Task IsGuestUser(string userName) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PostAsync(PlayerRoute, content); + var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}")); + var response = await client.SendAsync(req); return response.IsSuccessStatusCode; } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs index 30fed61..d219516 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs @@ -1,4 +1,4 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; +using Gameboard.ShogiUI.Sockets.Models; using System; using System.Threading.Tasks; @@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers Task CreateGuestUser(); Task IsPlayer1(string sessionName, string playerName); bool IsGuest(string playerName); - Task PlayerExists(string playerName); + Task CreateSession(Session session); } public class GameboardRepositoryManager : IGameboardRepositoryManager @@ -30,11 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers { count++; var clientId = $"Guest-{Guid.NewGuid()}"; - var request = new PostPlayer - { - PlayerName = clientId - }; - var isCreated = await repository.PostPlayer(request); + var isCreated = await repository.CreateGuestUser(clientId); if (isCreated) { return clientId; @@ -45,22 +41,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers public async Task IsPlayer1(string sessionName, string playerName) { - var session = await repository.GetGame(sessionName); - return session?.Player1 == playerName; + //var session = await repository.GetGame(sessionName); + //return session?.Player1 == playerName; + return true; } public async Task CreateJoinCode(string sessionName, string playerName) { - var session = await repository.GetGame(sessionName); - if (playerName == session?.Player1) - { - return await repository.PostJoinCode(sessionName, playerName); - } + //var session = await repository.GetGame(sessionName); + //if (playerName == session?.Player1) + //{ + // return await repository.PostJoinCode(sessionName, playerName); + //} return null; } - public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); + public async Task CreateSession(Session session) + { + var success = await repository.CreateSession(session); + if (success) + { + return await repository.CreateBoardState(session.Name, new BoardState(), null); + } + return false; + } - public async Task PlayerExists(string playerName) => await repository.GetPlayer(playerName) != null; + public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs deleted file mode 100644 index 3b4067d..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs +++ /dev/null @@ -1,122 +0,0 @@ -using IdentityModel.Client; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Repositories.Utility -{ - public interface IAuthenticatedHttpClient - { - 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 - { - private readonly ILogger logger; - private readonly string identityServerUrl; - private TokenResponse tokenResponse; - private readonly string clientId; - private readonly string clientSecret; - - public AuthenticatedHttpClient(ILogger logger, IConfiguration configuration) : base() - { - this.logger = logger; - identityServerUrl = configuration["AppSettings:IdentityServer"]; - clientId = configuration["AppSettings:ClientId"]; - clientSecret = configuration["AppSettings:ClientSecret"]; - BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]); - } - - private async Task RefreshBearerToken() - { - var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl); - if (disco.IsError) - { - logger.LogError("{DiscoveryErrorType}", disco.ErrorType); - throw new Exception(disco.Error); - } - - var request = new ClientCredentialsTokenRequest - { - Address = disco.TokenEndpoint, - ClientId = clientId, - ClientSecret = clientSecret - }; - var response = await this.RequestClientCredentialsTokenAsync(request); - if (response.IsError) - { - throw new Exception(response.Error); - } - tokenResponse = response; - logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress); - this.SetBearerToken(tokenResponse.AccessToken); - } - - public new async Task GetAsync(string requestUri) - { - var response = await base.GetAsync(requestUri); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.GetAsync(requestUri); - } - logger.LogInformation( - "Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n", - BaseAddress, - requestUri, - await response.Content.ReadAsStringAsync()); - return response; - } - public new async Task PostAsync(string requestUri, HttpContent content) - { - var response = await base.PostAsync(requestUri, content); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.PostAsync(requestUri, content); - } - logger.LogInformation( - "Repository POST 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 new async 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 new async Task DeleteAsync(string requestUri) - { - var response = await base.DeleteAsync(requestUri); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.DeleteAsync(requestUri); - } - logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri); - return response; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 8431c68..cda318d 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -3,8 +3,6 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -15,8 +13,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System; -using System.Collections.Generic; using System.Linq; +using System.Text; namespace Gameboard.ShogiUI.Sockets { @@ -33,36 +31,33 @@ namespace Gameboard.ShogiUI.Sockets public void ConfigureServices(IServiceCollection services) { // Socket ActionHandlers - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Managers services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddScoped(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => action => - { - return action switch - { - ClientAction.ListGames => sp.GetService(), - ClientAction.CreateGame => sp.GetService(), - ClientAction.JoinGame => sp.GetService(), - ClientAction.JoinByCode => sp.GetService(), - ClientAction.LoadGame => sp.GetService(), - ClientAction.Move => sp.GetService(), - _ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"), - }; - }); // Repositories + services.AddHttpClient("couchdb", c => + { + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); + + var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; + c.BaseAddress = new Uri(baseUrl); + }); services.AddTransient(); - services.AddSingleton(); + //services.AddSingleton(); + //services.AddSingleton(provider => new CouchClient(databaseName, couchUrl)); services.AddControllers(); diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 1b50ee6..9d9f40f 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -1,10 +1,9 @@ { "AppSettings": { - "IdentityServer": "https://identity.lucaserver.space/", - "GameboardShogiApi": "https://dev.lucaserver.space/Gameboard.Shogi.Api/", - "ClientId": "DevClientId", - "ClientSecret": "DevSecret", - "Scope": "DevEnvironment" + "CouchDB": { + "Database": "shogi-dev", + "Url": "http://192.168.1.15:5984" + } }, "Logging": { "LogLevel": { diff --git a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs index 7145e5e..8cf4734 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs @@ -13,7 +13,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var name = self.WhichPiece switch { WhichPiece.King => " K ", - WhichPiece.GoldenGeneral => " G ", + WhichPiece.GoldGeneral => " G ", WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ", WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ", WhichPiece.Rook => self.IsPromoted ? "^R " : " R ", diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index fbe26b3..c558571 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -26,9 +26,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[4, 0].WhichPiece.Should().Be(WhichPiece.King); - board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); @@ -51,9 +51,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[4, 8].WhichPiece.Should().Be(WhichPiece.King); - board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); @@ -256,23 +256,23 @@ namespace Gameboard.ShogiUI.UnitTests.Rules // Act | Assert - It is P1 turn /// try illegally placing Knight from the hand. shogi.Board[7, 0].Should().BeNull(); - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 0) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 0].Should().BeNull(); - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 1) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 1) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 1].Should().BeNull(); /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); shogi.Board[7, 0].Should().BeNull(); /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 0].Should().BeNull(); @@ -312,7 +312,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act - P1 tries to place a Lance while in check. - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(4, 4) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) }); // Assert dropSuccess.Should().BeFalse(); @@ -347,7 +347,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules shogi.Board[4, 0].Should().NotBeNull(); // Act - P1 tries to place Bishop from hand to an already-occupied position - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) }); // Assert dropSuccess.Should().BeFalse(); diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index d276b12..48b20e7 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -4,7 +4,7 @@ namespace PathFinding { public interface IPlanarCollection : IEnumerable where T : IPlanarElement { - T this[float x, float y] { get; set; } + T? this[float x, float y] { get; set; } int GetLength(int dimension); } } diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index ee29921..4466c3d 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable From 178cb00253cc1a6a061bdfcb9ad2196d572a44a9 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 26 Jul 2021 06:28:56 -0500 Subject: [PATCH 15/27] Before changing Piece[,] to Dictionary --- Benchmarking/Benchmarking.csproj | 4 - Benchmarking/Benchmarks.cs | 12 - .../Gameboard.ShogiUI.Rules.csproj | 14 - Gameboard.ShogiUI.Rules/Move.cs | 27 -- Gameboard.ShogiUI.Rules/Pieces/Bishop.cs | 39 --- Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs | 30 -- Gameboard.ShogiUI.Rules/Pieces/King.cs | 32 -- Gameboard.ShogiUI.Rules/Pieces/Knight.cs | 27 -- Gameboard.ShogiUI.Rules/Pieces/Lance.cs | 26 -- Gameboard.ShogiUI.Rules/Pieces/Pawn.cs | 26 -- Gameboard.ShogiUI.Rules/Pieces/Piece.cs | 47 --- Gameboard.ShogiUI.Rules/Pieces/Rook.cs | 39 --- .../Pieces/SilverGeneral.cs | 29 -- Gameboard.ShogiUI.Rules/WhichPiece.cs | 14 - Gameboard.ShogiUI.Rules/WhichPlayer.cs | 8 - .../Socket/Messages/JoinGame.cs | 7 +- .../Socket/Messages/ListGames.cs | 5 +- .../Socket/Messages/LoadGame.cs | 5 +- .../Socket/Types/BoardState.cs | 2 + .../Socket/Types/Game.cs | 27 +- Gameboard.ShogiUI.Sockets.sln | 11 +- .../Controllers/GameController.cs | 9 +- .../Controllers/SocketController.cs | 5 +- .../Extensions/ModelExtensions.cs | 13 +- .../Gameboard.ShogiUI.Sockets.csproj | 11 +- .../Managers/ActiveSessionManager.cs | 33 ++ .../Managers/BoardManager.cs | 32 -- .../ClientActionHandlers/CreateGameHandler.cs | 19 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 4 +- .../ClientActionHandlers/JoinGameHandler.cs | 52 ++- .../ClientActionHandlers/ListGamesHandler.cs | 12 +- .../ClientActionHandlers/LoadGameHandler.cs | 28 +- .../ClientActionHandlers/MoveHandler.cs | 51 +-- .../GameboardManager.cs} | 55 +++- .../Managers/SocketCommunicationManager.cs | 140 -------- .../Managers/SocketConnectionManager.cs | 221 ++++++------- .../Models/BoardState.cs | 66 ---- Gameboard.ShogiUI.Sockets/Models/Coords.cs | 41 --- Gameboard.ShogiUI.Sockets/Models/Move.cs | 73 ++++- Gameboard.ShogiUI.Sockets/Models/MoveSets.cs | 95 ++++++ Gameboard.ShogiUI.Sockets/Models/Piece.cs | 52 ++- Gameboard.ShogiUI.Sockets/Models/Player.cs | 12 - Gameboard.ShogiUI.Sockets/Models/Session.cs | 55 +--- .../Models/SessionMetadata.cs | 30 ++ .../Models/Shogi.cs | 101 +++--- .../Repositories/CouchModels/BoardState.cs | 75 ----- .../CouchModels/BoardStateDocument.cs | 57 ++++ .../Repositories/CouchModels/CouchDocument.cs | 22 +- .../Repositories/CouchModels/Move.cs | 23 +- .../Repositories/CouchModels/Piece.cs | 2 +- .../Repositories/CouchModels/Readme.md | 4 - .../Repositories/CouchModels/Session.cs | 30 -- .../CouchModels/SessionDocument.cs | 48 +++ .../CouchModels/{User.cs => UserDocument.cs} | 8 +- .../CouchModels/WhichDocumentType.cs | 9 + .../Repositories/GameboardRepository.cs | 124 ++++--- .../CreateGameRequestValidator.cs | 15 + .../JoinByCodeRequestValidator.cs | 15 + .../JoinGameRequestValidator.cs | 15 + .../ListGamesRequestValidator.cs | 14 + .../LoadGameRequestValidator.cs | 15 + .../RequestValidators/MoveRequestValidator.cs | 23 ++ .../Services/SocketService.cs | 194 +++++++++++ Gameboard.ShogiUI.Sockets/Startup.cs | 30 +- .../Gameboard.ShogiUI.UnitTests.csproj | 2 + .../PathFinding/PlanarCollectionShould.cs | 92 ++++++ .../Rules/ShogiBoardShould.cs | 309 +++++++++--------- .../Sockets/CoordsModelShould.cs | 27 -- Gameboard.ShogiUI.xUnitTests/GameShould.cs | 17 + .../Gameboard.ShogiUI.xUnitTests.csproj | 28 ++ .../MoveRequestValidatorShould.cs | 76 +++++ PathFinding/PathFinder2D.cs | 18 +- .../PlanarCollection.cs | 22 +- 73 files changed, 1537 insertions(+), 1418 deletions(-) delete mode 100644 Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj delete mode 100644 Gameboard.ShogiUI.Rules/Move.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Bishop.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/King.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Knight.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Lance.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Pawn.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Piece.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Rook.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs delete mode 100644 Gameboard.ShogiUI.Rules/WhichPiece.cs delete mode 100644 Gameboard.ShogiUI.Rules/WhichPlayer.cs rename Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs => Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs (83%) create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs rename Gameboard.ShogiUI.Sockets/{Repositories/RepositoryManagers/GameboardRepositoryManager.cs => Managers/GameboardManager.cs} (50%) delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/BoardState.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/Coords.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/MoveSets.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/Player.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs rename Gameboard.ShogiUI.Rules/ShogiBoard.cs => Gameboard.ShogiUI.Sockets/Models/Shogi.cs (81%) delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs rename Gameboard.ShogiUI.Sockets/Repositories/CouchModels/{User.cs => UserDocument.cs} (58%) create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/SocketService.cs create mode 100644 Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs delete mode 100644 Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/GameShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj create mode 100644 Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs rename {Gameboard.ShogiUI.Rules => PathFinding}/PlanarCollection.cs (72%) diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index d84d541..4e77937 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -10,8 +10,4 @@ - - - - diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 31f6599..5c7c052 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; -using Gameboard.ShogiUI.Rules; using System; using System.Linq; using System.Numerics; @@ -10,7 +9,6 @@ namespace Benchmarking { public class Benchmarks { - private readonly Move[] moves; private readonly Vector2[] directions; private readonly Consumer consumer = new(); @@ -48,21 +46,11 @@ namespace Benchmarking //[Benchmark] public void One() { - var board = new ShogiBoard(); - foreach (var move in moves) - { - board.Move(move); - } } //[Benchmark] public void Two() { - var board = new ShogiBoard(); - foreach (var move in moves) - { - //board.TryMove2(move); - } } diff --git a/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj deleted file mode 100644 index 5befd78..0000000 --- a/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net5.0 - true - 5 - enable - - - - - - - diff --git a/Gameboard.ShogiUI.Rules/Move.cs b/Gameboard.ShogiUI.Rules/Move.cs deleted file mode 100644 index 81286fe..0000000 --- a/Gameboard.ShogiUI.Rules/Move.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics; -using System.Numerics; - -namespace Gameboard.ShogiUI.Rules -{ - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public WhichPiece? PieceFromHand { get; } - public Vector2? From { get; } - public Vector2 To { get; } - public bool IsPromotion { get; } - - public Move(Vector2 from, Vector2 to, bool isPromotion) - { - From = from; - To = to; - IsPromotion = isPromotion; - } - - public Move(WhichPiece pieceFromHand, Vector2 to) - { - PieceFromHand = pieceFromHand; - To = to; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs b/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs deleted file mode 100644 index 3c433cc..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Bishop : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - private static readonly List PromotedMoves = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, PromotedMoves); - } - - public override Piece DeepClone() - { - var clone = new Bishop(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs deleted file mode 100644 index fa984ab..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs +++ /dev/null @@ -1,30 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class GoldenGeneral : Piece - { - public static readonly List Moves = new(6) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down) - }; - public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, Moves); - } - - public override Piece DeepClone() - { - var clone = new GoldenGeneral(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/King.cs b/Gameboard.ShogiUI.Rules/Pieces/King.cs deleted file mode 100644 index ab4d9c4..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/King.cs +++ /dev/null @@ -1,32 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class King : Piece - { - private static readonly List Moves = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public King(WhichPlayer owner) : base(WhichPiece.King, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, Moves); - } - - public override Piece DeepClone() - { - var clone = new King(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Knight.cs b/Gameboard.ShogiUI.Rules/Pieces/Knight.cs deleted file mode 100644 index 7091ceb..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Knight.cs +++ /dev/null @@ -1,27 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Knight : Piece - { - private static readonly List Moves = new(2) - { - new PathFinding.Move(Direction.KnightLeft), - new PathFinding.Move(Direction.KnightRight) - }; - - public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Knight(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Lance.cs b/Gameboard.ShogiUI.Rules/Pieces/Lance.cs deleted file mode 100644 index 48be6a2..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Lance.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Lance : Piece - { - private static readonly List Moves = new(1) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - }; - - public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Lance(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs b/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs deleted file mode 100644 index 1005c6f..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Pawn : Piece - { - private static readonly List Moves = new(1) - { - new PathFinding.Move(Direction.Up) - }; - - public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Pawn(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Piece.cs b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs deleted file mode 100644 index b64584b..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Piece.cs +++ /dev/null @@ -1,47 +0,0 @@ -using PathFinding; -using System.Diagnostics; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public abstract class Piece : IPlanarElement - { - protected MoveSet promotedMoveSet; - protected MoveSet moveSet; - - public MoveSet MoveSet => IsPromoted ? promotedMoveSet : moveSet; - public abstract Piece DeepClone(); - public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } - public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPlayer.Player2; - - public Piece(WhichPiece piece, WhichPlayer owner) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = false; - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void ToggleOwnership() - { - Owner = Owner == WhichPlayer.Player1 - ? WhichPlayer.Player2 - : WhichPlayer.Player1; - } - - public void Promote() => IsPromoted = CanPromote; - - public void Demote() => IsPromoted = false; - - public void Capture() - { - ToggleOwnership(); - Demote(); - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Rook.cs b/Gameboard.ShogiUI.Rules/Pieces/Rook.cs deleted file mode 100644 index 3a990a4..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Rook.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Rook : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep) - }; - private static readonly List PromotedMoves = new(8) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, PromotedMoves); - } - - public override Piece DeepClone() - { - var clone = new Rook(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs deleted file mode 100644 index 3287604..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs +++ /dev/null @@ -1,29 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class SilverGeneral : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new SilverGeneral(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/WhichPiece.cs b/Gameboard.ShogiUI.Rules/WhichPiece.cs deleted file mode 100644 index 5e6356c..0000000 --- a/Gameboard.ShogiUI.Rules/WhichPiece.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Gameboard.ShogiUI.Rules -{ - public enum WhichPiece - { - King, - GoldGeneral, - SilverGeneral, - Bishop, - Rook, - Knight, - Lance, - Pawn - } -} diff --git a/Gameboard.ShogiUI.Rules/WhichPlayer.cs b/Gameboard.ShogiUI.Rules/WhichPlayer.cs deleted file mode 100644 index 4b8b8f2..0000000 --- a/Gameboard.ShogiUI.Rules/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Rules -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs index b662cb1..80a15bf 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs @@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class JoinByCodeRequest : IRequest { public ClientAction Action { get; set; } - public string JoinCode { get; set; } + public string JoinCode { get; set; } = ""; } public class JoinGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = ""; } public class JoinGameResponse : IResponse @@ -25,6 +25,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public JoinGameResponse(ClientAction action) { Action = action.ToString(); + Error = ""; + GameName = ""; + PlayerName = ""; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index ab9d67f..bbd9944 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -1,6 +1,7 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { @@ -13,11 +14,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; } public string Error { get; set; } - public ICollection Games { get; set; } + public IReadOnlyList Games { get; set; } public ListGamesResponse(ClientAction action) { Action = action.ToString(); + Error = ""; + Games = new Collection(); } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index a879953..d268e07 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -1,19 +1,22 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public class LoadGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = ""; } public class LoadGameResponse : IResponse { public string Action { get; } public Game Game { get; set; } + public WhichPlayer PlayerPerspective { get; set; } public BoardState BoardState { get; set; } + public IList MoveHistory { get; set; } public string Error { get; set; } public LoadGameResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs index 6ee3051..3ba7e8b 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -8,5 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types public Piece[,] Board { get; set; } = new Piece[0, 0]; public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); + public WhichPlayer? PlayerInCheck { get; set; } + public WhichPlayer WhoseTurn { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index 8ff5fb1..f9fb1bb 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,14 +1,33 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class Game { + public string Player1 { get; set; } = string.Empty; + public string? Player2 { get; set; } = string.Empty; public string GameName { get; set; } = string.Empty; /// - /// Players[0] is the session owner, Players[1] is the other guy + /// Players[0] is the session owner, Players[1] is the other person. /// - public IReadOnlyList Players { get; set; } = Array.Empty(); + public IReadOnlyList Players + { + get + { + var list = new List(2) { Player1 }; + if (!string.IsNullOrEmpty(Player2)) list.Add(Player2); + return list; + } + } + + public Game() + { + } + public Game(string gameName, string player1, string? player2 = null) + { + GameName = gameName; + Player1 = player1; + Player2 = player2; + } } } diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index a1576c2..afad198 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -49,16 +49,17 @@ Global {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 7900ed2..15bf163 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,14 +13,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [Route("[controller]")] public class GameController : ControllerBase { - private readonly IGameboardRepositoryManager manager; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager manager; + private readonly ISocketConnectionManager communicationManager; private readonly IGameboardRepository repository; public GameController( IGameboardRepository repository, - IGameboardRepositoryManager manager, - ISocketCommunicationManager communicationManager) + IGameboardManager manager, + ISocketConnectionManager communicationManager) { this.manager = manager; this.communicationManager = communicationManager; diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index f00b5bf..739e423 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,13 +16,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers { private readonly ILogger logger; private readonly ISocketTokenManager tokenManager; - private readonly IGameboardRepositoryManager gameboardManager; + private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; public SocketController( ILogger logger, ISocketTokenManager tokenManager, - IGameboardRepositoryManager gameboardManager, + IGameboardManager gameboardManager, IGameboardRepository gameboardRepository) { this.logger = logger; diff --git a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs similarity index 83% rename from Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs rename to Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index 8cf4734..36827e8 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,14 +1,13 @@ -using Gameboard.ShogiUI.Rules; -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System; using System.Text; using System.Text.RegularExpressions; -namespace Gameboard.ShogiUI.UnitTests.Rules +namespace Gameboard.ShogiUI.Sockets.Extensions { - public static class BoardStateExtensions + public static class ModelExtensions { - public static string GetShortName(this Piece self) + public static string GetShortName(this Models.Piece self) { var name = self.WhichPiece switch { @@ -27,7 +26,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules return name; } - public static void PrintStateAsAscii(this ShogiBoard self) + public static void PrintStateAsAscii(this Models.Shogi self) { var builder = new StringBuilder(); builder.Append(" Player 2(.)"); @@ -41,7 +40,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules builder.Append('|'); for (var x = 0; x < 9; x++) { - var piece = self.Board[x, y]; + var piece = self.Board[y, x]; if (piece == null) { builder.Append(" "); diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index c558037..241f8e8 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,14 +8,7 @@ - - - - - - - - + @@ -26,8 +19,8 @@ - + diff --git a/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs new file mode 100644 index 0000000..5018096 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs @@ -0,0 +1,33 @@ +using Gameboard.ShogiUI.Sockets.Models; +using System.Collections.Concurrent; + +namespace Gameboard.ShogiUI.Sockets.Managers +{ + public interface IActiveSessionManager + { + void Add(Session session); + Session? Get(string sessionName); + } + + // TODO: Consider moving this class' functionality into the ConnectionManager class. + public class ActiveSessionManager : IActiveSessionManager + { + private readonly ConcurrentDictionary Sessions; + + public ActiveSessionManager() + { + Sessions = new ConcurrentDictionary(); + } + + public void Add(Session session) => Sessions.TryAdd(session.Name, session); + + public Session? Get(string sessionName) + { + if (Sessions.TryGetValue(sessionName, out var session)) + { + return session; + } + return null; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs deleted file mode 100644 index f153527..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Gameboard.ShogiUI.Rules; -using System.Collections.Concurrent; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface IBoardManager - { - void Add(string sessionName, ShogiBoard board); - ShogiBoard? Get(string sessionName); - } - - public class BoardManager : IBoardManager - { - private readonly ConcurrentDictionary Boards; - - public BoardManager() - { - Boards = new ConcurrentDictionary(); - } - - public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); - - public ShogiBoard? Get(string sessionName) - { - if (Boards.TryGetValue(sessionName, out var board)) - { - return board; - } - return null; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index 8ff9dd1..6703d3b 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using System.Threading.Tasks; @@ -14,20 +13,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers // It can be an API route and still tell socket connections about the new session. public class CreateGameHandler : ICreateGameHandler { - private readonly IGameboardRepositoryManager manager; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager manager; + private readonly ISocketConnectionManager connectionManager; public CreateGameHandler( - ISocketCommunicationManager communicationManager, - IGameboardRepositoryManager manager) + ISocketConnectionManager communicationManager, + IGameboardManager manager) { this.manager = manager; - this.communicationManager = communicationManager; + this.connectionManager = communicationManager; } public async Task Handle(CreateGameRequest request, string userName) { - var model = new Session(request.GameName, request.IsPrivate, userName); + var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null); var success = await manager.CreateSession(model); if (!success) @@ -36,7 +35,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { Error = "Unable to create game with this name." }; - await communicationManager.BroadcastToPlayers(error, userName); + await connectionManager.BroadcastToPlayers(error, userName); } var response = new CreateGameResponse(request.Action) @@ -46,8 +45,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers }; var task = request.IsPrivate - ? communicationManager.BroadcastToPlayers(response, userName) - : communicationManager.BroadcastToAll(response); + ? connectionManager.BroadcastToPlayers(response, userName) + : connectionManager.BroadcastToAll(response); await task; } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 9f3ae26..8882e8f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -11,10 +11,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public class JoinByCodeHandler : IJoinByCodeHandler { private readonly IGameboardRepository repository; - private readonly ISocketCommunicationManager communicationManager; + private readonly ISocketConnectionManager communicationManager; public JoinByCodeHandler( - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository repository) { this.repository = repository; diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index a37cdfc..223782c 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -1,5 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -10,40 +10,34 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } public class JoinGameHandler : IJoinGameHandler { - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager gameboardManager; + private readonly ISocketConnectionManager connectionManager; public JoinGameHandler( - ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + ISocketConnectionManager communicationManager, + IGameboardManager gameboardManager) { - this.gameboardRepository = gameboardRepository; - this.communicationManager = communicationManager; + this.gameboardManager = gameboardManager; + this.connectionManager = communicationManager; } public async Task Handle(JoinGameRequest request, string userName) { - //var request = JsonConvert.DeserializeObject(json); + var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName); - //var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession - //{ - // PlayerName = userName, - // SessionName = request.GameName - //}); - - //var response = new JoinGameResponse(ClientAction.JoinGame) - //{ - // PlayerName = userName, - // GameName = request.GameName - //}; - //if (joinSucceeded) - //{ - // await communicationManager.BroadcastToAll(response); - //} - //else - //{ - // response.Error = "Game is full."; - // await communicationManager.BroadcastToPlayers(response, userName); - //} + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName, + GameName = request.GameName + }; + if (joinSucceeded) + { + await connectionManager.BroadcastToAll(response); + } + else + { + response.Error = "Game is full or does not exist."; + await connectionManager.BroadcastToPlayers(response, userName); + } } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 41b5d5e..64787bd 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -11,15 +11,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers Task Handle(ListGamesRequest request, string userName); } - // TODO: This doesn't need to be a socket action. - // It can be an HTTP route. public class ListGamesHandler : IListGamesHandler { - private readonly ISocketCommunicationManager communicationManager; + private readonly ISocketConnectionManager communicationManager; private readonly IGameboardRepository repository; public ListGamesHandler( - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository repository) { this.communicationManager = communicationManager; @@ -28,12 +26,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(ListGamesRequest _, string userName) { - var sessions = await repository.ReadSessions(); - var games = sessions.Select(s => s.ToServiceModel()); // yuck + var sessions = await repository.ReadSessionMetadatas(); + var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList(); var response = new ListGamesResponse(ClientAction.ListGames) { - Games = games.ToList() + Games = games }; await communicationManager.BroadcastToPlayers(response, userName); diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 9fcb72f..93317a6 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Rules; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -20,14 +20,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; - private readonly IBoardManager boardManager; + private readonly ISocketConnectionManager communicationManager; + private readonly IActiveSessionManager boardManager; public LoadGameHandler( ILogger logger, - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, - IBoardManager boardManager) + IActiveSessionManager boardManager) { this.logger = logger; this.gameboardRepository = gameboardRepository; @@ -37,10 +37,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(LoadGameRequest request, string userName) { - var readSession = gameboardRepository.ReadSession(request.GameName); - var readStates = gameboardRepository.ReadBoardStates(request.GameName); - - var sessionModel = await readSession; + var sessionModel = await gameboardRepository.ReadSession(request.GameName); if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); @@ -50,18 +47,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } communicationManager.SubscribeToGame(sessionModel, userName); - var boardStates = await readStates; - var moveModels = boardStates - .Where(_ => _.Move != null) - .Select(_ => _.Move!.ToRulesModel()) - .ToList(); - var shogiBoard = new ShogiBoard(moveModels); - boardManager.Add(sessionModel.Name, shogiBoard); + boardManager.Add(sessionModel); var response = new LoadGameResponse(ClientAction.LoadGame) { - Game = sessionModel.ToServiceModel(), - BoardState = new Models.BoardState(shogiBoard).ToServiceModel() + Game = new SessionMetadata(sessionModel).ToServiceModel(), + BoardState = sessionModel.Shogi.ToServiceModel(), + MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList() }; await communicationManager.BroadcastToPlayers(response, userName); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 74141d0..4c9e9a8 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,7 +1,6 @@ using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Newtonsoft.Json; using System.Threading.Tasks; @@ -13,35 +12,45 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } public class MoveHandler : IMoveHandler { - private readonly IBoardManager boardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; + private readonly IActiveSessionManager boardManager; + private readonly IGameboardManager gameboardManager; + private readonly ISocketConnectionManager communicationManager; public MoveHandler( - IBoardManager boardManager, - ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + IActiveSessionManager boardManager, + ISocketConnectionManager communicationManager, + IGameboardManager gameboardManager) { this.boardManager = boardManager; - this.gameboardRepository = gameboardRepository; + this.gameboardManager = gameboardManager; this.communicationManager = communicationManager; } public async Task Handle(MoveRequest request, string userName) { - //var request = JsonConvert.DeserializeObject(json); - //var moveModel = new Move(request.Move); - //var board = boardManager.Get(request.GameName); - //if (board == null) - //{ - // // TODO: Find a flow for this - // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - // { - // Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." - // }; - // await communicationManager.BroadcastToPlayers(response, userName); + Move moveModel; + if (request.Move.PieceFromCaptured.HasValue) + { + moveModel = new Move(request.Move.PieceFromCaptured.Value, request.Move.To); + } + else + { + moveModel = new Move(request.Move.From!, request.Move.To, request.Move.IsPromotion); + } + + var board = boardManager.Get(request.GameName); + if (board == null) + { + // TODO: Find a flow for this + var response = new MoveResponse(ServiceModels.Socket.Types.ClientAction.Move) + { + Error = $"Game isn't loaded. Send a message with the {ServiceModels.Socket.Types.ClientAction.LoadGame} action first." + }; + await communicationManager.BroadcastToPlayers(response, userName); + } + + + - //} - //var boardMove = moveModel.ToBoardModel(); //var moveSuccess = board.Move(boardMove); //if (moveSuccess) //{ diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs similarity index 50% rename from Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs rename to Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index d219516..bc2be4b 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -1,24 +1,28 @@ using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories; using System; using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers +namespace Gameboard.ShogiUI.Sockets.Managers { - public interface IGameboardRepositoryManager + public interface IGameboardManager { Task CreateGuestUser(); Task IsPlayer1(string sessionName, string playerName); bool IsGuest(string playerName); - Task CreateSession(Session session); + Task CreateSession(SessionMetadata session); + Task ReadSession(string gameName); + Task UpdateSession(Session session); + Task AssignPlayer2ToSession(string sessionName, string userName); } - public class GameboardRepositoryManager : IGameboardRepositoryManager + public class GameboardManager : IGameboardManager { private const int MaxTries = 3; private const string GuestPrefix = "Guest-"; private readonly IGameboardRepository repository; - public GameboardRepositoryManager(IGameboardRepository repository) + public GameboardManager(IGameboardRepository repository) { this.repository = repository; } @@ -53,19 +57,44 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers //{ // return await repository.PostJoinCode(sessionName, playerName); //} - return null; + return string.Empty; } - public async Task CreateSession(Session session) + public Task CreateSession(SessionMetadata session) { - var success = await repository.CreateSession(session); - if (success) - { - return await repository.CreateBoardState(session.Name, new BoardState(), null); - } - return false; + return repository.CreateSession(session); } public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); + + public Task ReadSession(string sessionName) + { + return repository.ReadSession(sessionName); + } + + /// + /// Saves the session to storage. + /// + /// The session to save. + /// True if the session was saved successfully. + public Task UpdateSession(Session session) + { + return repository.UpdateSession(session); + } + + public async Task AssignPlayer2ToSession(string sessionName, string userName) + { + var isSuccess = false; + var session = await repository.ReadSession(sessionName); + if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2)) + { + session.SetPlayer2(userName); + if (await repository.UpdateSession(session)) + { + isSuccess = true; + } + } + return isSuccess; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs deleted file mode 100644 index c3dfc6a..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -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.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface ISocketCommunicationManager - { - Task BroadcastToAll(IResponse response); - //Task BroadcastToGame(string gameName, IResponse response); - //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); - void SubscribeToGame(Session session, string playerName); - void SubscribeToBroadcast(WebSocket socket, string playerName); - void UnsubscribeFromBroadcastAndGames(string playerName); - void UnsubscribeFromGame(string gameName, string playerName); - Task BroadcastToPlayers(IResponse response, params string[] playerNames); - } - - public class SocketCommunicationManager : ISocketCommunicationManager - { - /// Dictionary key is player name. - private readonly ConcurrentDictionary connections; - /// Dictionary key is game name. - private readonly ConcurrentDictionary sessions; - private readonly ILogger logger; - - public SocketCommunicationManager(ILogger logger) - { - this.logger = logger; - connections = new ConcurrentDictionary(); - sessions = new ConcurrentDictionary(); - } - - public void SubscribeToBroadcast(WebSocket socket, string playerName) - { - connections.TryAdd(playerName, socket); - } - - public void UnsubscribeFromBroadcastAndGames(string playerName) - { - connections.TryRemove(playerName, out _); - foreach (var kvp in sessions) - { - var sessionName = kvp.Key; - UnsubscribeFromGame(sessionName, playerName); - } - } - - /// - /// Unsubscribes the player from their current game, then subscribes to the new game. - /// - public void SubscribeToGame(Session session, string playerName) - { - // Unsubscribe from any other games - foreach (var kvp in sessions) - { - var gameNameKey = kvp.Key; - UnsubscribeFromGame(gameNameKey, playerName); - } - - // Subscribe - if (connections.TryGetValue(playerName, out var socket)) - { - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); - } - } - - public void UnsubscribeFromGame(string gameName, string playerName) - { - if (sessions.TryGetValue(gameName, out var s)) - { - s.Subscriptions.TryRemove(playerName, out _); - if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); - } - } - - public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) - { - var tasks = new List(playerNames.Length); - foreach (var name in playerNames) - { - if (connections.TryGetValue(name, out var socket)) - { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", name, serialized); - tasks.Add(socket.SendTextAsync(serialized)); - - } - } - await Task.WhenAll(tasks); - } - public Task BroadcastToAll(IResponse response) - { - var message = JsonConvert.SerializeObject(response); - logger.LogInformation($"Broadcasting\n{0}", message); - var tasks = new List(connections.Count); - foreach (var kvp in connections) - { - var socket = kvp.Value; - tasks.Add(socket.SendTextAsync(message)); - } - return Task.WhenAll(tasks); - } - - //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized1 = JsonConvert.SerializeObject(forPlayer1); - // var serialized2 = JsonConvert.SerializeObject(forPlayer2); - // return Task.WhenAll( - // session.SendToPlayer1(serialized1), - // session.SendToPlayer2(serialized2)); - // } - // return Task.CompletedTask; - //} - - //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // 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 5ae3ff0..54798b0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,13 +1,10 @@ using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.AspNetCore.Http; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Net; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; @@ -15,127 +12,127 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface ISocketConnectionManager { - Task HandleSocketRequest(HttpContext context); + Task BroadcastToAll(IResponse response); + //Task BroadcastToGame(string gameName, IResponse response); + //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + void SubscribeToGame(Session session, string playerName); + void SubscribeToBroadcast(WebSocket socket, string playerName); + void UnsubscribeFromBroadcastAndGames(string playerName); + void UnsubscribeFromGame(string gameName, string playerName); + Task BroadcastToPlayers(IResponse response, params string[] playerNames); } + /// + /// Retains all active socket connections and provides convenient methods for sending messages to clients. + /// public class SocketConnectionManager : ISocketConnectionManager { + /// Dictionary key is player name. + private readonly ConcurrentDictionary connections; + /// Dictionary key is game name. + private readonly ConcurrentDictionary sessions; private readonly ILogger logger; - private readonly ISocketCommunicationManager communicationManager; - private readonly ISocketTokenManager tokenManager; - private readonly ICreateGameHandler createGameHandler; - private readonly IJoinByCodeHandler joinByCodeHandler; - private readonly IJoinGameHandler joinGameHandler; - private readonly IListGamesHandler listGamesHandler; - private readonly ILoadGameHandler loadGameHandler; - private readonly IMoveHandler moveHandler; - public SocketConnectionManager( - ILogger logger, - ISocketCommunicationManager communicationManager, - ISocketTokenManager tokenManager, - ICreateGameHandler createGameHandler, - IJoinByCodeHandler joinByCodeHandler, - IJoinGameHandler joinGameHandler, - IListGamesHandler listGamesHandler, - ILoadGameHandler loadGameHandler, - IMoveHandler moveHandler) : base() + public SocketConnectionManager(ILogger logger) { this.logger = logger; - this.communicationManager = communicationManager; - this.tokenManager = tokenManager; - this.createGameHandler = createGameHandler; - this.joinByCodeHandler = joinByCodeHandler; - this.joinGameHandler = joinGameHandler; - this.listGamesHandler = listGamesHandler; - this.loadGameHandler = loadGameHandler; - this.moveHandler = moveHandler; + connections = new ConcurrentDictionary(); + sessions = new ConcurrentDictionary(); } - public async Task HandleSocketRequest(HttpContext context) + public void SubscribeToBroadcast(WebSocket socket, string playerName) { - var hasToken = context.Request.Query.Keys.Contains("token"); - if (hasToken) + connections.TryAdd(playerName, socket); + } + + public void UnsubscribeFromBroadcastAndGames(string playerName) + { + connections.TryRemove(playerName, out _); + foreach (var kvp in sessions) { - var oneTimeToken = context.Request.Query["token"][0]; - var tokenAsGuid = Guid.Parse(oneTimeToken); - var userName = tokenManager.GetUsername(tokenAsGuid); - if (userName != null) + var sessionName = kvp.Key; + UnsubscribeFromGame(sessionName, playerName); + } + } + + /// + /// Unsubscribes the player from their current game, then subscribes to the new game. + /// + public void SubscribeToGame(Session session, string playerName) + { + // Unsubscribe from any other games + foreach (var kvp in sessions) + { + var gameNameKey = kvp.Key; + UnsubscribeFromGame(gameNameKey, playerName); + } + + // Subscribe + if (connections.TryGetValue(playerName, out var socket)) + { + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); + } + } + + public void UnsubscribeFromGame(string gameName, string playerName) + { + if (sessions.TryGetValue(gameName, out var s)) + { + s.Subscriptions.TryRemove(playerName, out _); + if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); + } + } + + public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (connections.TryGetValue(name, out var socket)) { - var socket = await context.WebSockets.AcceptWebSocketAsync(); + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); - communicationManager.SubscribeToBroadcast(socket, userName); - while (!socket.CloseStatus.HasValue) - { - try - { - 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)) - { - await socket.SendTextAsync("Error: Action not recognized."); - continue; - } - switch (request.Action) - { - case ClientAction.ListGames: - { - var req = JsonConvert.DeserializeObject(message); - await listGamesHandler.Handle(req, userName); - break; - } - case ClientAction.CreateGame: - { - var req = JsonConvert.DeserializeObject(message); - await createGameHandler.Handle(req, userName); - break; - } - case ClientAction.JoinGame: - { - var req = JsonConvert.DeserializeObject(message); - await joinGameHandler.Handle(req, userName); - break; - } - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(message); - await joinByCodeHandler.Handle(req, userName); - break; - } - case ClientAction.LoadGame: - { - var req = JsonConvert.DeserializeObject(message); - await loadGameHandler.Handle(req, userName); - break; - } - case ClientAction.Move: - { - var req = JsonConvert.DeserializeObject(message); - await moveHandler.Handle(req, userName); - break; - } - } - } - catch (OperationCanceledException ex) - { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); - } - } - communicationManager.UnsubscribeFromBroadcastAndGames(userName); - - return; } } - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - return; + await Task.WhenAll(tasks); } + public Task BroadcastToAll(IResponse response) + { + var message = JsonConvert.SerializeObject(response); + logger.LogInformation($"Broadcasting\n{0}", message); + var tasks = new List(connections.Count); + foreach (var kvp in connections) + { + var socket = kvp.Value; + tasks.Add(socket.SendTextAsync(message)); + } + return Task.WhenAll(tasks); + } + + //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized1 = JsonConvert.SerializeObject(forPlayer1); + // var serialized2 = JsonConvert.SerializeObject(forPlayer2); + // return Task.WhenAll( + // session.SendToPlayer1(serialized1), + // session.SendToPlayer2(serialized2)); + // } + // return Task.CompletedTask; + //} + + //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); + // return session.Broadcast(serialized); + // } + // return Task.CompletedTask; + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs deleted file mode 100644 index df3d1f0..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Gameboard.ShogiUI.Rules; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class BoardState - { - // TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity. - public Piece?[,] Board { get; } - public IReadOnlyCollection Player1Hand { get; } - public IReadOnlyCollection Player2Hand { get; } - /// - /// Move is null in the first BoardState of a Session, before any moves have been made. - /// - public Move? Move { get; } - - public BoardState() : this(new ShogiBoard()) { } - - public BoardState(Piece?[,] board, IList player1Hand, ICollection player2Hand, Move move) - { - Board = board; - Player1Hand = new ReadOnlyCollection(player1Hand); - } - - public BoardState(ShogiBoard shogi) - { - Board = new Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = shogi.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList(); - Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); - Move = new Move(shogi.MoveHistory[^1]); - } - - public ServiceTypes.BoardState ToServiceModel() - { - var board = new ServiceTypes.Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = Board[x, y]; - if (piece != null) - { - board[x, y] = piece.ToServiceModel(); - } - } - return new ServiceTypes.BoardState - { - Board = board, - Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), - Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() - }; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Coords.cs b/Gameboard.ShogiUI.Sockets/Models/Coords.cs deleted file mode 100644 index 2104cde..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Coords.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class Coords - { - private const string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private const char A = 'A'; - public int X { get; } - public int Y { get; } - public Coords(int x, int y) - { - X = x; - Y = y; - } - - public string ToBoardNotation() - { - var file = (char)(X + A); - var rank = Y + 1; - return $"{file}{rank}"; - } - - public static Coords FromBoardNotation(string notation) - { - if (string.IsNullOrEmpty(notation)) - { - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Coords(file - A, rank); - } - throw new ArgumentException("Board notation not recognized."); // TODO: Move this error handling to the service layer. - } - return new Coords(-1, -1); // Temporarily this is how I tell Gameboard.API that a piece came from the hand. - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 3617d88..568594c 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,41 +1,86 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System; +using System.Diagnostics; using System.Numerics; +using System.Text.RegularExpressions; namespace Gameboard.ShogiUI.Sockets.Models { + [DebuggerDisplay("{From} - {To}")] public class Move { - public Coords? From { get; set; } - public bool IsPromotion { get; set; } - public WhichPiece? PieceFromHand { get; set; } - public Coords To { get; set; } + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private static readonly char A = 'A'; - public Move(Coords from, Coords to, bool isPromotion) + public Vector2? From { get; } + public bool IsPromotion { get; } + public WhichPiece? PieceFromHand { get; } + public Vector2 To { get; } + + public Move(Vector2 from, Vector2 to, bool isPromotion = false) { From = from; To = to; IsPromotion = isPromotion; } - - public Move(WhichPiece pieceFromHand, Coords to) + public Move(WhichPiece pieceFromHand, Vector2 to) { PieceFromHand = pieceFromHand; To = to; } + /// + /// Constructor to represent moving a piece on the Board to another position on the Board. + /// + /// Position the piece is being moved from. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(string fromNotation, string toNotation, bool isPromotion = false) + { + From = FromBoardNotation(fromNotation); + To = FromBoardNotation(toNotation); + IsPromotion = isPromotion; + } + + /// + /// Constructor to represent moving a piece from the Hand to the Board. + /// + /// The piece being moved from the Hand to the Board. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false) + { + From = null; + PieceFromHand = pieceFromHand; + To = FromBoardNotation(toNotation); + IsPromotion = isPromotion; + } + public ServiceModels.Socket.Types.Move ToServiceModel() => new() { - From = From?.ToBoardNotation(), + From = From.HasValue ? ToBoardNotation(From.Value) : null, IsPromotion = IsPromotion, - To = To.ToBoardNotation(), - PieceFromCaptured = PieceFromHand + PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null, + To = ToBoardNotation(To) }; - public Rules.Move ToRulesModel() + private static string ToBoardNotation(Vector2 vector) { - return PieceFromHand != null - ? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y)) - : new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion); + var file = (char)(vector.X + A); + var rank = vector.Y + 1; + return $"{file}{rank}"; + } + + private static Vector2 FromBoardNotation(string notation) + { + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs new file mode 100644 index 0000000..5f4ee8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs @@ -0,0 +1,95 @@ +using PathFinding; +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public static class MoveSets + { + public static readonly List King = new(8) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + + public static readonly List Bishop = new(4) + { + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) + }; + + public static readonly List PromotedBishop = new(8) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) + }; + + public static readonly List GoldGeneral = new(6) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down) + }; + + public static readonly List Knight = new(2) + { + new PathFinding.Move(Direction.KnightLeft), + new PathFinding.Move(Direction.KnightRight) + }; + + public static readonly List Lance = new(1) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + }; + + public static readonly List Pawn = new(1) + { + new PathFinding.Move(Direction.Up) + }; + + public static readonly List Rook = new(4) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep) + }; + + public static readonly List PromotedRook = new(8) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + + public static readonly List SilverGeneral = new(4) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index d8bc5b9..f86d120 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -1,27 +1,59 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using PathFinding; +using System.Diagnostics; namespace Gameboard.ShogiUI.Sockets.Models { - public class Piece + [DebuggerDisplay("{WhichPiece} {Owner}")] + public class Piece : IPlanarElement { - public bool IsPromoted { get; } - public WhichPlayer Owner { get; } public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; - public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece) + public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) { - IsPromoted = isPromoted; + WhichPiece = piece; Owner = owner; - WhichPiece = whichPiece; + IsPromoted = isPromoted; } - public Piece(Rules.Pieces.Piece piece) + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void ToggleOwnership() { - IsPromoted = piece.IsPromoted; - Owner = (WhichPlayer)piece.Owner; - WhichPiece = (WhichPiece)piece.WhichPiece; + Owner = Owner == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; } + public void Promote() => IsPromoted = CanPromote; + + public void Demote() => IsPromoted = false; + + public void Capture() + { + ToggleOwnership(); + Demote(); + } + + // TODO: There is no reason to make "new" MoveSets every time this property is accessed. + public MoveSet MoveSet => WhichPiece switch + { + WhichPiece.King => new MoveSet(this, MoveSets.King), + WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral), + WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral), + WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop), + WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook), + WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight), + WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance), + WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn), + _ => throw new System.NotImplementedException() + }; + public ServiceModels.Socket.Types.Piece ToServiceModel() { return new ServiceModels.Socket.Types.Piece diff --git a/Gameboard.ShogiUI.Sockets/Models/Player.cs b/Gameboard.ShogiUI.Sockets/Models/Player.cs deleted file mode 100644 index 90fa28e..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Player.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class Player - { - public string Name { get; } - - public Player(string name) - { - Name = name; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index bf2af49..32bd545 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -1,22 +1,21 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Net.WebSockets; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Models { public class Session { + // TODO: Separate subscriptions to the Session from the Session. [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } public string Name { get; } public string Player1 { get; } - public string? Player2 { get; } + public string? Player2 { get; private set; } public bool IsPrivate { get; } - public Session(string name, bool isPrivate, string player1, string? player2 = null) + public Shogi Shogi { get; } + + public Session(string name, bool isPrivate, Shogi shogi, string player1, string? player2 = null) { Subscriptions = new ConcurrentDictionary(); @@ -24,48 +23,12 @@ namespace Gameboard.ShogiUI.Sockets.Models Player1 = player1; Player2 = player2; IsPrivate = isPrivate; + Shogi = shogi; } - public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); - - public Task Broadcast(string message) + public void SetPlayer2(string userName) { - 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 (Player2 != null && 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() - }; + Player2 = userName; } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs new file mode 100644 index 0000000..80f74fe --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -0,0 +1,30 @@ +namespace Gameboard.ShogiUI.Sockets.Models +{ + /// + /// A representation of a Session without the board and game-rules. + /// + public class SessionMetadata + { + public string Name { get; } + public string Player1 { get; } + public string? Player2 { get; } + public bool IsPrivate { get; } + + public SessionMetadata(string name, bool isPrivate, string player1, string? player2) + { + Name = name; + IsPrivate = isPrivate; + Player1 = player1; + Player2 = player2; + } + public SessionMetadata(Session sessionModel) + { + Name = sessionModel.Name; + IsPrivate = sessionModel.IsPrivate; + Player1 = sessionModel.Player1; + Player2 = sessionModel.Player2; + } + + public ServiceModels.Socket.Types.Game ToServiceModel() => new(Name, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Rules/ShogiBoard.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs similarity index 81% rename from Gameboard.ShogiUI.Rules/ShogiBoard.cs rename to Gameboard.ShogiUI.Sockets/Models/Shogi.cs index e5f104e..678aa92 100644 --- a/Gameboard.ShogiUI.Rules/ShogiBoard.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs @@ -1,21 +1,22 @@ -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using PathFinding; using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; -namespace Gameboard.ShogiUI.Rules +namespace Gameboard.ShogiUI.Sockets.Models { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. /// The board is always from Player1's perspective. /// [0,0] is the lower-left position, [8,8] is the higher-right position /// - public class ShogiBoard + public class Shogi { private delegate void MoveSetCallback(Piece piece, Vector2 position); private readonly PathFinder2D pathFinder; - private ShogiBoard? validationBoard; + private Shogi? validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } @@ -27,7 +28,7 @@ namespace Gameboard.ShogiUI.Rules public string Error { get; private set; } - public ShogiBoard() + public Shogi() { Board = new PlanarCollection(9, 9); MoveHistory = new List(20); @@ -36,13 +37,14 @@ namespace Gameboard.ShogiUI.Rules { WhichPlayer.Player2, new List()}, }; pathFinder = new PathFinder2D(Board); - InitializeBoardState(); player1King = new Vector2(4, 8); player2King = new Vector2(4, 0); Error = string.Empty; + + InitializeBoardState(); } - public ShogiBoard(IList moves) : this() + public Shogi(IList moves) : this() { for (var i = 0; i < moves.Count; i++) { @@ -54,16 +56,16 @@ namespace Gameboard.ShogiUI.Rules } } - private ShogiBoard(ShogiBoard toCopy) + private Shogi(Shogi toCopy) { Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) { - var piece = toCopy.Board[x, y]; + var piece = toCopy.Board[y, x]; if (piece != null) { - Board[x, y] = piece.DeepClone(); + Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted); } } @@ -105,7 +107,7 @@ namespace Gameboard.ShogiUI.Rules // Try making the move in a "throw away" board. if (validationBoard == null) { - validationBoard = new ShogiBoard(this); + validationBoard = new Shogi(this); } var isValid = move.PieceFromHand.HasValue @@ -138,7 +140,7 @@ namespace Gameboard.ShogiUI.Rules if (move.PieceFromHand.HasValue == false) return false; //Invalid move var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) return false; // Invalid move - if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. + if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; switch (move.PieceFromHand.Value) @@ -157,7 +159,7 @@ namespace Gameboard.ShogiUI.Rules if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; // Mutate the board. - Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; + Board[move.To.Y, move.To.X] = Hands[WhoseTurn][index]; Hands[WhoseTurn].RemoveAt(index); return true; @@ -165,7 +167,7 @@ namespace Gameboard.ShogiUI.Rules /// True if the move was successful. private bool PlaceFromBoard(Move move) { - var fromPiece = Board[move.From.Value.X, move.From.Value.Y]; + var fromPiece = Board[move.From.Value.Y, move.From.Value.X]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; @@ -182,7 +184,7 @@ namespace Gameboard.ShogiUI.Rules return false; // Invalid move; move not part of move-set. } - var captured = Board[move.To.X, move.To.Y]; + var captured = Board[move.To.Y, move.To.X]; if (captured != null) { if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. @@ -202,8 +204,8 @@ namespace Gameboard.ShogiUI.Rules fromPiece.Promote(); } } - Board[move.To.X, move.To.Y] = fromPiece; - Board[move.From.Value.X, move.From.Value.Y] = null; + Board[move.To.Y, move.To.X] = fromPiece; + Board[move.From.Value.Y, move.From.Value.X] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) @@ -223,7 +225,7 @@ namespace Gameboard.ShogiUI.Rules private bool IsPathable(Vector2 from, Vector2 to) { - var piece = Board[from.X, from.Y]; + var piece = Board[from.Y, from.X]; if (piece == null) return false; var isObstructed = false; @@ -308,8 +310,8 @@ namespace Gameboard.ShogiUI.Rules // ...evaluate if any move gets the player out of check. pathFinder.PathEvery(from, (other, position) => { - if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveToTry = new Move(from, position, false); + if (validationBoard == null) validationBoard = new Shogi(this); + var moveToTry = new Move(from, position); var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { @@ -331,44 +333,44 @@ namespace Gameboard.ShogiUI.Rules { for (int y = 3; y < 6; y++) for (int x = 0; x < 9; x++) - Board[x, y] = null; + Board[y, x] = null; } private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 6 : 2; - for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); + for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player); } private void ResetMiddleRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 7 : 1; - Board[0, y] = null; - for (int x = 2; x < 7; x++) Board[x, y] = null; - Board[8, y] = null; + Board[y, 0] = null; + for (int x = 2; x < 7; x++) Board[y, x] = null; + Board[y, 8] = null; if (player == WhichPlayer.Player1) { - Board[1, y] = new Bishop(player); - Board[7, y] = new Rook(player); + Board[y, 1] = new Piece(WhichPiece.Bishop, player); + Board[y, 7] = new Piece(WhichPiece.Rook, player); } else { - Board[1, y] = new Rook(player); - Board[7, y] = new Bishop(player); + Board[y, 1] = new Piece(WhichPiece.Rook, player); + Board[y, 7] = new Piece(WhichPiece.Bishop, player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 8 : 0; - Board[0, y] = new Lance(player); - Board[1, y] = new Knight(player); - Board[2, y] = new SilverGeneral(player); - Board[3, y] = new GoldenGeneral(player); - Board[4, y] = new King(player); - Board[5, y] = new GoldenGeneral(player); - Board[6, y] = new SilverGeneral(player); - Board[7, y] = new Knight(player); - Board[8, y] = new Lance(player); + Board[y, 0] = new Piece(WhichPiece.Lance, player); + Board[y, 1] = new Piece(WhichPiece.Knight, player); + Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player); + Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player); + Board[y, 4] = new Piece(WhichPiece.King, player); + Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player); + Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player); + Board[y, 7] = new Piece(WhichPiece.Knight, player); + Board[y, 8] = new Piece(WhichPiece.Lance, player); } private void InitializeBoardState() { @@ -381,5 +383,28 @@ namespace Gameboard.ShogiUI.Rules ResetRearRow(WhichPlayer.Player1); } #endregion + + public BoardState ToServiceModel() + { + var board = new ServiceModels.Socket.Types.Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = Board[y, x]; + if (piece != null) + { + board[y, x] = piece.ToServiceModel(); + } + } + + return new BoardState + { + Board = board, + PlayerInCheck = InCheck, + WhoseTurn = WhoseTurn, + Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(), + Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList() + }; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs deleted file mode 100644 index 2658248..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Linq; - -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels -{ - public class BoardState : CouchDocument - { - public string Name { get; set; } - public Piece?[,] Board { get; set; } - public Piece[] Player1Hand { get; set; } - public Piece[] Player2Hand { get; set; } - /// - /// Move is null for first BoardState of a session - before anybody has made moves. - /// - public Move? Move { get; set; } - - /// - /// Default constructor and setters are for deserialization. - /// - public BoardState() : base() - { - Name = string.Empty; - Board = new Piece[9, 9]; - Player1Hand = Array.Empty(); - Player2Hand = Array.Empty(); - } - - public BoardState(string sessionName, Models.BoardState boardState) : base($"{sessionName}-{DateTime.Now:O}", nameof(BoardState)) - { - Name = sessionName; - Board = new Piece[9, 9]; - - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = boardState.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = boardState.Player1Hand.Select(model => new Piece(model)).ToArray(); - Player2Hand = boardState.Player2Hand.Select(model => new Piece(model)).ToArray(); - if (boardState.Move != null) - { - Move = new Move(boardState.Move); - } - } - - public Models.BoardState ToDomainModel() - { - /* - * Board = new Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = boardState.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = boardState.Player1Hand.Select(_ => new Piece(_)).ToList(); - Player2Hand = boardState.Player2Hand.Select(_ => new Piece(_)).ToList(); - if (boardState.Move != null) - { - Move = new Move(boardState.Move); - } - */ - return null; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs new file mode 100644 index 0000000..3c8d9e7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -0,0 +1,57 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System; +using System.Linq; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class BoardStateDocument : CouchDocument + { + public string Name { get; set; } + + public Piece?[,] Board { get; set; } + + public Piece[] Player1Hand { get; set; } + + public Piece[] Player2Hand { get; set; } + + /// + /// Move is null for first BoardState of a session - before anybody has made moves. + /// + public Move? Move { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public BoardStateDocument() : base(WhichDocumentType.BoardState) + { + Name = string.Empty; + Board = new Piece[9, 9]; + Player1Hand = Array.Empty(); + Player2Hand = Array.Empty(); + } + + public BoardStateDocument(string sessionName, Models.Shogi shogi) + : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) + { + Name = sessionName; + Board = new Piece[9, 9]; + + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = shogi.Board[y, x]; + if (piece != null) + { + Board[y, x] = new Piece(piece); + } + } + + Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(model => new Piece(model)).ToArray(); + Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(model => new Piece(model)).ToArray(); + if (shogi.MoveHistory.Count > 0) + { + Move = new Move(shogi.MoveHistory[^1]); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs index bd1a793..4549de0 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -5,21 +5,21 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public abstract class CouchDocument { - [JsonProperty("_id")] - public string Id { get; set; } - public string Type { get; set; } + [JsonProperty("_id")] public string Id { get; set; } + public WhichDocumentType DocumentType { get; } public DateTimeOffset CreatedDate { get; set; } - public CouchDocument() - { - Id = string.Empty; - Type = string.Empty; - CreatedDate = DateTimeOffset.UtcNow; - } - public CouchDocument(string id, string type) + public CouchDocument(WhichDocumentType documentType) + : this(string.Empty, documentType, DateTimeOffset.UtcNow) { } + + public CouchDocument(string id, WhichDocumentType documentType) + : this(id, documentType, DateTimeOffset.UtcNow) { } + + public CouchDocument(string id, WhichDocumentType documentType, DateTimeOffset createdDate) { Id = id; - Type = type; + DocumentType = documentType; + CreatedDate = createdDate; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs index a362f7d..5dbbf03 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -1,5 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { @@ -32,14 +32,25 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public Move(Models.Move move) { - From = move.From?.ToBoardNotation(); + if (move.From.HasValue) + { + From = ToBoardNotation(move.From.Value); + } IsPromotion = move.IsPromotion; - To = move.To.ToBoardNotation(); + To = ToBoardNotation(move.To); PieceFromHand = move.PieceFromHand; } + private static readonly char A = 'A'; + private static string ToBoardNotation(Vector2 vector) + { + var file = (char)(vector.X + A); + var rank = vector.Y + 1; + return $"{file}{rank}"; + } + public Models.Move ToDomainModel() => PieceFromHand.HasValue - ? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To)) - : new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion); + ? new(PieceFromHand.Value, To) + : new(From!, To, IsPromotion); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs index 68642f9..1df6a20 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -22,6 +22,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels WhichPiece = piece.WhichPiece; } - public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece); + public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md deleted file mode 100644 index 50c4380..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md +++ /dev/null @@ -1,4 +0,0 @@ -### Couch Models - -Couch models should accept domain models during construction and offer a ToDomainModel method which constructs a domain model. -In this way, domain models have the freedom to define their valid states. diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs deleted file mode 100644 index a3dd34e..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels -{ - public class Session : CouchDocument - { - public string Name { get; set; } - public string Player1 { get; set; } - public string? Player2 { get; set; } - public bool IsPrivate { get; set; } - - /// - /// Default constructor and setters are for deserialization. - /// - public Session() : base() - { - Name = string.Empty; - Player1 = string.Empty; - Player2 = string.Empty; - } - - public Session(string id, Models.Session session) : base(id, nameof(Session)) - { - Name = session.Name; - Player1 = session.Player1; - Player2 = session.Player2; - IsPrivate = session.IsPrivate; - } - - public Models.Session ToDomainModel() => new(Name, IsPrivate, Player1, Player2); - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs new file mode 100644 index 0000000..59e8bf8 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class SessionDocument : CouchDocument + { + public string Name { get; set; } + public string Player1 { get; set; } + public string? Player2 { get; set; } + public bool IsPrivate { get; set; } + public IList History { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public SessionDocument() : base(WhichDocumentType.Session) + { + Name = string.Empty; + Player1 = string.Empty; + Player2 = string.Empty; + History = new List(0); + } + + public SessionDocument(Models.Session session) + : base(session.Name, WhichDocumentType.Session) + { + Name = session.Name; + Player1 = session.Player1; + Player2 = session.Player2; + IsPrivate = session.IsPrivate; + History = new List(0); + } + + public SessionDocument(Models.SessionMetadata sessionMetaData) + : base(sessionMetaData.Name, WhichDocumentType.Session) + { + Name = sessionMetaData.Name; + Player1 = sessionMetaData.Player1; + Player2 = sessionMetaData.Player2; + IsPrivate = sessionMetaData.IsPrivate; + History = new List(0); + } + + public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2); + + public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs similarity index 58% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs rename to Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index 22d30c7..7fcb388 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,8 +1,6 @@ -using System; - -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { - public class User : CouchDocument + public class UserDocument : CouchDocument { public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; @@ -14,7 +12,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public string Name { get; set; } public LoginPlatform Platform { get; set; } - public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User)) + public UserDocument(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", WhichDocumentType.User) { Name = name; Platform = platform; diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs new file mode 100644 index 0000000..f5a4f72 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs @@ -0,0 +1,9 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public enum WhichDocumentType + { + User, + Session, + BoardState + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 162e2df..a7d9617 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -12,14 +12,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move); Task CreateGuestUser(string userName); - Task CreateSession(Models.Session session); - Task> ReadSessions(); + Task CreateSession(Models.SessionMetadata session); + Task> ReadSessionMetadatas(); Task IsGuestUser(string userName); Task PostJoinCode(string gameName, string userName); Task ReadSession(string name); - Task> ReadBoardStates(string name); + Task ReadShogi(string name); + Task UpdateSession(Models.Session session); } public class GameboardRepository : IGameboardRepository @@ -34,75 +34,99 @@ namespace Gameboard.ShogiUI.Sockets.Repositories this.logger = logger; } - public async Task> ReadSessions() + public async Task> ReadSessionMetadatas() { - var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}"; - var query = $@"{{ ""selector"": {selector} }}"; - var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var selector = new Dictionary(2) + { + [nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session + }; + var q = new { Selector = selector }; + var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync("_find", content); var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); + var sessions = JsonConvert.DeserializeObject>(responseContent).docs; - if (result == null) - { - logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); - return Array.Empty(); - } - return result.docs - .Select(_ => _.ToDomainModel()) + return sessions + .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2)) .ToList(); } public async Task ReadSession(string name) { + var readShogiTask = ReadShogi(name); var response = await client.GetAsync(name); var responseContent = await response.Content.ReadAsStringAsync(); - var couchModel = JsonConvert.DeserializeObject(responseContent); - return couchModel.ToDomainModel(); + var couchModel = JsonConvert.DeserializeObject(responseContent); + var shogi = await readShogiTask; + if (shogi == null) + { + return null; + } + return couchModel.ToDomainModel(shogi); } - public async Task> ReadBoardStates(string name) + public async Task ReadShogi(string name) { - var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}"; - var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}"; - var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}"; + var selector = new Dictionary(2) + { + [nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState, + [nameof(BoardStateDocument.Name)] = name + }; + var sort = new Dictionary(1) + { + [nameof(BoardStateDocument.CreatedDate)] = "desc" + }; + var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } }); + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); var response = await client.PostAsync("_find", content); var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - - if (result == null) + if (!response.IsSuccessStatusCode) { - logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); - return Array.Empty(); + logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query); + return null; } - return result.docs - .Select(_ => new Models.BoardState(_)) - .ToList(); + var boardStates = JsonConvert + .DeserializeObject>(responseContent) + .docs; + if (boardStates.Length == 0) return null; + + // Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session. + var moves = boardStates.Skip(1).Select(couchModel => + { + var move = couchModel.Move; + Models.Move model = move!.PieceFromHand.HasValue + ? new Models.Move(move.PieceFromHand.Value, move.To) + : new Models.Move(move.From!, move.To, move.IsPromotion); + return model; + }).ToList(); + return new Models.Shogi(moves); } - //public async Task DeleteGame(string gameName) - //{ - // //var uri = $"Session/{gameName}"; - // //await client.DeleteAsync(Uri.EscapeUriString(uri)); - //} - - public async Task CreateSession(Models.Session session) + public async Task CreateSession(Models.SessionMetadata session) { - var couchModel = new Session(session.Name, session); + var sessionDocument = new SessionDocument(session); + var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); + var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); + + var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi()); + var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + + if ((await postSessionDocumentTask).IsSuccessStatusCode) + { + var response = await client.PostAsync(string.Empty, boardStateContent); + return response.IsSuccessStatusCode; + } + return false; + } + + public async Task UpdateSession(Models.Session session) + { + var couchModel = new SessionDocument(session); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); + var response = await client.PutAsync(couchModel.Id, content); return response.IsSuccessStatusCode; } - - public async Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move) - { - var couchModel = new BoardState(sessionName, boardState, move); - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - return response.IsSuccessStatusCode; - } - //public async Task PutJoinPublicSession(PutJoinPublicSession request) //{ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); @@ -169,7 +193,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task CreateGuestUser(string userName) { - var couchModel = new User(userName, User.LoginPlatform.Guest); + var couchModel = new UserDocument(userName, UserDocument.LoginPlatform.Guest); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); return response.IsSuccessStatusCode; @@ -177,7 +201,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task IsGuestUser(string userName) { - var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}")); + var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{UserDocument.GetDocumentId(userName)}")); var response = await client.SendAsync(req); return response.IsSuccessStatusCode; } diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs new file mode 100644 index 0000000..0535a8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class CreateGameRequestValidator : AbstractValidator + { + public CreateGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.CreateGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs new file mode 100644 index 0000000..e3837a0 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class JoinByCodeRequestValidator : AbstractValidator + { + public JoinByCodeRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode); + RuleFor(_ => _.JoinCode).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs new file mode 100644 index 0000000..fa0e2d5 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class JoinGameRequestValidator : AbstractValidator + { + public JoinGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.JoinGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs new file mode 100644 index 0000000..c2ddc8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class ListGamesRequestValidator : AbstractValidator + { + public ListGamesRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.ListGames); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs new file mode 100644 index 0000000..5a4ad8a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class LoadGameRequestValidator : AbstractValidator + { + public LoadGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.LoadGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs new file mode 100644 index 0000000..2eb06f1 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class MoveRequestValidator : AbstractValidator + { + public MoveRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.Move); + RuleFor(_ => _.GameName).NotEmpty(); + RuleFor(_ => _.Move.From) + .Null() + .When(_ => _.Move.PieceFromCaptured.HasValue) + .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); + RuleFor(_ => _.Move.From) + .NotEmpty() + .When(_ => !_.Move.PieceFromCaptured.HasValue) + .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs new file mode 100644 index 0000000..f8be6cf --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -0,0 +1,194 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; +using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets.Services +{ + public interface ISocketService + { + Task HandleSocketRequest(HttpContext context); + } + + /// + /// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages. + /// + public class SocketService : ISocketService + { + private readonly ILogger logger; + private readonly ISocketConnectionManager communicationManager; + private readonly ISocketTokenManager tokenManager; + private readonly ICreateGameHandler createGameHandler; + private readonly IJoinByCodeHandler joinByCodeHandler; + private readonly IJoinGameHandler joinGameHandler; + private readonly IListGamesHandler listGamesHandler; + private readonly ILoadGameHandler loadGameHandler; + private readonly IMoveHandler moveHandler; + private readonly IValidator createGameRequestValidator; + private readonly IValidator joinByCodeRequestValidator; + private readonly IValidator joinGameRequestValidator; + private readonly IValidator listGamesRequestValidator; + private readonly IValidator loadGameRequestValidator; + private readonly IValidator moveRequestValidator; + + public SocketService( + ILogger logger, + ISocketConnectionManager communicationManager, + ISocketTokenManager tokenManager, + ICreateGameHandler createGameHandler, + IJoinByCodeHandler joinByCodeHandler, + IJoinGameHandler joinGameHandler, + IListGamesHandler listGamesHandler, + ILoadGameHandler loadGameHandler, + IMoveHandler moveHandler, + IValidator createGameRequestValidator, + IValidator joinByCodeRequestValidator, + IValidator joinGameRequestValidator, + IValidator listGamesRequestValidator, + IValidator loadGameRequestValidator, + IValidator moveRequestValidator + ) : base() + { + this.logger = logger; + this.communicationManager = communicationManager; + this.tokenManager = tokenManager; + this.createGameHandler = createGameHandler; + this.joinByCodeHandler = joinByCodeHandler; + this.joinGameHandler = joinGameHandler; + this.listGamesHandler = listGamesHandler; + this.loadGameHandler = loadGameHandler; + this.moveHandler = moveHandler; + this.createGameRequestValidator = createGameRequestValidator; + this.joinByCodeRequestValidator = joinByCodeRequestValidator; + this.joinGameRequestValidator = joinGameRequestValidator; + this.listGamesRequestValidator = listGamesRequestValidator; + this.loadGameRequestValidator = loadGameRequestValidator; + this.moveRequestValidator = moveRequestValidator; + } + + public async Task HandleSocketRequest(HttpContext context) + { + var hasToken = context.Request.Query.Keys.Contains("token"); + if (hasToken) + { + var oneTimeToken = context.Request.Query["token"][0]; + var tokenAsGuid = Guid.Parse(oneTimeToken); + var userName = tokenManager.GetUsername(tokenAsGuid); + if (userName != null) + { + var socket = await context.WebSockets.AcceptWebSocketAsync(); + + communicationManager.SubscribeToBroadcast(socket, userName); + while (socket.State == WebSocketState.Open) + { + try + { + 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)) + { + await socket.SendTextAsync("Error: Action not recognized."); + continue; + } + switch (request.Action) + { + case ClientAction.ListGames: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) + { + await listGamesHandler.Handle(req, userName); + } + break; + } + case ClientAction.CreateGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) + { + await createGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) + { + await joinGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) + { + await joinByCodeHandler.Handle(req, userName); + } + break; + } + case ClientAction.LoadGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) + { + await loadGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.Move: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) + { + await moveHandler.Handle(req, userName); + } + break; + } + } + } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } + } + communicationManager.UnsubscribeFromBroadcastAndGames(userName); + return; + } + } + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + + public async Task ValidateRequestAndReplyIfInvalid(WebSocket socket, IValidator validator, TRequest request) + { + var results = validator.Validate(request); + if (!results.IsValid) + { + await socket.SendTextAsync(string.Join('\n', results.Errors.Select(_ => _.ErrorMessage).ToString())); + } + return results.IsValid; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index cda318d..a8e52ac 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -1,8 +1,11 @@ +using FluentValidation; using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.Services; +using Gameboard.ShogiUI.Sockets.Services.RequestValidators; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -39,11 +42,19 @@ namespace Gameboard.ShogiUI.Sockets services.AddSingleton(); // Managers - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Services + services.AddSingleton, CreateGameRequestValidator>(); + services.AddSingleton, JoinByCodeRequestValidator>(); + services.AddSingleton, JoinGameRequestValidator>(); + services.AddSingleton, ListGamesRequestValidator>(); + services.AddSingleton, LoadGameRequestValidator>(); + services.AddSingleton, MoveRequestValidator>(); + services.AddSingleton(); // Repositories services.AddHttpClient("couchdb", c => @@ -77,7 +88,7 @@ namespace Gameboard.ShogiUI.Sockets } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) { var origins = new[] { "http://localhost:3000", "https://localhost:3000", @@ -135,10 +146,13 @@ namespace Gameboard.ShogiUI.Sockets Formatting = Formatting.Indented, ContractResolver = new DefaultContractResolver { - NamingStrategy = new CamelCaseNamingStrategy(), + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = true + } }, Converters = new[] { new StringEnumConverter() }, - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, }; } } diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index 02bde77..a15e786 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -5,10 +5,12 @@ + + diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs new file mode 100644 index 0000000..1ef33de --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs @@ -0,0 +1,92 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PathFinding; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.UnitTests.PathFinding +{ + [TestClass] + public class PlanarCollectionShould + { + private class SimpleElement : IPlanarElement + { + public static int Seed { get; private set; } + public MoveSet MoveSet => null; + public bool IsUpsideDown => false; + + + public SimpleElement() + { + Seed = Seed++; + } + } + + private Fixture fixture; + + [TestInitialize] + public void TestInitialize() + { + fixture = new Fixture(); + } + + [TestMethod] + public void Index() + { + // Arrange + var collection = new PlanarCollection(10, 10); + var expected1 = new SimpleElement(); + var expected2 = new SimpleElement(); + + // Act + collection[0, 0] = expected1; + collection[2, 1] = expected2; + + // Assert + collection[0, 0].Should().Be(expected1); + collection[2, 1].Should().Be(expected2); + } + + [TestMethod] + public void Iterate() + { + // Arrange + var expected = new List(); + for (var i = 0; i < 9; i++) expected.Add(new SimpleElement()); + var collection = new PlanarCollection(3, 3); + for (var x = 0; x < 3; x++) + for (var y = 0; y < 3; y++) + collection[x, y] = expected[x + y]; + + // Act + var actual = new List(); + foreach (var elem in collection) + actual.Add(elem); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void Yep() + { + var collection = new PlanarCollection(3, 3); + collection[0, 0] = new SimpleElement(); + collection[1, 0] = new SimpleElement(); + collection[0, 1] = new SimpleElement(); + + // Act + var array2d = new SimpleElement[3, 3]; + for (var x = 0; x < 3; x++) + for (var y = 0; y < 3; y++) + { + array2d[x, y] = collection[x, y]; + } + + + Console.WriteLine("hey"); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index c558571..e5151c1 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -1,11 +1,10 @@ using FluentAssertions; -using Gameboard.ShogiUI.Rules; -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Linq; using System.Numerics; - +using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPlayer; +using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPiece; namespace Gameboard.ShogiUI.UnitTests.Rules { [TestClass] @@ -15,54 +14,54 @@ namespace Gameboard.ShogiUI.UnitTests.Rules public void InitializeBoardState() { // Assert - var board = new ShogiBoard().Board; + var board = new Shogi().Board; // Assert pieces do not start promoted. foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); // Assert Player1. for (var y = 0; y < 3; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); + board[y, x]?.Owner.Should().Be(WhichPlayer.Player2); board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); - board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[4, 0].WhichPiece.Should().Be(WhichPiece.King); - board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); - board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[0, 1].Should().BeNull(); + board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); + board[0, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[0, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[0, 4].WhichPiece.Should().Be(WhichPiece.King); + board[0, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[0, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[0, 7].WhichPiece.Should().Be(WhichPiece.Knight); + board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 0].Should().BeNull(); board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook); - for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); - board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); - board[8, 1].Should().BeNull(); - for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + for (var x = 2; x < 7; x++) board[1, x].Should().BeNull(); + board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + board[1, 8].Should().BeNull(); + for (var x = 0; x < 9; x++) board[2, x].WhichPiece.Should().Be(WhichPiece.Pawn); // Assert empty locations. for (var y = 3; y < 6; y++) for (var x = 0; x < 9; x++) - board[x, y].Should().BeNull(); + board[y, x].Should().BeNull(); // Assert Player2. for (var y = 6; y < 9; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); - board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); - board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[4, 8].WhichPiece.Should().Be(WhichPiece.King); - board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[y, x]?.Owner.Should().Be(WhichPlayer.Player1); + board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[8, 1].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[8, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[8, 4].WhichPiece.Should().Be(WhichPiece.King); + board[8, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[8, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[8, 7].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[0, 7].Should().BeNull(); - board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); - for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); + board[7, 0].Should().BeNull(); + board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + for (var x = 2; x < 7; x++) board[7, x].Should().BeNull(); board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook); - board[8, 7].Should().BeNull(); - for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + board[7, 8].Should().BeNull(); + for (var x = 0; x < 9; x++) board[6, x].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -70,60 +69,52 @@ namespace Gameboard.ShogiUI.UnitTests.Rules { var moves = new[] { - new Move - { // Pawn - From = new Vector2(0, 6), - To = new Vector2(0, 5) - } + new Move(new Vector2(0, 6), new Vector2(0, 5)) }; - var shogi = new ShogiBoard(moves); - shogi.Board[0, 6].Should().BeNull(); - shogi.Board[0, 5].WhichPiece.Should().Be(WhichPiece.Pawn); + var shogi = new Shogi(moves); + shogi.Board[6, 0].Should().BeNull(); + shogi.Board[5, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] public void PreventInvalidMoves_MoveFromEmptyPosition() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); // Prerequisit shogi.Board[4, 4].Should().BeNull(); // Act - var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }); + var moveSuccess = shogi.Move(new Move(new Vector2(4, 4), new Vector2(4, 5))); // Assert moveSuccess.Should().BeFalse(); shogi.Board[4, 4].Should().BeNull(); - shogi.Board[4, 5].Should().BeNull(); + shogi.Board[5, 4].Should().BeNull(); } [TestMethod] public void PreventInvalidMoves_MoveToCurrentPosition() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 6), To = new Vector2(0, 6) }); + var moveSuccess = shogi.Move(new Move(new Vector2(0, 6), new Vector2(0, 6))); // Assert moveSuccess.Should().BeFalse(); - shogi.Board[0, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[6, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] public void PreventInvalidMoves_MoveSet() { - var invalidLanceMove = new Move - { - // Bishop moving lateral - From = new Vector2(1, 1), - To = new Vector2(2, 1) - }; + // Bishop moving lateral + var invalidLanceMove = new Move(new Vector2(1, 1), new Vector2(2, 1)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); @@ -135,30 +126,26 @@ namespace Gameboard.ShogiUI.UnitTests.Rules public void PreventInvalidMoves_Ownership() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); - shogi.Board[8, 2].Owner.Should().Be(WhichPlayer.Player2); + shogi.Board[2, 8].Owner.Should().Be(WhichPlayer.Player2); // Act - Move Player2 Pawn when it's Player1 turn. - var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }); + var moveSuccess = shogi.Move(new Move(new Vector2(8, 2), new Vector2(8, 3))); // Assert moveSuccess.Should().BeFalse(); - shogi.Board[8, 6].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board[8, 5].Should().BeNull(); + shogi.Board[6, 8].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[5, 8].Should().BeNull(); } [TestMethod] public void PreventInvalidMoves_MoveThroughAllies() { - var invalidLanceMove = new Move - { - // Lance moving through the pawn before it. - From = new Vector2(0, 8), - To = new Vector2(0, 4) - }; + // Lance moving through the pawn before it. + var invalidLanceMove = new Move(new Vector2(0, 8), new Vector2(0, 4)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); @@ -169,20 +156,16 @@ namespace Gameboard.ShogiUI.UnitTests.Rules [TestMethod] public void PreventInvalidMoves_CaptureAlly() { - var invalidKnightMove = new Move - { - // Knight capturing allied Pawn - From = new Vector2(1, 8), - To = new Vector2(0, 6) - }; + // Knight capturing allied Pawn + var invalidKnightMove = new Move(new Vector2(1, 8), new Vector2(0, 6)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidKnightMove); moveSuccess.Should().BeFalse(); // Assert the Knight has not actually moved or captured. - shogi.Board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); - shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board[2, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -192,25 +175,25 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(6, 2), new Vector2(6, 3)), // P1 Bishop puts P2 in check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) } + new Move(new Vector2(1, 7), new Vector2(6, 2)) }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisit shogi.InCheck.Should().Be(WhichPlayer.Player2); // Act - P2 moves Lance while remaining in check. - var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }); + var moveSuccess = shogi.Move(new Move(new Vector2(0, 8), new Vector2(0, 7))); // Assert moveSuccess.Should().BeFalse(); shogi.InCheck.Should().Be(WhichPlayer.Player2); shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board[8, 7].Should().BeNull(); + shogi.Board[7, 8].Should().BeNull(); } [TestMethod] @@ -220,31 +203,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(0, 2), new Vector2(0, 3) ), // P1 Bishop takes P2 Pawn - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2) ), // P2 Gold, block check from P1 Bishop. - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1) ), // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - new Move { From = new Vector2(6, 2), To = new Vector2(7, 1), IsPromotion = true }, + new Move(new Vector2(6, 2), new Vector2(7, 1), true ), // P2 Pawn again - new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, + new Move(new Vector2(0, 3), new Vector2(0, 4) ), // P1 Bishop takes P2 Knight - new Move { From = new Vector2(7, 1), To = new Vector2(7, 0) }, + new Move(new Vector2(7, 1), new Vector2(7, 0) ), // P2 Pawn again - new Move { From = new Vector2(0, 4), To = new Vector2(0, 5) }, + new Move(new Vector2(0, 4), new Vector2(0, 5) ), // P1 Bishop takes P2 Lance - new Move { From = new Vector2(7, 0), To = new Vector2(8, 0) }, + new Move(new Vector2(7, 0), new Vector2(8, 0) ), // P2 Lance (move to make room for attempted P1 Pawn placement) - new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, + new Move(new Vector2(0, 0), new Vector2(0, 1) ), // P1 arbitrary move - new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, + new Move(new Vector2(4, 8), new Vector2(4, 7) ), // P2 Pawn again, takes P1 Pawn - new Move { From = new Vector2(0, 5), To = new Vector2(0, 6) }, + new Move(new Vector2(0, 5) , new Vector2(0, 6) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4); @@ -255,27 +238,27 @@ namespace Gameboard.ShogiUI.UnitTests.Rules // Act | Assert - It is P1 turn /// try illegally placing Knight from the hand. - shogi.Board[7, 0].Should().BeNull(); - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 0) }); + shogi.Board[0, 7].Should().BeNull(); + var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 0].Should().BeNull(); - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 1) }); + shogi.Board[0, 7].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 1))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 1].Should().BeNull(); + shogi.Board[1, 7].Should().BeNull(); /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Board[7, 0].Should().BeNull(); + shogi.Board[0, 7].Should().BeNull(); /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 0].Should().BeNull(); + shogi.Board[0, 7].Should().BeNull(); } [TestMethod] @@ -285,34 +268,34 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), + // P2 Pawn + new Move(new Vector2(8, 2), new Vector2(8, 3)), // P1 Bishop, check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2)), // P2 Gold, block check - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1)), // P1 arbitrary move - new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, + new Move(new Vector2(0, 6), new Vector2(0, 5)), // P2 Bishop - new Move { From = new Vector2(7, 1), To = new Vector2(8, 2) }, + new Move(new Vector2(7, 1), new Vector2(8, 2)), // P1 Bishop takes P2 Lance - new Move { From = new Vector2(6, 2), To = new Vector2(8, 0) }, + new Move(new Vector2(6, 2), new Vector2(8, 0)), // P2 Bishop - new Move { From = new Vector2(8, 2), To = new Vector2(7, 1) }, + new Move(new Vector2(8, 2), new Vector2(7, 1)), // P1 arbitrary move - new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, + new Move(new Vector2(0, 5), new Vector2(0, 4)), // P2 Bishop, check - new Move { From = new Vector2(7, 1), To = new Vector2(2, 6) }, + new Move(new Vector2(7, 1), new Vector2(2, 6)), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.InCheck.Should().Be(WhichPlayer.Player1); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act - P1 tries to place a Lance while in check. - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) }); + var dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(4, 4))); // Assert dropSuccess.Should().BeFalse(); @@ -328,31 +311,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3)), // P1 Bishop, capture P2 Pawn, check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2)), // P2 Gold, block check - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1)), // P1 Bishop capture P2 Bishop - new Move { From = new Vector2(6, 2), To = new Vector2(7, 1) }, + new Move(new Vector2(6, 2), new Vector2(7, 1)), // P2 arbitrary move - new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, + new Move(new Vector2(0, 0), new Vector2(0, 1)), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[4, 0].Should().NotBeNull(); + shogi.Board[0, 4].Should().NotBeNull(); // Act - P1 tries to place Bishop from hand to an already-occupied position - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) }); + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, new Vector2(4, 0))); // Assert dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[4, 0].WhichPiece.Should().Be(WhichPiece.King); + shogi.Board[0, 4].WhichPiece.Should().Be(WhichPiece.King); } [TestMethod] @@ -362,14 +345,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Bishop, check - shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }); + shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2))); // Assert shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -382,14 +365,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } + new Move(new Vector2(6, 2), new Vector2(6, 3)) }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(7, 1) }); + var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(7, 1))); // Assert moveSuccess.Should().BeTrue(); @@ -398,20 +381,20 @@ namespace Gameboard.ShogiUI.UnitTests.Rules .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) .Should() .Be(1); - shogi.Board[1, 7].Should().BeNull(); - shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board[7, 1].Should().BeNull(); + shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); shogi.Hands[WhichPlayer.Player1] .Should() .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); // Act - P2 Silver captures P1 Bishop - moveSuccess = shogi.Move(new Move { From = new Vector2(6, 0), To = new Vector2(7, 1) }); + moveSuccess = shogi.Move(new Move(new Vector2(6, 0), new Vector2(7, 1))); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[6, 0].Should().BeNull(); - shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + shogi.Board[0, 6].Should().BeNull(); + shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); shogi.Board .Cast() .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) @@ -428,19 +411,19 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } - }; - var shogi = new ShogiBoard(moves); + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3) ) + }; + var shogi = new Shogi(moves); // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2), IsPromotion = true }); + var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2), true)); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[1, 7].Should().BeNull(); - shogi.Board[6, 2].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); + shogi.Board[7, 1].Should().BeNull(); + shogi.Board[2, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); } [TestMethod] @@ -450,30 +433,30 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Rook - new Move { From = new Vector2(7, 7), To = new Vector2(4, 7) }, + new Move(new Vector2(7, 7), new Vector2(4, 7) ), // P2 Gold - new Move { From = new Vector2(3, 0), To = new Vector2(2, 1) }, + new Move(new Vector2(3, 0), new Vector2(2, 1) ), // P1 Pawn - new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, + new Move(new Vector2(4, 6), new Vector2(4, 5) ), // P2 other Gold - new Move { From = new Vector2(5, 0), To = new Vector2(6, 1) }, + new Move(new Vector2(5, 0), new Vector2(6, 1) ), // P1 same Pawn - new Move { From = new Vector2(4, 5), To = new Vector2(4, 4) }, + new Move(new Vector2(4, 5), new Vector2(4, 4) ), // P2 Pawn - new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, + new Move(new Vector2(4, 2), new Vector2(4, 3) ), // P1 Pawn takes P2 Pawn - new Move { From = new Vector2(4, 4), To = new Vector2(4, 3) }, + new Move(new Vector2(4, 4), new Vector2(4, 3) ), // P2 King - new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, + new Move(new Vector2(4, 0), new Vector2(4, 1) ), // P1 Pawn promotes, threatens P2 King - new Move { From = new Vector2(4, 3), To = new Vector2(4, 2), IsPromotion = true }, + new Move(new Vector2(4, 3), new Vector2(4, 2), true ), // P2 King retreat - new Move { From = new Vector2(4, 1), To = new Vector2(4, 0) }, + new Move(new Vector2(4, 1), new Vector2(4, 0) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 2), To = new Vector2(4, 1) }); + var moveSuccess = shogi.Move(new Move(new Vector2(4, 2), new Vector2(4, 1))); // Assert - checkmate moveSuccess.Should().BeTrue(); diff --git a/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs deleted file mode 100644 index f878da3..0000000 --- a/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentAssertions; -using Gameboard.ShogiUI.Sockets.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Gameboard.ShogiUI.UnitTests.Sockets -{ - [TestClass] - public class CoordsModelShould - { - [TestMethod] - public void ConvertToNotation() - { - var letters = "ABCDEFGHI"; - - for (var x = 0; x < 8; x++) // file - { - for (var y = 0; y < 8; y++) // rank - { - var move = new Coords(x, y); - var actual = move.ToBoardNotation(); - var expected = $"{letters[x]}{y + 1}"; - actual.Should().Be(expected); - } - } - } - } -} diff --git a/Gameboard.ShogiUI.xUnitTests/GameShould.cs b/Gameboard.ShogiUI.xUnitTests/GameShould.cs new file mode 100644 index 0000000..80f299d --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/GameShould.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests +{ + public class GameShould + { + [Fact] + public void DiscardNullPLayers() + { + var game = new Game("Test", "P1", null); + + game.Players.Count.Should().Be(1); + } + } +} diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj new file mode 100644 index 0000000..5cc61ef --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs new file mode 100644 index 0000000..2ae7da4 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs @@ -0,0 +1,76 @@ +using AutoFixture; +using FluentAssertions; +using FluentAssertions.Execution; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.Services.RequestValidators; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests.RequestValidators +{ + public class MoveRequestValidatorShould + { + private readonly Fixture fixture; + private readonly MoveRequestValidator validator; + public MoveRequestValidatorShould() + { + fixture = new Fixture(); + validator = new MoveRequestValidator(); + } + + [Fact] + public void PreventInvalidPropertyCombinations() + { + // Arrange + var request = fixture.Create(); + + // Act + var results = validator.Validate(request); + + // Assert + using (new AssertionScope()) + { + results.IsValid.Should().BeFalse(); + } + } + + [Fact] + public void AllowValidPropertyCombinations() + { + // Arrange + var requestWithoutFrom = new MoveRequest() + { + Action = ClientAction.Move, + GameName = "Some game name", + Move = new Move() + { + IsPromotion = false, + PieceFromCaptured = WhichPiece.Bishop, + To = "A4" + } + }; + var requestWithoutPieceFromCaptured = new MoveRequest() + { + Action = ClientAction.Move, + GameName = "Some game name", + Move = new Move() + { + From = "A1", + IsPromotion = false, + To = "A4" + } + }; + + // Act + var results = validator.Validate(requestWithoutFrom); + var results2 = validator.Validate(requestWithoutPieceFromCaptured); + + // Assert + using (new AssertionScope()) + { + results.IsValid.Should().BeTrue(); + results2.IsValid.Should().BeTrue(); + } + } + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index f2c9323..5f2f769 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -34,7 +35,7 @@ namespace PathFinding { return false; } - var element = collection[origin.X, origin.Y]; + var element = collection[origin.Y, origin.X]; if (element == null) return false; var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); @@ -49,7 +50,7 @@ namespace PathFinding while (shouldPath && next != destination) { next = Vector2.Add(next, path.Direction); - var collider = collection[(int)next.X, (int)next.Y]; + var collider = collection[(int)next.Y, (int)next.X]; if (collider != null) { callback?.Invoke(collider, next); @@ -65,14 +66,19 @@ namespace PathFinding public void PathEvery(Vector2 from, Callback callback) { - var element = collection[from.X, from.Y]; + var element = collection[from.Y, from.X]; + if (element == null) + { + Console.WriteLine("Null element in PathEvery"); + return; + } foreach (var path in element.MoveSet.GetMoves()) { var shouldPath = true; var next = Vector2.Add(from, path.Direction); ; while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) { - var collider = collection[(int)next.X, (int)next.Y]; + var collider = collection[(int)next.Y, (int)next.X]; if (collider != null) { callback(collider, next); @@ -97,7 +103,7 @@ namespace PathFinding var next = Vector2.Add(origin, direction); while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) { - var element = collection[next.X, next.Y]; + var element = collection[next.Y, next.X]; if (element != null) callback(element, next); next = Vector2.Add(next, direction); } diff --git a/Gameboard.ShogiUI.Rules/PlanarCollection.cs b/PathFinding/PlanarCollection.cs similarity index 72% rename from Gameboard.ShogiUI.Rules/PlanarCollection.cs rename to PathFinding/PlanarCollection.cs index d606134..9fa7266 100644 --- a/Gameboard.ShogiUI.Rules/PlanarCollection.cs +++ b/PathFinding/PlanarCollection.cs @@ -1,10 +1,10 @@ -using PathFinding; -using System; +using System; using System.Collections; using System.Collections.Generic; -namespace Gameboard.ShogiUI.Rules +namespace PathFinding { + // TODO: Get rid of this thing in favor of T[,] multi-dimensional array with extension methods. public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { public delegate void ForEachDelegate(T element, int x, int y); @@ -19,12 +19,12 @@ namespace Gameboard.ShogiUI.Rules array = new T[width * height]; } - public T? this[int x, int y] + public T? this[int y, int x] { get => array[y * width + x]; set => array[y * width + x] = value; } - public T? this[float x, float y] + public T? this[float y, float x] { get => array[(int)y * width + (int)x]; set => array[(int)y * width + (int)x] = value; @@ -32,8 +32,8 @@ namespace Gameboard.ShogiUI.Rules public int GetLength(int dimension) => dimension switch { - 0 => width, - 1 => height, + 0 => height, + 1 => width, _ => throw new IndexOutOfRangeException() }; @@ -43,15 +43,17 @@ namespace Gameboard.ShogiUI.Rules { for (var y = 0; y < height; y++) { - if (this[x, y] != null) - callback(this[x, y], x, y); + var elem = this[y, x]; + if (elem != null) + callback(elem, x, y); } } } public IEnumerator GetEnumerator() { - foreach (var item in array) yield return item; + foreach (var item in array) + if (item != null) yield return item; } IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); From b10f61a48965d1262eaa81f2d7162997ebc6ecbe Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 1 Aug 2021 17:32:43 -0500 Subject: [PATCH 16/27] yep --- Benchmarking/Benchmarks.cs | 7 +- .../Api/GetGuestToken.cs | 20 + .../Api/{Messages => }/GetToken.cs | 2 +- .../Api/Messages/GetGuestToken.cs | 21 - .../Api/{Messages => }/PostGameInvitation.cs | 2 +- .../Api/PostMove.cs | 14 + .../Api/{Messages => }/PostSession.cs | 2 +- .../Socket/{Messages => }/CreateGame.cs | 9 +- .../Socket/IRequest.cs | 9 + .../Socket/{Interfaces => }/IResponse.cs | 2 +- .../Socket/Interfaces/IRequest.cs | 9 - .../Socket/{Messages => }/JoinGame.cs | 19 +- .../Socket/{Messages => }/ListGames.cs | 9 +- .../Socket/{Messages => }/LoadGame.cs | 9 +- .../Socket/{Messages => }/Move.cs | 13 +- .../Socket/Types/WhichPlayer.cs | 8 - .../{Socket => }/Types/BoardState.cs | 4 +- .../{Socket => }/Types/ClientActionEnum.cs | 2 +- .../{Socket => }/Types/Game.cs | 2 +- .../{Socket => }/Types/Move.cs | 2 +- .../{Socket => }/Types/Piece.cs | 2 +- .../{Socket => }/Types/WhichPiece.cs | 2 +- .../Types/WhichPlayer.cs | 8 + .../Controllers/GameController.cs | 99 ++- .../Controllers/SocketController.cs | 57 +- .../Extensions/ModelExtensions.cs | 10 +- .../Managers/ActiveSessionManager.cs | 33 - .../ClientActionHandlers/CreateGameHandler.cs | 6 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 2 +- .../ClientActionHandlers/JoinGameHandler.cs | 6 +- .../ClientActionHandlers/ListGamesHandler.cs | 6 +- .../ClientActionHandlers/LoadGameHandler.cs | 14 +- .../ClientActionHandlers/MoveHandler.cs | 74 +-- .../Managers/GameboardManager.cs | 34 +- .../Managers/SocketConnectionManager.cs | 9 +- .../Managers/SocketTokenManager.cs | 2 +- .../Managers/Utility/Request.cs | 10 - Gameboard.ShogiUI.Sockets/Models/Move.cs | 41 +- Gameboard.ShogiUI.Sockets/Models/Piece.cs | 9 +- .../Models/SessionMetadata.cs | 9 +- Gameboard.ShogiUI.Sockets/Models/Shogi.cs | 358 ++++++----- Gameboard.ShogiUI.Sockets/Models/User.cs | 18 + .../CouchModels/BoardStateDocument.cs | 23 +- .../CouchModels/CouchFindResult.cs | 2 + .../Repositories/CouchModels/Move.cs | 2 +- .../Repositories/CouchModels/Piece.cs | 2 +- .../Repositories/CouchModels/UserDocument.cs | 27 +- .../Repositories/GameboardRepository.cs | 89 ++- .../CreateGameRequestValidator.cs | 4 +- .../JoinByCodeRequestValidator.cs | 4 +- .../JoinGameRequestValidator.cs | 4 +- .../ListGamesRequestValidator.cs | 4 +- .../LoadGameRequestValidator.cs | 4 +- .../RequestValidators/MoveRequestValidator.cs | 4 +- .../Services/SocketService.cs | 207 +++--- .../Utility/JsonRequest.cs | 4 +- .../Services/Utility/Request.cs | 10 + .../Services/Utility/Response.cs | 10 + Gameboard.ShogiUI.Sockets/Startup.cs | 24 +- .../Utilities/CoordsToNotationCollection.cs | 48 ++ .../Utilities/NotationHelper.cs | 37 ++ .../PathFinding/PlanarCollectionShould.cs | 73 +-- .../PathFinding/TestPlanarCollection.cs | 48 ++ .../Rules/ShogiBoardShould.cs | 457 +------------- .../CoordsToNotationCollectionShould.cs | 41 ++ Gameboard.ShogiUI.xUnitTests/GameShould.cs | 2 +- .../Gameboard.ShogiUI.xUnitTests.csproj | 6 + .../NotationHelperShould.cs | 22 + .../MoveRequestValidatorShould.cs | 4 +- Gameboard.ShogiUI.xUnitTests/ShogiShould.cs | 594 ++++++++++++++++++ .../xunit.runner.json | 3 + PathFinding/Direction.cs | 16 +- PathFinding/IPlanarCollection.cs | 7 +- PathFinding/PathFinder2D.cs | 19 +- PathFinding/PathFinding.csproj | 4 + PathFinding/PlanarCollection.cs | 61 -- 76 files changed, 1655 insertions(+), 1185 deletions(-) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Api/{Messages => }/GetToken.cs (74%) delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Api/{Messages => }/PostGameInvitation.cs (85%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Api/{Messages => }/PostSession.cs (74%) rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Messages => }/CreateGame.cs (64%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Interfaces => }/IResponse.cs (58%) delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Messages => }/JoinGame.cs (56%) rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Messages => }/ListGames.cs (60%) rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Messages => }/LoadGame.cs (64%) rename Gameboard.ShogiUI.Sockets.ServiceModels/Socket/{Messages => }/Move.cs (58%) delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/BoardState.cs (70%) rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/ClientActionEnum.cs (65%) rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/Game.cs (92%) rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/Move.cs (84%) rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/Piece.cs (71%) rename Gameboard.ShogiUI.Sockets.ServiceModels/{Socket => }/Types/WhichPiece.cs (67%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/User.cs rename Gameboard.ShogiUI.Sockets/{Managers => Services}/Utility/JsonRequest.cs (66%) create mode 100644 Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs create mode 100644 Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs create mode 100644 Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs create mode 100644 Gameboard.ShogiUI.UnitTests/PathFinding/TestPlanarCollection.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/ShogiShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/xunit.runner.json delete mode 100644 PathFinding/PlanarCollection.cs diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 5c7c052..4270a3e 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -10,6 +10,7 @@ namespace Benchmarking public class Benchmarks { private readonly Vector2[] directions; + // Consumer is for IEnumerables. private readonly Consumer consumer = new(); public Benchmarks() @@ -43,9 +44,13 @@ namespace Benchmarking //for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); } - //[Benchmark] + [Benchmark] public void One() { + for(var i=0; i<10000; i++) + { + Guid.NewGuid(); + } } //[Benchmark] diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs new file mode 100644 index 0000000..41f93ec --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs @@ -0,0 +1,20 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api +{ + public class GetGuestToken + { + } + + public class GetGuestTokenResponse + { + public string PlayerName { get; } + public Guid OneTimeToken { get; } + + public GetGuestTokenResponse(string playerName, Guid token) + { + PlayerName = playerName; + OneTimeToken = token; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs similarity index 74% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs index eb59291..acc03a9 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs @@ -1,6 +1,6 @@ using System; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { public class GetTokenResponse { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs deleted file mode 100644 index 6049f8e..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages -{ - public class GetGuestToken - { - public string? ClientId { get; set; } - } - - public class GetGuestTokenResponse - { - public string ClientId { get; } - public Guid OneTimeToken { get; } - - public GetGuestTokenResponse(string clientId, Guid token) - { - ClientId = clientId; - OneTimeToken = token; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs similarity index 85% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs index 10850f6..acb270f 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { public class PostGameInvitation { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs new file mode 100644 index 0000000..153b1d9 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs @@ -0,0 +1,14 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using System.ComponentModel.DataAnnotations; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api +{ + public class PostMove + { + [Required] + public string GameName { get; set; } + + [Required] + public Move Move { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs similarity index 74% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs index 8b7061a..de71f67 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { public class PostSession { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs similarity index 64% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs index 9fa701a..6aa082d 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs @@ -1,7 +1,6 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class CreateGameRequest : IRequest { @@ -17,9 +16,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public Game Game { get; set; } public string PlayerName { get; set; } - public CreateGameResponse(ClientAction action) + public CreateGameResponse() { - Action = action.ToString(); + Action = ClientAction.CreateGame.ToString(); Error = string.Empty; Game = new Game(); PlayerName = string.Empty; diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs new file mode 100644 index 0000000..aca4cc7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs @@ -0,0 +1,9 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket +{ + public interface IRequest + { + ClientAction Action { get; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs similarity index 58% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs index 746d3ab..77f0dc2 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public interface IResponse { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs deleted file mode 100644 index 79f20e5..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces -{ - public interface IRequest - { - ClientAction Action { get; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs similarity index 56% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs index 80a15bf..48d1b34 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs @@ -1,7 +1,6 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class JoinByCodeRequest : IRequest { @@ -17,17 +16,25 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class JoinGameResponse : IResponse { - public string Action { get; } + public string Action { get; protected set; } public string Error { get; set; } public string GameName { get; set; } public string PlayerName { get; set; } - public JoinGameResponse(ClientAction action) + public JoinGameResponse() { - Action = action.ToString(); + Action = ClientAction.JoinGame.ToString(); Error = ""; GameName = ""; PlayerName = ""; } } + + public class JoinByCodeResponse : JoinGameResponse, IResponse + { + public JoinByCodeResponse() + { + Action = ClientAction.JoinByCode.ToString(); + } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs similarity index 60% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs index bbd9944..d8e5556 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs @@ -1,9 +1,8 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class ListGamesRequest : IRequest { @@ -16,9 +15,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public string Error { get; set; } public IReadOnlyList Games { get; set; } - public ListGamesResponse(ClientAction action) + public ListGamesResponse() { - Action = action.ToString(); + Action = ClientAction.ListGames.ToString(); Error = ""; Games = new Collection(); } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs similarity index 64% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs index d268e07..09884cb 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs @@ -1,8 +1,7 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Collections.Generic; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class LoadGameRequest : IRequest { @@ -19,9 +18,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public IList MoveHistory { get; set; } public string Error { get; set; } - public LoadGameResponse(ClientAction action) + public LoadGameResponse() { - Action = action.ToString(); + Action = ClientAction.LoadGame.ToString(); } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs similarity index 58% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs index 3ce4c6e..d7b9e91 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs @@ -1,7 +1,6 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class MoveRequest : IRequest { @@ -15,16 +14,16 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public string Action { get; } public string Error { get; set; } public string GameName { get; set; } - public BoardState BoardState { get; set; } public string PlayerName { get; set; } + public Move Move { get; set; } - public MoveResponse(ClientAction action) + public MoveResponse() { - Action = action.ToString(); + Action = ClientAction.Move.ToString(); Error = string.Empty; GameName = string.Empty; - BoardState = new BoardState(); PlayerName = string.Empty; + Move = new Move(); } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs deleted file mode 100644 index 835b8e6..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs similarity index 70% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs index 3ba7e8b..398ba4a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public class BoardState { - public Piece[,] Board { get; set; } = new Piece[0, 0]; + public Dictionary Board { get; set; } = new Dictionary(); public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); public WhichPlayer? PlayerInCheck { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs similarity index 65% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs index 5215171..f3a655b 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public enum ClientAction { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs similarity index 92% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs index f9fb1bb..aa489ea 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public class Game { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs similarity index 84% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs index ef9723a..7116648 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public class Move { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs similarity index 71% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs index 71d9dbc..1c0ee78 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public class Piece { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs similarity index 67% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs index b83e22e..214bdba 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { public enum WhichPiece { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs new file mode 100644 index 0000000..2ce7270 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 15bf163..bb17546 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,8 +1,10 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Api; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System; using System.Linq; using System.Threading.Tasks; @@ -13,34 +15,36 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [Route("[controller]")] public class GameController : ControllerBase { - private readonly IGameboardManager manager; + private static readonly string UsernameClaim = "preferred_username"; + private readonly IGameboardManager gameboardManager; + private readonly IGameboardRepository gameboardRepository; private readonly ISocketConnectionManager communicationManager; - private readonly IGameboardRepository repository; + private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value; public GameController( IGameboardRepository repository, IGameboardManager manager, ISocketConnectionManager communicationManager) { - this.manager = manager; + gameboardManager = manager; + gameboardRepository = repository; this.communicationManager = communicationManager; - this.repository = repository; } [HttpPost("JoinCode")] public async Task PostGameInvitation([FromBody] PostGameInvitation request) { - var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; - var isPlayer1 = await manager.IsPlayer1(request.SessionName, userName); - if (isPlayer1) - { - var code = await repository.PostJoinCode(request.SessionName, userName); - return new CreatedResult("", new PostGameInvitationResponse(code)); - } - else - { - return new UnauthorizedResult(); - } + + //var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName); + //if (isPlayer1) + //{ + // var code = await gameboardRepository.PostJoinCode(request.SessionName, userName); + // return new CreatedResult("", new PostGameInvitationResponse(code)); + //} + //else + //{ + return new UnauthorizedResult(); + //} } [AllowAnonymous] @@ -48,19 +52,58 @@ namespace Gameboard.ShogiUI.Sockets.Controllers public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) { - var isGuest = manager.IsGuest(request.GuestId); - var isPlayer1 = manager.IsPlayer1(request.SessionName, request.GuestId); - if (isGuest && await isPlayer1) - { - var code = await repository.PostJoinCode(request.SessionName, request.GuestId); - return new CreatedResult("", new PostGameInvitationResponse(code)); - } - else - { - return new UnauthorizedResult(); - } + //var isGuest = gameboardManager.IsGuest(request.GuestId); + //var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId); + //if (isGuest && await isPlayer1) + //{ + // var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId); + // return new CreatedResult("", new PostGameInvitationResponse(code)); + //} + //else + //{ + return new UnauthorizedResult(); + //} } + [HttpPost("{gameName}/Move")] + public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request) + { + Models.User? user = null; + if (Request.Cookies.ContainsKey(SocketController.WebSessionKey)) + { + var webSessionId = Guid.Parse(Request.Cookies[SocketController.WebSessionKey]!); + user = await gameboardManager.ReadUser(webSessionId); + } + else if (!string.IsNullOrEmpty(JwtUserName)) + { + user = await gameboardManager.ReadUser(JwtUserName); + } + + var session = await gameboardManager.ReadSession(gameName); + + if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name)) + { + throw new UnauthorizedAccessException("You are not seated at this game."); + } + + var move = request.Move; + var moveModel = move.PieceFromCaptured.HasValue + ? new Models.Move(move.PieceFromCaptured.Value, move.To, move.IsPromotion) + : new Models.Move(move.From!, move.To, move.IsPromotion); + var moveSuccess = session.Shogi.Move(moveModel); + + if (moveSuccess) + { + await communicationManager.BroadcastToPlayers(new MoveResponse + { + GameName = session.Name, + PlayerName = user.Name, + Move = moveModel.ToServiceModel() + }, session.Player1, session.Player2); + return Ok(); + } + throw new InvalidOperationException("Illegal move."); + } // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. //[Route("")] //public async Task PostSession([FromBody] PostSession request) @@ -69,7 +112,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers // var success = await repository.CreateSession(model); // if (success) // { - // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Socket.Types.ClientAction.CreateGame) + // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame) // { // Game = model.ToServiceModel(), // PlayerName = diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 739e423..e5ee6db 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,9 +1,11 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Api; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using System; using System.Linq; using System.Threading.Tasks; @@ -14,10 +16,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [ApiController] public class SocketController : ControllerBase { + public static readonly string WebSessionKey = "session-id"; private readonly ILogger logger; private readonly ISocketTokenManager tokenManager; private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; + private readonly CookieOptions createSessionOptions; + private readonly CookieOptions deleteSessionOptions; public SocketController( ILogger logger, @@ -29,6 +34,23 @@ namespace Gameboard.ShogiUI.Sockets.Controllers this.tokenManager = tokenManager; this.gameboardManager = gameboardManager; this.gameboardRepository = gameboardRepository; + createSessionOptions = new CookieOptions + { + Secure = true, + HttpOnly = true, + SameSite = SameSiteMode.None, + Expires = DateTimeOffset.Now.AddYears(5) + }; + deleteSessionOptions = new CookieOptions(); + } + + [HttpGet("Yep")] + [AllowAnonymous] + public IActionResult Yep() + { + deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1); + Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions); + return Ok(); } [HttpGet("Token")] @@ -39,25 +61,36 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return new JsonResult(new GetTokenResponse(token)); } + /// + /// Builds a token for guest users to send when requesting a socket connection. + /// Sends a HttpOnly cookie to the client with which to identify guest users. + /// + /// + /// [AllowAnonymous] [HttpGet("GuestToken")] - public async Task GetGuestToken([FromQuery] GetGuestToken request) + public async Task GetGuestToken() { - if (request.ClientId == null) + var cookies = Request.Cookies; + var webSessionId = cookies.ContainsKey(WebSessionKey) + ? Guid.Parse(cookies[WebSessionKey]!) + : Guid.NewGuid(); + var webSessionIdAsString = webSessionId.ToString(); + + var user = await gameboardRepository.ReadGuestUser(webSessionId); + if (user == null) { - var clientId = await gameboardManager.CreateGuestUser(); - var token = tokenManager.GenerateToken(clientId); - return new JsonResult(new GetGuestTokenResponse(clientId, token)); + var userName = await gameboardManager.CreateGuestUser(webSessionId); + var token = tokenManager.GenerateToken(webSessionIdAsString); + Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions); + return new JsonResult(new GetGuestTokenResponse(userName, token)); } else { - if (await gameboardRepository.IsGuestUser(request.ClientId)) - { - var token = tokenManager.GenerateToken(request.ClientId); - return new JsonResult(new GetGuestTokenResponse(request.ClientId, token)); - } + var token = tokenManager.GenerateToken(webSessionIdAsString); + Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions); + return new JsonResult(new GetGuestTokenResponse(user.Name, token)); } - return new UnauthorizedResult(); } } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index 36827e8..3820ae7 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System; using System.Text; using System.Text.RegularExpressions; @@ -26,12 +26,12 @@ namespace Gameboard.ShogiUI.Sockets.Extensions return name; } - public static void PrintStateAsAscii(this Models.Shogi self) + public static string PrintStateAsAscii(this Models.Shogi self) { var builder = new StringBuilder(); builder.Append(" Player 2(.)"); builder.AppendLine(); - for (var y = 0; y < 9; y++) + for (var y = 8; y >= 0; y--) { builder.Append("- "); for (var x = 0; x < 8; x++) builder.Append("- - "); @@ -40,7 +40,7 @@ namespace Gameboard.ShogiUI.Sockets.Extensions builder.Append('|'); for (var x = 0; x < 9; x++) { - var piece = self.Board[y, x]; + var piece = self.Board[x, y]; if (piece == null) { builder.Append(" "); @@ -58,7 +58,7 @@ namespace Gameboard.ShogiUI.Sockets.Extensions builder.Append("- -"); builder.AppendLine(); builder.Append(" Player 1"); - Console.WriteLine(builder.ToString()); + return builder.ToString(); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs deleted file mode 100644 index 5018096..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using System.Collections.Concurrent; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface IActiveSessionManager - { - void Add(Session session); - Session? Get(string sessionName); - } - - // TODO: Consider moving this class' functionality into the ConnectionManager class. - public class ActiveSessionManager : IActiveSessionManager - { - private readonly ConcurrentDictionary Sessions; - - public ActiveSessionManager() - { - Sessions = new ConcurrentDictionary(); - } - - public void Add(Session session) => Sessions.TryAdd(session.Name, session); - - public Session? Get(string sessionName) - { - if (Sessions.TryGetValue(sessionName, out var session)) - { - return session; - } - return null; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index 6703d3b..2041ef8 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,5 +1,5 @@ using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -31,14 +31,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (!success) { - var error = new CreateGameResponse(request.Action) + var error = new CreateGameResponse() { Error = "Unable to create game with this name." }; await connectionManager.BroadcastToPlayers(error, userName); } - var response = new CreateGameResponse(request.Action) + var response = new CreateGameResponse() { PlayerName = userName, Game = model.ToServiceModel() diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 8882e8f..2dcac05 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -1,5 +1,5 @@ using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 223782c..52415ee 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -1,5 +1,5 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -24,7 +24,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName); - var response = new JoinGameResponse(ClientAction.JoinGame) + var response = new JoinGameResponse() { PlayerName = userName, GameName = request.GameName diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 64787bd..b92c8ca 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,6 +1,6 @@ using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Linq; using System.Threading.Tasks; @@ -29,7 +29,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers var sessions = await repository.ReadSessionMetadatas(); var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList(); - var response = new ListGamesResponse(ClientAction.ListGames) + var response = new ListGamesResponse() { Games = games }; diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 93317a6..d82c880 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,7 +1,7 @@ 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 Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Microsoft.Extensions.Logging; using System.Linq; using System.Threading.Tasks; @@ -21,18 +21,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; private readonly ISocketConnectionManager communicationManager; - private readonly IActiveSessionManager boardManager; public LoadGameHandler( ILogger logger, ISocketConnectionManager communicationManager, - IGameboardRepository gameboardRepository, - IActiveSessionManager boardManager) + IGameboardRepository gameboardRepository) { this.logger = logger; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; - this.boardManager = boardManager; } public async Task Handle(LoadGameRequest request, string userName) @@ -41,15 +38,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); - var error = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; + var error = new LoadGameResponse() { Error = "Game not found." }; await communicationManager.BroadcastToPlayers(error, userName); return; } communicationManager.SubscribeToGame(sessionModel, userName); - boardManager.Add(sessionModel); - var response = new LoadGameResponse(ClientAction.LoadGame) + var response = new LoadGameResponse() { Game = new SessionMetadata(sessionModel).ToServiceModel(), BoardState = sessionModel.Shogi.ToServiceModel(), diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 4c9e9a8..706e125 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,9 +1,6 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using System.Threading.Tasks; - namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public interface IMoveHandler @@ -12,66 +9,53 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } public class MoveHandler : IMoveHandler { - private readonly IActiveSessionManager boardManager; private readonly IGameboardManager gameboardManager; - private readonly ISocketConnectionManager communicationManager; + private readonly ISocketConnectionManager connectionManager; public MoveHandler( - IActiveSessionManager boardManager, - ISocketConnectionManager communicationManager, + ISocketConnectionManager connectionManager, IGameboardManager gameboardManager) { - this.boardManager = boardManager; this.gameboardManager = gameboardManager; - this.communicationManager = communicationManager; + this.connectionManager = connectionManager; } public async Task Handle(MoveRequest request, string userName) { - Move moveModel; + Models.Move moveModel; if (request.Move.PieceFromCaptured.HasValue) { - moveModel = new Move(request.Move.PieceFromCaptured.Value, request.Move.To); + moveModel = new Models.Move(request.Move.PieceFromCaptured.Value, request.Move.To); } else { - moveModel = new Move(request.Move.From!, request.Move.To, request.Move.IsPromotion); + moveModel = new Models.Move(request.Move.From!, request.Move.To, request.Move.IsPromotion); } - var board = boardManager.Get(request.GameName); - if (board == null) + var session = await gameboardManager.ReadSession(request.GameName); + if (session != null) { - // TODO: Find a flow for this - var response = new MoveResponse(ServiceModels.Socket.Types.ClientAction.Move) + var shogi = session.Shogi; + var moveSuccess = shogi.Move(moveModel); + if (moveSuccess) { - Error = $"Game isn't loaded. Send a message with the {ServiceModels.Socket.Types.ClientAction.LoadGame} action first." - }; - await communicationManager.BroadcastToPlayers(response, userName); + await gameboardManager.CreateBoardState(session.Name, shogi); + var response = new MoveResponse() + { + GameName = request.GameName, + PlayerName = userName, + Move = moveModel.ToServiceModel() + }; + await connectionManager.BroadcastToPlayers(response, session.Player1, session.Player2); + } + else + { + var response = new MoveResponse() + { + Error = "Invalid move." + }; + await connectionManager.BroadcastToPlayers(response, userName); + } } - - - - - //var moveSuccess = board.Move(boardMove); - //if (moveSuccess) - //{ - // await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); - // var boardState = new BoardState(board); - // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - // { - // GameName = request.GameName, - // PlayerName = userName, - // BoardState = boardState.ToServiceModel() - // }; - // await communicationManager.BroadcastToGame(request.GameName, response); - //} - //else - //{ - // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - // { - // Error = "Invalid move." - // }; - // await communicationManager.BroadcastToPlayers(response, userName); - //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index bc2be4b..17e3de1 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -7,19 +7,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface IGameboardManager { - Task CreateGuestUser(); + Task CreateGuestUser(Guid webSessionId); Task IsPlayer1(string sessionName, string playerName); - bool IsGuest(string playerName); Task CreateSession(SessionMetadata session); Task ReadSession(string gameName); - Task UpdateSession(Session session); + Task UpdateSession(SessionMetadata session); Task AssignPlayer2ToSession(string sessionName, string userName); + Task CreateBoardState(string sessionName, Shogi shogi); + Task ReadUser(string userName); + Task ReadUser(Guid webSessionId); } public class GameboardManager : IGameboardManager { private const int MaxTries = 3; - private const string GuestPrefix = "Guest-"; private readonly IGameboardRepository repository; public GameboardManager(IGameboardRepository repository) @@ -27,22 +28,30 @@ namespace Gameboard.ShogiUI.Sockets.Managers this.repository = repository; } - public async Task CreateGuestUser() + public async Task CreateGuestUser(Guid webSessionId) { var count = 0; while (count < MaxTries) { count++; var clientId = $"Guest-{Guid.NewGuid()}"; - var isCreated = await repository.CreateGuestUser(clientId); + var isCreated = await repository.CreateGuestUser(clientId, webSessionId); if (isCreated) { return clientId; } } - throw new OperationCanceledException($"Failed to create guest user after {MaxTries} tries."); + throw new OperationCanceledException($"Failed to create guest user after {count} tries."); } + public Task ReadUser(Guid webSessionId) + { + return repository.ReadGuestUser(webSessionId); + } + public Task ReadUser(string userName) + { + return repository.ReadUser(userName); + } public async Task IsPlayer1(string sessionName, string playerName) { //var session = await repository.GetGame(sessionName); @@ -65,8 +74,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers return repository.CreateSession(session); } - public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); - public Task ReadSession(string sessionName) { return repository.ReadSession(sessionName); @@ -77,15 +84,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// /// The session to save. /// True if the session was saved successfully. - public Task UpdateSession(Session session) + public Task UpdateSession(SessionMetadata session) { return repository.UpdateSession(session); } + public Task CreateBoardState(string sessionName, Shogi shogi) + { + return repository.CreateBoardState(sessionName, shogi); + } + public async Task AssignPlayer2ToSession(string sessionName, string userName) { var isSuccess = false; - var session = await repository.ReadSession(sessionName); + var session = await repository.ReadSessionMetaData(sessionName); if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2)) { session.SetPlayer2(userName); diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 54798b0..720381d 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,6 +1,6 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Collections.Concurrent; @@ -19,7 +19,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromGame(string gameName, string playerName); - Task BroadcastToPlayers(IResponse response, params string[] playerNames); + Task BroadcastToPlayers(IResponse response, params string?[] playerNames); } /// @@ -84,17 +84,16 @@ namespace Gameboard.ShogiUI.Sockets.Managers } } - public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) + public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames) { var tasks = new List(playerNames.Length); foreach (var name in playerNames) { - if (connections.TryGetValue(name, out var socket)) + if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket)) { var serialized = JsonConvert.SerializeObject(response); logger.LogInformation("Response to {0} \n{1}\n", name, serialized); tasks.Add(socket.SendTextAsync(serialized)); - } } await Task.WhenAll(tasks); diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs index b2e2f73..d8c46ab 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs @@ -15,7 +15,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers public class SocketTokenManager : ISocketTokenManager { /// - /// Key is userName + /// Key is userName or webSessionId /// private readonly ConcurrentDictionary Tokens; diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs deleted file mode 100644 index 9d8903f..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.Managers.Utility -{ - public class Request : IRequest - { - public ClientAction Action { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 568594c..bbb32f7 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,18 +1,14 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Gameboard.ShogiUI.Sockets.Utilities; using System.Diagnostics; using System.Numerics; -using System.Text.RegularExpressions; namespace Gameboard.ShogiUI.Sockets.Models { [DebuggerDisplay("{From} - {To}")] public class Move { - private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private static readonly char A = 'A'; - - public Vector2? From { get; } + public Vector2? From { get; } // TODO: Use string notation public bool IsPromotion { get; } public WhichPiece? PieceFromHand { get; } public Vector2 To { get; } @@ -37,8 +33,8 @@ namespace Gameboard.ShogiUI.Sockets.Models /// If the moving piece should be promoted. public Move(string fromNotation, string toNotation, bool isPromotion = false) { - From = FromBoardNotation(fromNotation); - To = FromBoardNotation(toNotation); + From = NotationHelper.FromBoardNotation(fromNotation); + To = NotationHelper.FromBoardNotation(toNotation); IsPromotion = isPromotion; } @@ -52,35 +48,16 @@ namespace Gameboard.ShogiUI.Sockets.Models { From = null; PieceFromHand = pieceFromHand; - To = FromBoardNotation(toNotation); + To = NotationHelper.FromBoardNotation(toNotation); IsPromotion = isPromotion; } - public ServiceModels.Socket.Types.Move ToServiceModel() => new() + public ServiceModels.Types.Move ToServiceModel() => new() { - From = From.HasValue ? ToBoardNotation(From.Value) : null, + From = From.HasValue ? NotationHelper.ToBoardNotation(From.Value) : null, IsPromotion = IsPromotion, PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null, - To = ToBoardNotation(To) + To = NotationHelper.ToBoardNotation(To) }; - - private static string ToBoardNotation(Vector2 vector) - { - var file = (char)(vector.X + A); - var rank = vector.Y + 1; - return $"{file}{rank}"; - } - - private static Vector2 FromBoardNotation(string notation) - { - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Vector2(file - A, rank); - } - throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); - } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index f86d120..3c830e6 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using PathFinding; using System.Diagnostics; @@ -18,6 +18,9 @@ namespace Gameboard.ShogiUI.Sockets.Models Owner = owner; IsPromoted = isPromoted; } + public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted) + { + } public bool CanPromote => !IsPromoted && WhichPiece != WhichPiece.King @@ -54,9 +57,9 @@ namespace Gameboard.ShogiUI.Sockets.Models _ => throw new System.NotImplementedException() }; - public ServiceModels.Socket.Types.Piece ToServiceModel() + public ServiceModels.Types.Piece ToServiceModel() { - return new ServiceModels.Socket.Types.Piece + return new ServiceModels.Types.Piece { IsPromoted = IsPromoted, Owner = Owner, diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index 80f74fe..f3cfade 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -7,7 +7,7 @@ { public string Name { get; } public string Player1 { get; } - public string? Player2 { get; } + public string? Player2 { get; private set; } public bool IsPrivate { get; } public SessionMetadata(string name, bool isPrivate, string player1, string? player2) @@ -25,6 +25,11 @@ Player2 = sessionModel.Player2; } - public ServiceModels.Socket.Types.Game ToServiceModel() => new(Name, Player1, Player2); + public void SetPlayer2(string playerName) + { + Player2 = playerName; + } + + public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1, Player2); } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs index 678aa92..121b0d6 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs @@ -1,4 +1,5 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Gameboard.ShogiUI.Sockets.Utilities; using PathFinding; using System; using System.Collections.Generic; @@ -19,8 +20,10 @@ namespace Gameboard.ShogiUI.Sockets.Models private Shogi? validationBoard; private Vector2 player1King; private Vector2 player2King; - public IReadOnlyDictionary> Hands { get; } - public PlanarCollection Board { get; } //TODO: Hide this being a getter method + private List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public List Player1Hand { get; } + public List Player2Hand { get; } + public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } @@ -30,15 +33,13 @@ namespace Gameboard.ShogiUI.Sockets.Models public Shogi() { - Board = new PlanarCollection(9, 9); + Board = new CoordsToNotationCollection(); MoveHistory = new List(20); - Hands = new Dictionary> { - { WhichPlayer.Player1, new List()}, - { WhichPlayer.Player2, new List()}, - }; - pathFinder = new PathFinder2D(Board); - player1King = new Vector2(4, 8); - player2King = new Vector2(4, 0); + Player1Hand = new List(); + Player2Hand = new List(); + pathFinder = new PathFinder2D(Board, 9, 9); + player1King = new Vector2(4, 0); + player2King = new Vector2(4, 8); Error = string.Empty; InitializeBoardState(); @@ -58,24 +59,16 @@ namespace Gameboard.ShogiUI.Sockets.Models private Shogi(Shogi toCopy) { - Board = new PlanarCollection(9, 9); - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = toCopy.Board[y, x]; - if (piece != null) - { - Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted); - } - } - - pathFinder = new PathFinder2D(Board); - MoveHistory = new List(toCopy.MoveHistory); - Hands = new Dictionary> + Board = new CoordsToNotationCollection(); + foreach (var kvp in toCopy.Board) { - { WhichPlayer.Player1, new List(toCopy.Hands[WhichPlayer.Player1]) }, - { WhichPlayer.Player2, new List(toCopy.Hands[WhichPlayer.Player2]) } - }; + Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); + } + + pathFinder = new PathFinder2D(Board, 9, 9); + MoveHistory = new List(toCopy.MoveHistory); + Player1Hand = new List(toCopy.Player1Hand); + Player2Hand = new List(toCopy.Player2Hand); player1King = toCopy.player1King; player2King = toCopy.player2King; Error = toCopy.Error; @@ -115,6 +108,8 @@ namespace Gameboard.ShogiUI.Sockets.Models : validationBoard.PlaceFromBoard(move); if (!isValid) { + // Surface the error description. + Error = validationBoard.Error; // Invalidate the "throw away" board. validationBoard = null; return false; @@ -137,37 +132,55 @@ namespace Gameboard.ShogiUI.Sockets.Models /// True if the move was successful. private bool PlaceFromHand(Move move) { - if (move.PieceFromHand.HasValue == false) return false; //Invalid move - var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); - if (index < 0) return false; // Invalid move - if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand. + var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); + if (index < 0) + { + Error = $"{move.PieceFromHand} does not exist in the hand."; + return false; + } + if (Board[move.To] != null) + { + Error = $"Illegal move - attempting to capture while playing a piece from the hand."; + return false; + } - var minimumY = 0; - switch (move.PieceFromHand.Value) + switch (move.PieceFromHand!.Value) { case WhichPiece.Knight: - // Knight cannot be placed onto the farthest two ranks from the hand. - minimumY = WhoseTurn == WhichPlayer.Player1 ? 6 : 2; - break; + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6) + || (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2)) + { + Error = $"Knight has no valid moves after placed."; + return false; + } + break; + } case WhichPiece.Lance: case WhichPiece.Pawn: - // Lance and Pawn cannot be placed onto the farthest rank from the hand. - minimumY = WhoseTurn == WhichPlayer.Player1 ? 7 : 1; - break; + { + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8) + || (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0)) + { + Error = $"{move.PieceFromHand} has no valid moves after placed."; + return false; + } + break; + } } - if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; - if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; // Mutate the board. - Board[move.To.Y, move.To.X] = Hands[WhoseTurn][index]; - Hands[WhoseTurn].RemoveAt(index); + Board[move.To] = Hand[index]; + Hand.RemoveAt(index); return true; } /// True if the move was successful. private bool PlaceFromBoard(Move move) { - var fromPiece = Board[move.From.Value.Y, move.From.Value.X]; + var fromPiece = Board[move.From!.Value]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; @@ -184,28 +197,28 @@ namespace Gameboard.ShogiUI.Sockets.Models return false; // Invalid move; move not part of move-set. } - var captured = Board[move.To.Y, move.To.X]; + var captured = Board[move.To]; if (captured != null) { if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. captured.Capture(); - Hands[captured.Owner].Add(captured); + Hand.Add(captured); } //Mutate the board. if (move.IsPromotion) { - if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Value.Y < 3)) + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5)) { fromPiece.Promote(); } - else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5)) + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3)) { fromPiece.Promote(); } } - Board[move.To.Y, move.To.X] = fromPiece; - Board[move.From.Value.Y, move.From.Value.X] = null; + Board[move.To] = fromPiece; + Board[move.From!.Value] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) @@ -225,7 +238,7 @@ namespace Gameboard.ShogiUI.Sockets.Models private bool IsPathable(Vector2 from, Vector2 to) { - var piece = Board[from.Y, from.X]; + var piece = Board[from]; if (piece == null) return false; var isObstructed = false; @@ -239,56 +252,66 @@ namespace Gameboard.ShogiUI.Sockets.Models #region Rules Validation private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) { + if (whichPlayer == InCheck) return true; // If we already know the player is in check, don't bother. + var isCheck = false; var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; // Check if the move put the king in check. if (pathFinder.PathTo(move.To, kingPosition)) return true; - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From.Value); - var slope = Math.Abs(direction.Y / direction.X); - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) + if (move.From.HasValue) { - // if slope of the move is also infinity...can skip this? - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, move.From!.Value); + var slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) { - if (piece.Owner != whichPlayer) + // if slope of the move is also infinity...can skip this? + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { - switch (piece.WhichPiece) + if (piece.Owner != whichPlayer) { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; + switch (piece.WhichPiece) + { + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; + } } - } - }); - } - else if (slope == 1) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + }); + } + else if (slope == 1) { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { - isCheck = true; - } - }); - } - else if (slope == 0) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { - isCheck = true; - } - }); + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); + } + } + else + { + // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. + // Go read the shogi rules to be sure this is true. } return isCheck; @@ -299,12 +322,11 @@ namespace Gameboard.ShogiUI.Sockets.Models // Assume true and try to disprove. var isCheckmate = true; - Board.ForEachNotNull((piece, x, y) => // For each piece... + Board.ForEachNotNull((piece, from) => // For each piece... { // Short circuit if (!isCheckmate) return; - var from = new Vector2(x, y); if (piece.Owner == InCheck) // ...owned by the player in check... { // ...evaluate if any move gets the player out of check. @@ -328,82 +350,108 @@ namespace Gameboard.ShogiUI.Sockets.Models } #endregion - #region Initialize - private void ResetEmptyRows() - { - for (int y = 3; y < 6; y++) - for (int x = 0; x < 9; x++) - Board[y, x] = null; - } - private void ResetFrontRow(WhichPlayer player) - { - int y = player == WhichPlayer.Player1 ? 6 : 2; - for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player); - } - private void ResetMiddleRow(WhichPlayer player) - { - int y = player == WhichPlayer.Player1 ? 7 : 1; - - Board[y, 0] = null; - for (int x = 2; x < 7; x++) Board[y, x] = null; - Board[y, 8] = null; - if (player == WhichPlayer.Player1) - { - Board[y, 1] = new Piece(WhichPiece.Bishop, player); - Board[y, 7] = new Piece(WhichPiece.Rook, player); - } - else - { - Board[y, 1] = new Piece(WhichPiece.Rook, player); - Board[y, 7] = new Piece(WhichPiece.Bishop, player); - } - } - private void ResetRearRow(WhichPlayer player) - { - int y = player == WhichPlayer.Player1 ? 8 : 0; - - Board[y, 0] = new Piece(WhichPiece.Lance, player); - Board[y, 1] = new Piece(WhichPiece.Knight, player); - Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player); - Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player); - Board[y, 4] = new Piece(WhichPiece.King, player); - Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player); - Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player); - Board[y, 7] = new Piece(WhichPiece.Knight, player); - Board[y, 8] = new Piece(WhichPiece.Lance, player); - } private void InitializeBoardState() { - ResetRearRow(WhichPlayer.Player2); - ResetMiddleRow(WhichPlayer.Player2); - ResetFrontRow(WhichPlayer.Player2); - ResetEmptyRows(); - ResetFrontRow(WhichPlayer.Player1); - ResetMiddleRow(WhichPlayer.Player1); - ResetRearRow(WhichPlayer.Player1); + Board["A1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + Board["B1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); + Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); + Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); + Board["E1"] = new Piece(WhichPiece.King, WhichPlayer.Player1); + Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); + Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); + Board["H1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); + Board["I1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + + Board["A2"] = null; + Board["B2"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player1); + Board["C2"] = null; + Board["D2"] = null; + Board["E2"] = null; + Board["F2"] = null; + Board["G2"] = null; + Board["H2"] = new Piece(WhichPiece.Rook, WhichPlayer.Player1); + Board["I2"] = null; + + Board["A3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["B3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["C3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["D3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["E3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["F3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["G3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["H3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["I3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + + Board["A4"] = null; + Board["B4"] = null; + Board["C4"] = null; + Board["D4"] = null; + Board["E4"] = null; + Board["F4"] = null; + Board["G4"] = null; + Board["H4"] = null; + Board["I4"] = null; + + Board["A5"] = null; + Board["B5"] = null; + Board["C5"] = null; + Board["D5"] = null; + Board["E5"] = null; + Board["F5"] = null; + Board["G5"] = null; + Board["H5"] = null; + Board["I5"] = null; + + Board["A6"] = null; + Board["B6"] = null; + Board["C6"] = null; + Board["D6"] = null; + Board["E6"] = null; + Board["F6"] = null; + Board["G6"] = null; + Board["H6"] = null; + Board["I6"] = null; + + Board["A7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["B7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["C7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["D7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["E7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["F7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["G7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["H7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["I7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + + Board["A8"] = null; + Board["B8"] = new Piece(WhichPiece.Rook, WhichPlayer.Player2); + Board["C8"] = null; + Board["D8"] = null; + Board["E8"] = null; + Board["F8"] = null; + Board["G8"] = null; + Board["H8"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player2); + Board["I8"] = null; + + Board["A9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); + Board["B9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); + Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); + Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); + Board["E9"] = new Piece(WhichPiece.King, WhichPlayer.Player2); + Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); + Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); + Board["H9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); + Board["I9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); } - #endregion public BoardState ToServiceModel() { - var board = new ServiceModels.Socket.Types.Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = Board[y, x]; - if (piece != null) - { - board[y, x] = piece.ToServiceModel(); - } - } - return new BoardState { - Board = board, + Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()), PlayerInCheck = InCheck, WhoseTurn = WhoseTurn, - Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(), - Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList() + Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), + Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() }; } } diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs new file mode 100644 index 0000000..ba6959d --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -0,0 +1,18 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public class User + { + public static readonly string GuestPrefix = "Guest-"; + public string Name { get; } + public Guid? WebSessionId { get; } + public bool IsGuest => Name.StartsWith(GuestPrefix) && WebSessionId.HasValue; + + public User(string name, Guid? webSessionId = null) + { + Name = name; + WebSessionId = webSessionId; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs index 3c8d9e7..08de6a5 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -1,6 +1,9 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Gameboard.ShogiUI.Sockets.Utilities; using System; +using System.Collections.Generic; using System.Linq; +using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { @@ -8,7 +11,10 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public string Name { get; set; } - public Piece?[,] Board { get; set; } + /// + /// A dictionary where the key is a board-notation position, like D3. + /// + public Dictionary Board { get; set; } public Piece[] Player1Hand { get; set; } @@ -25,7 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public BoardStateDocument() : base(WhichDocumentType.BoardState) { Name = string.Empty; - Board = new Piece[9, 9]; + Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); Player1Hand = Array.Empty(); Player2Hand = Array.Empty(); } @@ -34,20 +40,23 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) { Name = sessionName; - Board = new Piece[9, 9]; + Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) { + var position = new Vector2(x, y); var piece = shogi.Board[y, x]; + if (piece != null) { - Board[y, x] = new Piece(piece); + var positionNotation = NotationHelper.ToBoardNotation(position); + Board[positionNotation] = new Piece(piece); } } - Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(model => new Piece(model)).ToArray(); - Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(model => new Piece(model)).ToArray(); + Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray(); + Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray(); if (shogi.MoveHistory.Count > 0) { Move = new Move(shogi.MoveHistory[^1]); diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs index fd02af6..daab934 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs @@ -5,10 +5,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels internal class CouchFindResult { public T[] docs; + public string warning; public CouchFindResult() { docs = Array.Empty(); + warning = ""; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs index 5dbbf03..8b29998 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs index 1df6a20..3f28f87 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index 7fcb388..843094e 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,9 +1,9 @@ -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public class UserDocument : CouchDocument { - public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; - public enum LoginPlatform { Microsoft, @@ -12,10 +12,27 @@ public string Name { get; set; } public LoginPlatform Platform { get; set; } - public UserDocument(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", WhichDocumentType.User) + /// + /// The browser session ID saved via Set-Cookie headers. + /// Only used with guest accounts. + /// + public Guid? WebSessionId { get; set; } + + /// + /// Constructor for JSON deserializing. + /// + public UserDocument() : base(WhichDocumentType.User) + { + Name = string.Empty; + } + + public UserDocument(string name, Guid? webSessionId = null) : base($"org.couchdb.user:{name}", WhichDocumentType.User) { Name = name; - Platform = platform; + WebSessionId = webSessionId; + Platform = WebSessionId.HasValue + ? LoginPlatform.Guest + : LoginPlatform.Microsoft; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index a7d9617..c519e4f 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -12,14 +12,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task CreateGuestUser(string userName); + Task CreateBoardState(string sessionName, Models.Shogi shogi); Task CreateSession(Models.SessionMetadata session); + Task CreateUser(Models.User user); Task> ReadSessionMetadatas(); - Task IsGuestUser(string userName); - Task PostJoinCode(string gameName, string userName); + Task ReadGuestUser(Guid webSessionId); Task ReadSession(string name); Task ReadShogi(string name); - Task UpdateSession(Models.Session session); + Task UpdateSession(Models.SessionMetadata session); + Task ReadSessionMetaData(string name); + Task ReadUser(string userName); } public class GameboardRepository : IGameboardRepository @@ -65,6 +67,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return couchModel.ToDomainModel(shogi); } + public async Task ReadSessionMetaData(string name) + { + var response = await client.GetAsync(name); + var responseContent = await response.Content.ReadAsStringAsync(); + var couchModel = JsonConvert.DeserializeObject(responseContent); + return couchModel.ToDomainModel(); + } + public async Task ReadShogi(string name) { var selector = new Dictionary(2) @@ -74,7 +84,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories }; var sort = new Dictionary(1) { - [nameof(BoardStateDocument.CreatedDate)] = "desc" + [nameof(BoardStateDocument.CreatedDate)] = "asc" }; var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } }); @@ -103,6 +113,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return new Models.Shogi(moves); } + public async Task CreateBoardState(string sessionName, Models.Shogi shogi) + { + var boardStateDocument = new BoardStateDocument(sessionName, shogi); + var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; + } + public async Task CreateSession(Models.SessionMetadata session) { var sessionDocument = new SessionDocument(session); @@ -120,7 +138,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return false; } - public async Task UpdateSession(Models.Session session) + public async Task UpdateSession(Models.SessionMetadata session) { var couchModel = new SessionDocument(session); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); @@ -178,32 +196,53 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return string.Empty; } - //public async Task GetPlayer(string playerName) - //{ - // var uri = $"Player/{playerName}"; - // var get = await client.GetAsync(Uri.EscapeUriString(uri)); - // var content = await get.Content.ReadAsStringAsync(); - // if (!string.IsNullOrWhiteSpace(content)) - // { - // var response = JsonConvert.DeserializeObject(content); - // return new Player(response.Player.Name); - // } - // return null; - //} - - public async Task CreateGuestUser(string userName) + public async Task ReadUser(string userName) { - var couchModel = new UserDocument(userName, UserDocument.LoginPlatform.Guest); + var document = new UserDocument(userName); + var response = await client.GetAsync(document.Id); + var responseContent = await response.Content.ReadAsStringAsync(); + if (response.IsSuccessStatusCode) + { + var user = JsonConvert.DeserializeObject(responseContent); + + return new Models.User(user.Name); + } + return null; + } + + public async Task CreateUser(Models.User user) + { + var couchModel = new UserDocument(user.Name, user.WebSessionId); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); return response.IsSuccessStatusCode; } - public async Task IsGuestUser(string userName) + public async Task ReadGuestUser(Guid webSessionId) { - var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{UserDocument.GetDocumentId(userName)}")); - var response = await client.SendAsync(req); - return response.IsSuccessStatusCode; + var selector = new Dictionary(2) + { + [nameof(UserDocument.DocumentType)] = WhichDocumentType.User, + [nameof(UserDocument.WebSessionId)] = webSessionId.ToString() + }; + var query = JsonConvert.SerializeObject(new { selector, limit = 1 }); + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync("_find", content); + var responseContent = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadGuestUser), responseContent, query); + return null; + } + + var result = JsonConvert.DeserializeObject>(responseContent); + if (!string.IsNullOrWhiteSpace(result.warning)) + { + logger.LogError(new InvalidOperationException(result.warning), result.warning); + return null; + } + + return new Models.User(result.docs.Single().Name); } } } diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs index 0535a8e..033b218 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs index e3837a0..e1de3a2 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs index fa0e2d5..da598e3 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs index c2ddc8e..38ecc66 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs index 5a4ad8a..3fb477d 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs index 2eb06f1..29a0cb9 100644 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators { diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs index f8be6cf..925c018 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -1,11 +1,12 @@ using FluentValidation; +using Gameboard.ShogiUI.Sockets.Controllers; using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Gameboard.ShogiUI.Sockets.Services.Utility; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -29,6 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Services { private readonly ILogger logger; private readonly ISocketConnectionManager communicationManager; + private readonly IGameboardRepository gameboardRepository; private readonly ISocketTokenManager tokenManager; private readonly ICreateGameHandler createGameHandler; private readonly IJoinByCodeHandler joinByCodeHandler; @@ -46,6 +48,7 @@ namespace Gameboard.ShogiUI.Sockets.Services public SocketService( ILogger logger, ISocketConnectionManager communicationManager, + IGameboardRepository gameboardRepository, ISocketTokenManager tokenManager, ICreateGameHandler createGameHandler, IJoinByCodeHandler joinByCodeHandler, @@ -63,6 +66,7 @@ namespace Gameboard.ShogiUI.Sockets.Services { this.logger = logger; this.communicationManager = communicationManager; + this.gameboardRepository = gameboardRepository; this.tokenManager = tokenManager; this.createGameHandler = createGameHandler; this.joinByCodeHandler = joinByCodeHandler; @@ -80,105 +84,115 @@ namespace Gameboard.ShogiUI.Sockets.Services public async Task HandleSocketRequest(HttpContext context) { - var hasToken = context.Request.Query.Keys.Contains("token"); - if (hasToken) + string? userName = null; + if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey)) { - var oneTimeToken = context.Request.Query["token"][0]; - var tokenAsGuid = Guid.Parse(oneTimeToken); - var userName = tokenManager.GetUsername(tokenAsGuid); - if (userName != null) - { - var socket = await context.WebSockets.AcceptWebSocketAsync(); + // Guest account + var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!); + userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name; + } + else if (context.Request.Query.Keys.Contains("token")) + { + // Microsoft account + var token = Guid.Parse(context.Request.Query["token"][0]); + userName = tokenManager.GetUsername(token); + } - communicationManager.SubscribeToBroadcast(socket, userName); - while (socket.State == WebSocketState.Open) + if (string.IsNullOrEmpty(userName)) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + else + { + var socket = await context.WebSockets.AcceptWebSocketAsync(); + + communicationManager.SubscribeToBroadcast(socket, userName); + while (socket.State == WebSocketState.Open) + { + try { - try + 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)) { - 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)) - { - await socket.SendTextAsync("Error: Action not recognized."); - continue; - } - switch (request.Action) - { - case ClientAction.ListGames: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) - { - await listGamesHandler.Handle(req, userName); - } - break; - } - case ClientAction.CreateGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) - { - await createGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.JoinGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) - { - await joinGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) - { - await joinByCodeHandler.Handle(req, userName); - } - break; - } - case ClientAction.LoadGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) - { - await loadGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.Move: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) - { - await moveHandler.Handle(req, userName); - } - break; - } - } + await socket.SendTextAsync("Error: Action not recognized."); + continue; } - catch (OperationCanceledException ex) + switch (request.Action) { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); + case ClientAction.ListGames: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) + { + await listGamesHandler.Handle(req, userName); + } + break; + } + case ClientAction.CreateGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) + { + await createGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) + { + await joinGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) + { + await joinByCodeHandler.Handle(req, userName); + } + break; + } + case ClientAction.LoadGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) + { + await loadGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.Move: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) + { + await moveHandler.Handle(req, userName); + } + break; + } } } - communicationManager.UnsubscribeFromBroadcastAndGames(userName); - return; + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } } + communicationManager.UnsubscribeFromBroadcastAndGames(userName); + return; } - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - return; } public async Task ValidateRequestAndReplyIfInvalid(WebSocket socket, IValidator validator, TRequest request) @@ -186,7 +200,12 @@ namespace Gameboard.ShogiUI.Sockets.Services var results = validator.Validate(request); if (!results.IsValid) { - await socket.SendTextAsync(string.Join('\n', results.Errors.Select(_ => _.ErrorMessage).ToString())); + var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage)); + var message = JsonConvert.SerializeObject(new Response + { + Error = errors + }); + await socket.SendTextAsync(message); } return results.IsValid; } diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs similarity index 66% rename from Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs rename to Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs index 9ac96f6..c4fde65 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs +++ b/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs @@ -1,6 +1,6 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -namespace Gameboard.ShogiUI.Sockets.Managers.Utility +namespace Gameboard.ShogiUI.Sockets.Services.Utility { public class JsonRequest { diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs new file mode 100644 index 0000000..1928408 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs @@ -0,0 +1,10 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.Utility +{ + public class Request : IRequest + { + public ClientAction Action { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs new file mode 100644 index 0000000..f33af9f --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs @@ -0,0 +1,10 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; + +namespace Gameboard.ShogiUI.Sockets.Services.Utility +{ + public class Response : IResponse + { + public string Action { get; set; } + public string Error { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index a8e52ac..32cd211 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -3,7 +3,7 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Gameboard.ShogiUI.Sockets.Services; using Gameboard.ShogiUI.Sockets.Services.RequestValidators; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -18,6 +18,7 @@ using Newtonsoft.Json.Serialization; using System; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets { @@ -45,7 +46,6 @@ namespace Gameboard.ShogiUI.Sockets services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); // Services services.AddSingleton, CreateGameRequestValidator>(); @@ -84,6 +84,18 @@ namespace Gameboard.ShogiUI.Sockets options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122"; options.TokenValidationParameters.ValidateIssuer = true; options.TokenValidationParameters.ValidateAudience = true; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = (context) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + Console.WriteLine("Yep"); + } + return Task.FromResult(0); + } + }; }); } @@ -114,6 +126,7 @@ namespace Gameboard.ShogiUI.Sockets .WithOrigins(origins) .AllowAnyMethod() .AllowAnyHeader() + .WithExposedHeaders("Set-Cookie") .AllowCredentials() ) .UseRouting() @@ -126,12 +139,7 @@ namespace Gameboard.ShogiUI.Sockets }) .Use(async (context, next) => { - var isUpgradeHeader = context - .Request - .Headers - .Any(h => h.Key.Contains("upgrade", StringComparison.InvariantCultureIgnoreCase) - && h.Value.ToString().Contains("websocket", StringComparison.InvariantCultureIgnoreCase)); - if (isUpgradeHeader) + if (context.WebSockets.IsWebSocketRequest) { await socketConnectionManager.HandleSocketRequest(context); } diff --git a/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs b/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs new file mode 100644 index 0000000..c302164 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs @@ -0,0 +1,48 @@ +using Gameboard.ShogiUI.Sockets.Models; +using PathFinding; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Gameboard.ShogiUI.Sockets.Utilities +{ + public class CoordsToNotationCollection : Dictionary, IPlanarCollection + { + public delegate void ForEachDelegate(Piece element, Vector2 position); + + public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase) + { + } + + public CoordsToNotationCollection(Dictionary board) : base(board, StringComparer.OrdinalIgnoreCase) + { + } + + public Piece? this[Vector2 vector] + { + get => this[NotationHelper.ToBoardNotation(vector)]; + set => this[NotationHelper.ToBoardNotation(vector)] = value; + } + + public Piece? this[int x, int y] + { + get => this[NotationHelper.ToBoardNotation(x, y)]; + set => this[NotationHelper.ToBoardNotation(x, y)] = value; + } + + + public void ForEachNotNull(ForEachDelegate callback) + { + for (var x = 0; x < 9; x++) + { + for (var y = 0; y < 9; y++) + { + var position = new Vector2(x, y); + var elem = this[position]; + if (elem != null) + callback(elem, position); + } + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs new file mode 100644 index 0000000..8294dd6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace Gameboard.ShogiUI.Sockets.Utilities +{ + public static class NotationHelper + { + private static readonly string BoardNotationRegex = @"(?[a-iA-I])(?[1-9])"; + private static readonly char A = 'A'; + + public static string ToBoardNotation(Vector2 vector) + { + return ToBoardNotation((int)vector.X, (int)vector.Y); + } + public static string ToBoardNotation(int x, int y) + { + var file = (char)(x + A); + var rank = y + 1; + Console.WriteLine($"({x},{y}) - {file}{rank}"); + return $"{file}{rank}"; + } + + public static Vector2 FromBoardNotation(string notation) + { + notation = notation.ToUpper(); + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank - 1); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs index 1ef33de..2ac90d6 100644 --- a/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs @@ -1,44 +1,22 @@ using AutoFixture; using FluentAssertions; +using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; -using PathFinding; using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.UnitTests.PathFinding { [TestClass] public class PlanarCollectionShould { - private class SimpleElement : IPlanarElement - { - public static int Seed { get; private set; } - public MoveSet MoveSet => null; - public bool IsUpsideDown => false; - - - public SimpleElement() - { - Seed = Seed++; - } - } - - private Fixture fixture; - - [TestInitialize] - public void TestInitialize() - { - fixture = new Fixture(); - } - [TestMethod] public void Index() { // Arrange - var collection = new PlanarCollection(10, 10); - var expected1 = new SimpleElement(); - var expected2 = new SimpleElement(); + var collection = new TestPlanarCollection(); + var expected1 = new SimpleElement(1); + var expected2 = new SimpleElement(2); // Act collection[0, 0] = expected1; @@ -53,40 +31,27 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding public void Iterate() { // Arrange - var expected = new List(); - for (var i = 0; i < 9; i++) expected.Add(new SimpleElement()); - var collection = new PlanarCollection(3, 3); - for (var x = 0; x < 3; x++) - for (var y = 0; y < 3; y++) - collection[x, y] = expected[x + y]; + var planarCollection = new TestPlanarCollection(); + planarCollection[0, 0] = new SimpleElement(1); + planarCollection[0, 1] = new SimpleElement(2); + planarCollection[0, 2] = new SimpleElement(3); + planarCollection[1, 0] = new SimpleElement(4); + planarCollection[1, 1] = new SimpleElement(5); // Act var actual = new List(); - foreach (var elem in collection) + foreach (var elem in planarCollection) actual.Add(elem); // Assert - actual.Should().BeEquivalentTo(expected); - } - - [TestMethod] - public void Yep() - { - var collection = new PlanarCollection(3, 3); - collection[0, 0] = new SimpleElement(); - collection[1, 0] = new SimpleElement(); - collection[0, 1] = new SimpleElement(); - - // Act - var array2d = new SimpleElement[3, 3]; - for (var x = 0; x < 3; x++) - for (var y = 0; y < 3; y++) - { - array2d[x, y] = collection[x, y]; - } - - - Console.WriteLine("hey"); + using (new AssertionScope()) + { + actual[0].Number.Should().Be(1); + actual[1].Number.Should().Be(2); + actual[2].Number.Should().Be(3); + actual[3].Number.Should().Be(4); + actual[4].Number.Should().Be(5); + } } } } diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/TestPlanarCollection.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/TestPlanarCollection.cs new file mode 100644 index 0000000..4cb3a74 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/TestPlanarCollection.cs @@ -0,0 +1,48 @@ +using PathFinding; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; + +namespace Gameboard.ShogiUI.UnitTests.PathFinding +{ + public class SimpleElement : IPlanarElement + { + public int Number { get; } + public MoveSet MoveSet => null; + public bool IsUpsideDown => false; + + public SimpleElement(int number) + { + Number = number; + } + } + + public class TestPlanarCollection : IPlanarCollection + { + private readonly SimpleElement[,] array; + public TestPlanarCollection() + { + array = new SimpleElement[3, 3]; + } + public SimpleElement this[int x, int y] + { + get => array[x, y]; + set => array[x, y] = value; + } + public SimpleElement this[Vector2 vector] + { + get => this[(int)vector.X, (int)vector.Y]; + set => this[(int)vector.X, (int)vector.Y] = value; + } + + public IEnumerator GetEnumerator() + { + foreach (var e in array) + yield return e; + } + //IEnumerator IEnumerable.GetEnumerator() + //{ + // return array.GetEnumerator(); + //} + } +} diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index e5151c1..59f8695 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -3,464 +3,13 @@ using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; using System.Numerics; -using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPlayer; -using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPiece; +using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer; +using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; namespace Gameboard.ShogiUI.UnitTests.Rules { [TestClass] public class ShogiBoardShould { - [TestMethod] - public void InitializeBoardState() - { - // Assert - var board = new Shogi().Board; - // Assert pieces do not start promoted. - foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); - - // Assert Player1. - for (var y = 0; y < 3; y++) - for (var x = 0; x < 9; x++) - board[y, x]?.Owner.Should().Be(WhichPlayer.Player2); - board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); - board[0, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[0, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[0, 4].WhichPiece.Should().Be(WhichPiece.King); - board[0, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[0, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[0, 7].WhichPiece.Should().Be(WhichPiece.Knight); - board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[1, 0].Should().BeNull(); - board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook); - for (var x = 2; x < 7; x++) board[1, x].Should().BeNull(); - board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); - board[1, 8].Should().BeNull(); - for (var x = 0; x < 9; x++) board[2, x].WhichPiece.Should().Be(WhichPiece.Pawn); - - // Assert empty locations. - for (var y = 3; y < 6; y++) - for (var x = 0; x < 9; x++) - board[y, x].Should().BeNull(); - - // Assert Player2. - for (var y = 6; y < 9; y++) - for (var x = 0; x < 9; x++) - board[y, x]?.Owner.Should().Be(WhichPlayer.Player1); - board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[8, 1].WhichPiece.Should().Be(WhichPiece.Knight); - board[8, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[8, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[8, 4].WhichPiece.Should().Be(WhichPiece.King); - board[8, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[8, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[8, 7].WhichPiece.Should().Be(WhichPiece.Knight); - board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[7, 0].Should().BeNull(); - board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); - for (var x = 2; x < 7; x++) board[7, x].Should().BeNull(); - board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook); - board[7, 8].Should().BeNull(); - for (var x = 0; x < 9; x++) board[6, x].WhichPiece.Should().Be(WhichPiece.Pawn); - } - - [TestMethod] - public void InitializeBoardStateWithMoves() - { - var moves = new[] - { - // Pawn - new Move(new Vector2(0, 6), new Vector2(0, 5)) - }; - var shogi = new Shogi(moves); - shogi.Board[6, 0].Should().BeNull(); - shogi.Board[5, 0].WhichPiece.Should().Be(WhichPiece.Pawn); - } - - [TestMethod] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = new Shogi(); - // Prerequisit - shogi.Board[4, 4].Should().BeNull(); - - // Act - var moveSuccess = shogi.Move(new Move(new Vector2(4, 4), new Vector2(4, 5))); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board[4, 4].Should().BeNull(); - shogi.Board[5, 4].Should().BeNull(); - } - - [TestMethod] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var shogi = new Shogi(); - - // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move(new Vector2(0, 6), new Vector2(0, 6))); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board[6, 0].WhichPiece.Should().Be(WhichPiece.Pawn); - } - - [TestMethod] - public void PreventInvalidMoves_MoveSet() - { - // Bishop moving lateral - var invalidLanceMove = new Move(new Vector2(1, 1), new Vector2(2, 1)); - - var shogi = new Shogi(); - var moveSuccess = shogi.Move(invalidLanceMove); - - moveSuccess.Should().BeFalse(); - // Assert the Lance has not actually moved. - shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); - } - - [TestMethod] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var shogi = new Shogi(); - shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); - shogi.Board[2, 8].Owner.Should().Be(WhichPlayer.Player2); - - // Act - Move Player2 Pawn when it's Player1 turn. - var moveSuccess = shogi.Move(new Move(new Vector2(8, 2), new Vector2(8, 3))); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board[6, 8].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board[5, 8].Should().BeNull(); - } - - [TestMethod] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Lance moving through the pawn before it. - var invalidLanceMove = new Move(new Vector2(0, 8), new Vector2(0, 4)); - - var shogi = new Shogi(); - var moveSuccess = shogi.Move(invalidLanceMove); - - moveSuccess.Should().BeFalse(); - // Assert the Lance has not actually moved. - shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); - } - - [TestMethod] - public void PreventInvalidMoves_CaptureAlly() - { - // Knight capturing allied Pawn - var invalidKnightMove = new Move(new Vector2(1, 8), new Vector2(0, 6)); - - var shogi = new Shogi(); - var moveSuccess = shogi.Move(invalidKnightMove); - - moveSuccess.Should().BeFalse(); - // Assert the Knight has not actually moved or captured. - shogi.Board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); - shogi.Board[2, 0].WhichPiece.Should().Be(WhichPiece.Pawn); - } - - [TestMethod] - public void PreventInvalidMoves_Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5)), - // P2 Pawn - new Move(new Vector2(6, 2), new Vector2(6, 3)), - // P1 Bishop puts P2 in check - new Move(new Vector2(1, 7), new Vector2(6, 2)) - }; - var shogi = new Shogi(moves); - - // Prerequisit - shogi.InCheck.Should().Be(WhichPlayer.Player2); - - // Act - P2 moves Lance while remaining in check. - var moveSuccess = shogi.Move(new Move(new Vector2(0, 8), new Vector2(0, 7))); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.InCheck.Should().Be(WhichPlayer.Player2); - shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board[7, 8].Should().BeNull(); - } - - [TestMethod] - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5) ), - // P2 Pawn - new Move(new Vector2(0, 2), new Vector2(0, 3) ), - // P1 Bishop takes P2 Pawn - new Move(new Vector2(1, 7), new Vector2(6, 2) ), - // P2 Gold, block check from P1 Bishop. - new Move(new Vector2(5, 0), new Vector2(5, 1) ), - // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - new Move(new Vector2(6, 2), new Vector2(7, 1), true ), - // P2 Pawn again - new Move(new Vector2(0, 3), new Vector2(0, 4) ), - // P1 Bishop takes P2 Knight - new Move(new Vector2(7, 1), new Vector2(7, 0) ), - // P2 Pawn again - new Move(new Vector2(0, 4), new Vector2(0, 5) ), - // P1 Bishop takes P2 Lance - new Move(new Vector2(7, 0), new Vector2(8, 0) ), - // P2 Lance (move to make room for attempted P1 Pawn placement) - new Move(new Vector2(0, 0), new Vector2(0, 1) ), - // P1 arbitrary move - new Move(new Vector2(4, 8), new Vector2(4, 7) ), - // P2 Pawn again, takes P1 Pawn - new Move(new Vector2(0, 5) , new Vector2(0, 6) ), - }; - var shogi = new Shogi(moves); - - // Prerequisites - shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - - // Act | Assert - It is P1 turn - /// try illegally placing Knight from the hand. - shogi.Board[0, 7].Should().BeNull(); - var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 0))); - dropSuccess.Should().BeFalse(); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[0, 7].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 1))); - dropSuccess.Should().BeFalse(); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[1, 7].Should().BeNull(); - - /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, new Vector2(7, 0))); - dropSuccess.Should().BeFalse(); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Board[0, 7].Should().BeNull(); - - /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(7, 0))); - dropSuccess.Should().BeFalse(); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[0, 7].Should().BeNull(); - } - - [TestMethod] - public void PreventInvalidDrop_Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5)), - // P2 Pawn - new Move(new Vector2(8, 2), new Vector2(8, 3)), - // P1 Bishop, check - new Move(new Vector2(1, 7), new Vector2(6, 2)), - // P2 Gold, block check - new Move(new Vector2(5, 0), new Vector2(5, 1)), - // P1 arbitrary move - new Move(new Vector2(0, 6), new Vector2(0, 5)), - // P2 Bishop - new Move(new Vector2(7, 1), new Vector2(8, 2)), - // P1 Bishop takes P2 Lance - new Move(new Vector2(6, 2), new Vector2(8, 0)), - // P2 Bishop - new Move(new Vector2(8, 2), new Vector2(7, 1)), - // P1 arbitrary move - new Move(new Vector2(0, 5), new Vector2(0, 4)), - // P2 Bishop, check - new Move(new Vector2(7, 1), new Vector2(2, 6)), - }; - var shogi = new Shogi(moves); - - // Prerequisites - shogi.InCheck.Should().Be(WhichPlayer.Player1); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - - // Act - P1 tries to place a Lance while in check. - var dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(4, 4))); - - // Assert - dropSuccess.Should().BeFalse(); - shogi.Board[4, 4].Should().BeNull(); - shogi.InCheck.Should().Be(WhichPlayer.Player1); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - } - - [TestMethod] - public void PreventInvalidDrop_Capture() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5)), - // P2 Pawn - new Move(new Vector2(6, 2), new Vector2(6, 3)), - // P1 Bishop, capture P2 Pawn, check - new Move(new Vector2(1, 7), new Vector2(6, 2)), - // P2 Gold, block check - new Move(new Vector2(5, 0), new Vector2(5, 1)), - // P1 Bishop capture P2 Bishop - new Move(new Vector2(6, 2), new Vector2(7, 1)), - // P2 arbitrary move - new Move(new Vector2(0, 0), new Vector2(0, 1)), - }; - var shogi = new Shogi(moves); - - // Prerequisites - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[0, 4].Should().NotBeNull(); - - // Act - P1 tries to place Bishop from hand to an already-occupied position - var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, new Vector2(4, 0))); - - // Assert - dropSuccess.Should().BeFalse(); - shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[0, 4].WhichPiece.Should().Be(WhichPiece.King); - } - - [TestMethod] - public void Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5) ), - // P2 Pawn - new Move(new Vector2(6, 2), new Vector2(6, 3) ), - }; - var shogi = new Shogi(moves); - - // Act - P1 Bishop, check - shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2))); - - // Assert - shogi.InCheck.Should().Be(WhichPlayer.Player2); - } - - [TestMethod] - public void Capture() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5)), - // P2 Pawn - new Move(new Vector2(6, 2), new Vector2(6, 3)) - }; - var shogi = new Shogi(moves); - - // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(7, 1))); - - // Assert - moveSuccess.Should().BeTrue(); - shogi.Board - .Cast() - .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) - .Should() - .Be(1); - shogi.Board[7, 1].Should().BeNull(); - shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Hands[WhichPlayer.Player1] - .Should() - .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); - - - // Act - P2 Silver captures P1 Bishop - moveSuccess = shogi.Move(new Move(new Vector2(6, 0), new Vector2(7, 1))); - - // Assert - moveSuccess.Should().BeTrue(); - shogi.Board[0, 6].Should().BeNull(); - shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - shogi.Board - .Cast() - .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) - .Should().Be(0); - shogi.Hands[WhichPlayer.Player2] - .Should() - .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player2); - } - - [TestMethod] - public void Promote() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move(new Vector2(2, 6), new Vector2(2, 5) ), - // P2 Pawn - new Move(new Vector2(6, 2), new Vector2(6, 3) ) - }; - var shogi = new Shogi(moves); - - // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2), true)); - - // Assert - moveSuccess.Should().BeTrue(); - shogi.Board[7, 1].Should().BeNull(); - shogi.Board[2, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); - } - - [TestMethod] - public void CheckMate() - { - // Arrange - var moves = new[] - { - // P1 Rook - new Move(new Vector2(7, 7), new Vector2(4, 7) ), - // P2 Gold - new Move(new Vector2(3, 0), new Vector2(2, 1) ), - // P1 Pawn - new Move(new Vector2(4, 6), new Vector2(4, 5) ), - // P2 other Gold - new Move(new Vector2(5, 0), new Vector2(6, 1) ), - // P1 same Pawn - new Move(new Vector2(4, 5), new Vector2(4, 4) ), - // P2 Pawn - new Move(new Vector2(4, 2), new Vector2(4, 3) ), - // P1 Pawn takes P2 Pawn - new Move(new Vector2(4, 4), new Vector2(4, 3) ), - // P2 King - new Move(new Vector2(4, 0), new Vector2(4, 1) ), - // P1 Pawn promotes, threatens P2 King - new Move(new Vector2(4, 3), new Vector2(4, 2), true ), - // P2 King retreat - new Move(new Vector2(4, 1), new Vector2(4, 0) ), - }; - var shogi = new Shogi(moves); - - // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move(new Vector2(4, 2), new Vector2(4, 1))); - - // Assert - checkmate - moveSuccess.Should().BeTrue(); - shogi.IsCheckmate.Should().BeTrue(); - } + } } diff --git a/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs b/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs new file mode 100644 index 0000000..d7c0123 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs @@ -0,0 +1,41 @@ +using AutoFixture; +using FluentAssertions; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Utilities; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests +{ + public class CoordsToNotationCollectionShould + { + private readonly Fixture fixture; + private readonly CoordsToNotationCollection collection; + public CoordsToNotationCollectionShould() + { + fixture = new Fixture(); + collection = new CoordsToNotationCollection(); + } + + [Fact] + public void TranslateCoordinatesToNotation() + { + // Arrange + collection[0, 0] = fixture.Create(); + collection[4, 4] = fixture.Create(); + collection[8, 8] = fixture.Create(); + collection[2, 2] = fixture.Create(); + + // Assert + collection["A1"].Should().BeSameAs(collection[0, 0]); + collection["E5"].Should().BeSameAs(collection[4, 4]); + collection["I9"].Should().BeSameAs(collection[8, 8]); + collection["C3"].Should().BeSameAs(collection[2, 2]); + } + + [Fact] + public void Yep() + { + + } + } +} diff --git a/Gameboard.ShogiUI.xUnitTests/GameShould.cs b/Gameboard.ShogiUI.xUnitTests/GameShould.cs index 80f299d..7d9354e 100644 --- a/Gameboard.ShogiUI.xUnitTests/GameShould.cs +++ b/Gameboard.ShogiUI.xUnitTests/GameShould.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Xunit; namespace Gameboard.ShogiUI.xUnitTests diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj index 5cc61ef..c86f2d1 100644 --- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -25,4 +25,10 @@ + + + Always + + + diff --git a/Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs b/Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs new file mode 100644 index 0000000..45c4020 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs @@ -0,0 +1,22 @@ +using FluentAssertions; +using Gameboard.ShogiUI.Sockets.Utilities; +using System.Numerics; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests +{ + public class NotationHelperShould + { + [Fact] + public void TranslateVectorsToNotation() + { + NotationHelper.ToBoardNotation(2, 2).Should().Be("C3"); + } + + [Fact] + public void TranslateNotationToVectors() + { + NotationHelper.FromBoardNotation("C3").Should().Be(new Vector2(2, 2)); + } + } +} diff --git a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs index 2ae7da4..8541ea4 100644 --- a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs +++ b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs @@ -1,8 +1,8 @@ using AutoFixture; using FluentAssertions; using FluentAssertions.Execution; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Gameboard.ShogiUI.Sockets.Services.RequestValidators; using Xunit; diff --git a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs new file mode 100644 index 0000000..37234f1 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs @@ -0,0 +1,594 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Models; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; +using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer; + +namespace Gameboard.ShogiUI.xUnitTests +{ + public class ShogiShould + { + private readonly ITestOutputHelper output; + public ShogiShould(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void InitializeBoardState() + { + // Act + var board = new Shogi().Board; + + // Assert + board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + board["A1"].Owner.Should().Be(WhichPlayer.Player1); + board["A1"].IsPromoted.Should().Be(false); + board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + board["B1"].Owner.Should().Be(WhichPlayer.Player1); + board["B1"].IsPromoted.Should().Be(false); + board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C1"].Owner.Should().Be(WhichPlayer.Player1); + board["C1"].IsPromoted.Should().Be(false); + board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D1"].Owner.Should().Be(WhichPlayer.Player1); + board["D1"].IsPromoted.Should().Be(false); + board["E1"].WhichPiece.Should().Be(WhichPiece.King); + board["E1"].Owner.Should().Be(WhichPlayer.Player1); + board["E1"].IsPromoted.Should().Be(false); + board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F1"].Owner.Should().Be(WhichPlayer.Player1); + board["F1"].IsPromoted.Should().Be(false); + board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G1"].Owner.Should().Be(WhichPlayer.Player1); + board["G1"].IsPromoted.Should().Be(false); + board["H1"].WhichPiece.Should().Be(WhichPiece.Knight); + board["H1"].Owner.Should().Be(WhichPlayer.Player1); + board["H1"].IsPromoted.Should().Be(false); + board["I1"].WhichPiece.Should().Be(WhichPiece.Lance); + board["I1"].Owner.Should().Be(WhichPlayer.Player1); + board["I1"].IsPromoted.Should().Be(false); + + board["A2"].Should().BeNull(); + board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop); + board["B2"].Owner.Should().Be(WhichPlayer.Player1); + board["B2"].IsPromoted.Should().Be(false); + board["C2"].Should().BeNull(); + board["D2"].Should().BeNull(); + board["E2"].Should().BeNull(); + board["F2"].Should().BeNull(); + board["G2"].Should().BeNull(); + board["H2"].WhichPiece.Should().Be(WhichPiece.Rook); + board["H2"].Owner.Should().Be(WhichPlayer.Player1); + board["H2"].IsPromoted.Should().Be(false); + board["I2"].Should().BeNull(); + + board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["A3"].Owner.Should().Be(WhichPlayer.Player1); + board["A3"].IsPromoted.Should().Be(false); + board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["B3"].Owner.Should().Be(WhichPlayer.Player1); + board["B3"].IsPromoted.Should().Be(false); + board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["C3"].Owner.Should().Be(WhichPlayer.Player1); + board["C3"].IsPromoted.Should().Be(false); + board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["D3"].Owner.Should().Be(WhichPlayer.Player1); + board["D3"].IsPromoted.Should().Be(false); + board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["E3"].Owner.Should().Be(WhichPlayer.Player1); + board["E3"].IsPromoted.Should().Be(false); + board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["F3"].Owner.Should().Be(WhichPlayer.Player1); + board["F3"].IsPromoted.Should().Be(false); + board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["G3"].Owner.Should().Be(WhichPlayer.Player1); + board["G3"].IsPromoted.Should().Be(false); + board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["H3"].Owner.Should().Be(WhichPlayer.Player1); + board["H3"].IsPromoted.Should().Be(false); + board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["I3"].Owner.Should().Be(WhichPlayer.Player1); + board["I3"].IsPromoted.Should().Be(false); + + board["A4"].Should().BeNull(); + board["B4"].Should().BeNull(); + board["C4"].Should().BeNull(); + board["D4"].Should().BeNull(); + board["E4"].Should().BeNull(); + board["F4"].Should().BeNull(); + board["G4"].Should().BeNull(); + board["H4"].Should().BeNull(); + board["I4"].Should().BeNull(); + + board["A5"].Should().BeNull(); + board["B5"].Should().BeNull(); + board["C5"].Should().BeNull(); + board["D5"].Should().BeNull(); + board["E5"].Should().BeNull(); + board["F5"].Should().BeNull(); + board["G5"].Should().BeNull(); + board["H5"].Should().BeNull(); + board["I5"].Should().BeNull(); + + board["A6"].Should().BeNull(); + board["B6"].Should().BeNull(); + board["C6"].Should().BeNull(); + board["D6"].Should().BeNull(); + board["E6"].Should().BeNull(); + board["F6"].Should().BeNull(); + board["G6"].Should().BeNull(); + board["H6"].Should().BeNull(); + board["I6"].Should().BeNull(); + + board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["A7"].Owner.Should().Be(WhichPlayer.Player2); + board["A7"].IsPromoted.Should().Be(false); + board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["B7"].Owner.Should().Be(WhichPlayer.Player2); + board["B7"].IsPromoted.Should().Be(false); + board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["C7"].Owner.Should().Be(WhichPlayer.Player2); + board["C7"].IsPromoted.Should().Be(false); + board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["D7"].Owner.Should().Be(WhichPlayer.Player2); + board["D7"].IsPromoted.Should().Be(false); + board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["E7"].Owner.Should().Be(WhichPlayer.Player2); + board["E7"].IsPromoted.Should().Be(false); + board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["F7"].Owner.Should().Be(WhichPlayer.Player2); + board["F7"].IsPromoted.Should().Be(false); + board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["G7"].Owner.Should().Be(WhichPlayer.Player2); + board["G7"].IsPromoted.Should().Be(false); + board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["H7"].Owner.Should().Be(WhichPlayer.Player2); + board["H7"].IsPromoted.Should().Be(false); + board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["I7"].Owner.Should().Be(WhichPlayer.Player2); + board["I7"].IsPromoted.Should().Be(false); + + board["A8"].Should().BeNull(); + board["B8"].WhichPiece.Should().Be(WhichPiece.Rook); + board["B8"].Owner.Should().Be(WhichPlayer.Player2); + board["B8"].IsPromoted.Should().Be(false); + board["C8"].Should().BeNull(); + board["D8"].Should().BeNull(); + board["E8"].Should().BeNull(); + board["F8"].Should().BeNull(); + board["G8"].Should().BeNull(); + board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); + board["H8"].Owner.Should().Be(WhichPlayer.Player2); + board["H8"].IsPromoted.Should().Be(false); + board["I8"].Should().BeNull(); + + board["A9"].WhichPiece.Should().Be(WhichPiece.Lance); + board["A9"].Owner.Should().Be(WhichPlayer.Player2); + board["A9"].IsPromoted.Should().Be(false); + board["B9"].WhichPiece.Should().Be(WhichPiece.Knight); + board["B9"].Owner.Should().Be(WhichPlayer.Player2); + board["B9"].IsPromoted.Should().Be(false); + board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C9"].Owner.Should().Be(WhichPlayer.Player2); + board["C9"].IsPromoted.Should().Be(false); + board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D9"].Owner.Should().Be(WhichPlayer.Player2); + board["D9"].IsPromoted.Should().Be(false); + board["E9"].WhichPiece.Should().Be(WhichPiece.King); + board["E9"].Owner.Should().Be(WhichPlayer.Player2); + board["E9"].IsPromoted.Should().Be(false); + board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F9"].Owner.Should().Be(WhichPlayer.Player2); + board["F9"].IsPromoted.Should().Be(false); + board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G9"].Owner.Should().Be(WhichPlayer.Player2); + board["G9"].IsPromoted.Should().Be(false); + board["H9"].WhichPiece.Should().Be(WhichPiece.Knight); + board["H9"].Owner.Should().Be(WhichPlayer.Player2); + board["H9"].IsPromoted.Should().Be(false); + board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + board["I9"].Owner.Should().Be(WhichPlayer.Player2); + board["I9"].IsPromoted.Should().Be(false); + } + + [Fact] + public void InitializeBoardStateWithMoves() + { + var moves = new[] + { + // P1 Pawn + new Move("A3", "A4") + }; + var shogi = new Shogi(moves); + shogi.Board["A3"].Should().BeNull(); + shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = new Shogi(); + shogi.Board["D5"].Should().BeNull(); + + // Act + var moveSuccess = shogi.Move(new Move("D5", "D6")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["D5"].Should().BeNull(); + shogi.Board["D6"].Should().BeNull(); + } + + [Fact] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = new Shogi(); + + // Act - P1 "moves" pawn to the position it already exists at. + var moveSuccess = shogi.Move(new Move("A3", "A3")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } + + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var shogi = new Shogi(); + + // Act - Move Lance illegally + var moveSuccess = shogi.Move(new Move("A1", "D5")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["A5"].Should().BeNull(); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } + + [Fact] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = new Shogi(); + shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2); + + // Act - Move Player2 Pawn when it is Player1 turn. + var moveSuccess = shogi.Move(new Move("A7", "A6")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board["A6"].Should().BeNull(); + } + + [Fact] + public void PreventInvalidMoves_MoveThroughAllies() + { + // Arrange + var shogi = new Shogi(); + + // Act - Move P1 Lance through P1 Pawn. + var moveSuccess = shogi.Move(new Move("A1", "A5")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board["A5"].Should().BeNull(); + } + + [Fact] + public void PreventInvalidMoves_CaptureAlly() + { + // Arrange + var shogi = new Shogi(); + + // Act - P1 Knight tries to capture P1 Pawn. + var moveSuccess = shogi.Move(new Move("B1", "C3")); + + // Arrange + moveSuccess.Should().BeFalse(); + shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } + + [Fact] + public void PreventInvalidMoves_Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("G7", "G6"), + // P1 Bishop puts P2 in check + new Move("B2", "G7") + }; + var shogi = new Shogi(moves); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + + // Act - P2 moves Lance while in check. + var moveSuccess = shogi.Move(new Move("I9", "I8")); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I8"].Should().BeNull(); + } + + [Fact] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("I7", "I6"), + // P1 Bishop takes P2 Pawn. + new Move("B2", "G7"), + // P2 Gold, block check from P1 Bishop. + new Move("F9", "F8"), + // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + new Move("G7", "H8", true), + // P2 Pawn again + new Move("I6", "I5"), + // P1 Bishop takes P2 Knight + new Move("H8", "H9"), + // P2 Pawn again + new Move("I5", "I4"), + // P1 Bishop takes P2 Lance + new Move("H9", "I9"), + // P2 Pawn captures P1 Pawn + new Move("I4", "I3") + }; + var shogi = new Shogi(moves); + shogi.Player1Hand.Count.Should().Be(4); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + + // Act | Assert - Illegally placing Knight from the hand in farthest row. + shogi.Board["H9"].Should().BeNull(); + var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + + // Act | Assert - Illegally placing Knight from the hand in second farthest row. + shogi.Board["H8"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); + dropSuccess.Should().BeFalse(); + shogi.Board["H8"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + + // Act | Assert - Illegally place Lance from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + + // Act | Assert - Illegally place Pawn from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + + // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // TODO + } + + [Fact] + public void PreventInvalidDrop_Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("G7", "G6"), + // P1 Pawn, arbitrary move. + new Move("A3", "A4"), + // P2 Bishop takes P1 Bishop + new Move("H8", "B2"), + // P1 Silver takes P2 Bishop + new Move("C1", "B2"), + // P2 Pawn, arbtrary move + new Move("A7", "A6"), + // P1 drop Bishop, place P2 in check + new Move(WhichPiece.Bishop, "G7") + }; + var shogi = new Shogi(moves); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["E5"].Should().BeNull(); + + // Act - P2 places a Bishop while in check. + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); + + // Assert + dropSuccess.Should().BeFalse(); + shogi.Board["E5"].Should().BeNull(); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + } + + [Fact] + public void PreventInvalidDrop_Capture() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("G7", "G6"), + // P1 Bishop capture P2 Bishop + new Move("B2", "H8"), + // P2 Pawn + new Move("G6", "G5") + }; + var shogi = new Shogi(moves); + using (new AssertionScope()) + { + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["I9"].Should().NotBeNull(); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + } + + // Act - P1 tries to place a piece where an opponent's piece resides. + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); + + // Assert + using (new AssertionScope()) + { + dropSuccess.Should().BeFalse(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["I9"].Should().NotBeNull(); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + } + } + + [Fact] + public void Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("G7", "G6"), + }; + var shogi = new Shogi(moves); + + // Act - P1 Bishop, check + shogi.Move(new Move("B2", "G7")); + + // Assert + shogi.InCheck.Should().Be(WhichPlayer.Player2); + } + + [Fact] + public void Promote() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4" ), + // P2 Pawn + new Move("G7", "G6" ) + }; + var shogi = new Shogi(moves); + + // Act - P1 moves across promote threshold. + var moveSuccess = shogi.Move(new Move("B2", "G7", true)); + + // Assert + using (new AssertionScope()) + { + moveSuccess.Should().BeTrue(); + shogi.Board["B2"].Should().BeNull(); + shogi.Board["G7"].Should().NotBeNull(); + shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board["G7"].Owner.Should().Be(WhichPlayer.Player1); + shogi.Board["G7"].IsPromoted.Should().BeTrue(); + } + } + + [Fact] + public void CheckMate() + { + // Arrange + var moves = new[] + { + // P1 Rook + new Move("H2", "E2"), + // P2 Gold + new Move("F9", "G8"), + // P1 Pawn + new Move("E3", "E4"), + // P2 other Gold + new Move("D9", "C8"), + // P1 same Pawn + new Move("E4", "E5"), + // P2 Pawn + new Move("E7", "E6"), + // P1 Pawn takes P2 Pawn + new Move("E5", "E6"), + // P2 King + new Move("E9", "E8"), + // P1 Pawn promotes, threatens P2 King + new Move("E6", "E7", true), + // P2 King retreat + new Move("E8", "E9"), + }; + var shogi = new Shogi(moves); + output.WriteLine(shogi.PrintStateAsAscii()); + + // Act - P1 Pawn wins by checkmate. + var moveSuccess = shogi.Move(new Move("E7", "E8")); + output.WriteLine(shogi.PrintStateAsAscii()); + + // Assert - checkmate + moveSuccess.Should().BeTrue(); + shogi.IsCheckmate.Should().BeTrue(); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + } + + [Fact] + public void Capture() + { + // Arrange + var moves = new[] + { + new Move("C3", "C4"), + new Move("G7", "G6") + }; + var shogi = new Shogi(moves); + + // Act - P1 Bishop captures P2 Bishop + var moveSuccess = shogi.Move(new Move("B2", "H8")); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board["B2"].Should().BeNull(); + shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board["H8"].Owner.Should().Be(WhichPlayer.Player1); + shogi.Board.Values + .Where(p => p != null) + .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); + + shogi.Player1Hand + .Should() + .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + } + } +} diff --git a/Gameboard.ShogiUI.xUnitTests/xunit.runner.json b/Gameboard.ShogiUI.xUnitTests/xunit.runner.json new file mode 100644 index 0000000..1d28022 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "methodDisplay": "method" +} diff --git a/PathFinding/Direction.cs b/PathFinding/Direction.cs index 8a199e4..2ee825d 100644 --- a/PathFinding/Direction.cs +++ b/PathFinding/Direction.cs @@ -4,15 +4,15 @@ namespace PathFinding { public static class Direction { - public static readonly Vector2 Up = new(0, -1); - public static readonly Vector2 Down = new(0, 1); + public static readonly Vector2 Up = new(0, 1); + public static readonly Vector2 Down = new(0, -1); public static readonly Vector2 Left = new(-1, 0); public static readonly Vector2 Right = new(1, 0); - public static readonly Vector2 UpLeft = new(-1, -1); - public static readonly Vector2 UpRight = new(1, -1); - public static readonly Vector2 DownLeft = new(-1, 1); - public static readonly Vector2 DownRight = new(1, 1); - public static readonly Vector2 KnightLeft = new(-1, -2); - public static readonly Vector2 KnightRight = new(1, -2); + public static readonly Vector2 UpLeft = new(-1, 1); + public static readonly Vector2 UpRight = new(1, 1); + public static readonly Vector2 DownLeft = new(-1, -1); + public static readonly Vector2 DownRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); } } diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 48b20e7..0075dcd 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Numerics; namespace PathFinding { - public interface IPlanarCollection : IEnumerable where T : IPlanarElement + public interface IPlanarCollection where T : IPlanarElement { - T? this[float x, float y] { get; set; } - int GetLength(int dimension); + T? this[Vector2 vector] { get; set; } + T? this[int x, int y] { get; set; } } } diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 5f2f769..e5ded9f 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -14,11 +14,14 @@ namespace PathFinding private readonly IPlanarCollection collection; private readonly int width; private readonly int height; - public PathFinder2D(IPlanarCollection collection) + + /// Horizontal size, in steps, of the pathable plane. + /// Vertical size, in steps, of the pathable plane. + public PathFinder2D(IPlanarCollection collection, int width, int height) { this.collection = collection; - width = collection.GetLength(0); - height = collection.GetLength(1); + this.width = width; + this.height = height; } /// @@ -29,13 +32,13 @@ namespace PathFinding /// The destination. /// Do cool stuff here. /// True if the element reached the destination. - public bool PathTo(Vector2 origin, Vector2 destination, Callback callback = null) + public bool PathTo(Vector2 origin, Vector2 destination, Callback? callback = null) { if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) { return false; } - var element = collection[origin.Y, origin.X]; + var element = collection[origin]; if (element == null) return false; var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); @@ -50,7 +53,7 @@ namespace PathFinding while (shouldPath && next != destination) { next = Vector2.Add(next, path.Direction); - var collider = collection[(int)next.Y, (int)next.X]; + var collider = collection[next]; if (collider != null) { callback?.Invoke(collider, next); @@ -66,7 +69,7 @@ namespace PathFinding public void PathEvery(Vector2 from, Callback callback) { - var element = collection[from.Y, from.X]; + var element = collection[from]; if (element == null) { Console.WriteLine("Null element in PathEvery"); @@ -103,7 +106,7 @@ namespace PathFinding var next = Vector2.Add(origin, direction); while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) { - var element = collection[next.Y, next.X]; + var element = collection[next]; if (element != null) callback(element, next); next = Vector2.Add(next, direction); } diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index 4466c3d..68ec955 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/PathFinding/PlanarCollection.cs b/PathFinding/PlanarCollection.cs deleted file mode 100644 index 9fa7266..0000000 --- a/PathFinding/PlanarCollection.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace PathFinding -{ - // TODO: Get rid of this thing in favor of T[,] multi-dimensional array with extension methods. - public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement - { - public delegate void ForEachDelegate(T element, int x, int y); - private readonly T?[] array; - private readonly int width; - private readonly int height; - - public PlanarCollection(int width, int height) - { - this.width = width; - this.height = height; - array = new T[width * height]; - } - - public T? this[int y, int x] - { - get => array[y * width + x]; - set => array[y * width + x] = value; - } - public T? this[float y, float x] - { - get => array[(int)y * width + (int)x]; - set => array[(int)y * width + (int)x] = value; - } - - public int GetLength(int dimension) => dimension switch - { - 0 => height, - 1 => width, - _ => throw new IndexOutOfRangeException() - }; - - public void ForEachNotNull(ForEachDelegate callback) - { - for (var x = 0; x < width; x++) - { - for (var y = 0; y < height; y++) - { - var elem = this[y, x]; - if (elem != null) - callback(elem, x, y); - } - } - } - - public IEnumerator GetEnumerator() - { - foreach (var item in array) - if (item != null) yield return item; - } - - IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); - } -} From bb1d2c491c23e0fec5c07d05b9c9f918f3bb126a Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 1 Aug 2021 20:09:11 -0500 Subject: [PATCH 17/27] yep --- CouchDB/CouchDB.csproj | 4 ++- CouchDB/Selectors/CouchQuery.cs | 27 ------------------- CouchDB/Selectors/Equals.cs | 18 ------------- .../Controllers/GameController.cs | 2 +- .../Managers/GameboardManager.cs | 6 ++--- .../Repositories/GameboardRepository.cs | 8 ++++-- 6 files changed, 13 insertions(+), 52 deletions(-) delete mode 100644 CouchDB/Selectors/CouchQuery.cs delete mode 100644 CouchDB/Selectors/Equals.cs diff --git a/CouchDB/CouchDB.csproj b/CouchDB/CouchDB.csproj index 83f1b25..168228b 100644 --- a/CouchDB/CouchDB.csproj +++ b/CouchDB/CouchDB.csproj @@ -5,7 +5,9 @@ - + + + diff --git a/CouchDB/Selectors/CouchQuery.cs b/CouchDB/Selectors/CouchQuery.cs deleted file mode 100644 index 7bd7b29..0000000 --- a/CouchDB/Selectors/CouchQuery.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; - -namespace CouchDB.Selectors -{ - public class CouchQuery - { - public static CouchQuery Select => new(); - - private readonly List equals; - protected CouchQuery() - { - equals = new List(); - } - - public CouchQuery WithEqual(string key, string value) - { - equals.Add(new Equals(key, value)); - return this; - } - - public override string ToString() - { - var selector = string.Join(",", equals); - return $"{{ \"selector\": {selector}"; - } - } -} diff --git a/CouchDB/Selectors/Equals.cs b/CouchDB/Selectors/Equals.cs deleted file mode 100644 index 418f547..0000000 --- a/CouchDB/Selectors/Equals.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace CouchDB.Selectors -{ - public class Equals - { - private readonly string key; - private readonly string value; - internal Equals(string key, string value) - { - this.key = key; - this.value = value; - } - - public override string ToString() - { - return $"{{ \"{key}\": {{ \"$eq\": {value}}} }}"; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index bb17546..57fb268 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -100,7 +100,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers PlayerName = user.Name, Move = moveModel.ToServiceModel() }, session.Player1, session.Player2); - return Ok(); + return Created(string.Empty, null); } throw new InvalidOperationException("Illegal move."); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index 17e3de1..365722f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -34,11 +34,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers while (count < MaxTries) { count++; - var clientId = $"Guest-{Guid.NewGuid()}"; - var isCreated = await repository.CreateGuestUser(clientId, webSessionId); + var userName = $"Guest-{Guid.NewGuid()}"; + var isCreated = await repository.CreateUser(new User(userName, webSessionId)); if (isCreated) { - return clientId; + return userName; } } throw new OperationCanceledException($"Failed to create guest user after {count} tries."); diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index c519e4f..fce2aaa 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -241,8 +241,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories logger.LogError(new InvalidOperationException(result.warning), result.warning); return null; } - - return new Models.User(result.docs.Single().Name); + var userDocument = result.docs.SingleOrDefault(); + if (userDocument != null) + { + return new Models.User(userDocument.Name); + } + return null; } } } From 2a3b7b32b4a3fd34215fdcca426c8d0a48100a03 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 3 Sep 2021 22:43:06 -0500 Subject: [PATCH 18/27] massive checkpoint --- Benchmarking/Benchmarking.csproj | 2 +- .../Api/GetGame.cs | 17 ++ .../Api/PostMove.cs | 3 - .../Api/PostSession.cs | 2 - .../Socket/CreateGame.cs | 16 +- .../Socket/IResponse.cs | 1 - .../Socket/ListGames.cs | 25 --- .../Socket/LoadGame.cs | 26 ---- .../Socket/Move.cs | 22 +-- .../Types/ClientActionEnum.cs | 2 - .../Controllers/GameController.cs | 90 ++++++++--- .../Controllers/SocketController.cs | 107 +++++++------ .../Extensions/Extensions.cs | 18 +++ .../Gameboard.ShogiUI.Sockets.csproj | 16 +- .../ClientActionHandlers/CreateGameHandler.cs | 54 ------- .../ClientActionHandlers/ListGamesHandler.cs | 40 ----- .../ClientActionHandlers/LoadGameHandler.cs | 57 ------- .../ClientActionHandlers/MoveHandler.cs | 61 -------- .../Managers/GameboardManager.cs | 67 ++------ ...ketTokenManager.cs => SocketTokenCache.cs} | 10 +- Gameboard.ShogiUI.Sockets/Models/Session.cs | 5 +- .../Models/SessionMetadata.cs | 2 +- Gameboard.ShogiUI.Sockets/Models/User.cs | 47 +++++- .../Models/WhichLoginPlatform.cs | 8 + .../Properties/launchSettings.json | 5 +- .../Repositories/CouchModels/UserDocument.cs | 14 +- .../Repositories/GameboardRepository.cs | 44 ++++-- .../CreateGameRequestValidator.cs | 15 -- .../ListGamesRequestValidator.cs | 14 -- .../LoadGameRequestValidator.cs | 15 -- .../RequestValidators/MoveRequestValidator.cs | 23 --- .../Services/SocketService.cs | 75 ++------- .../ShogiUserClaimsTransformer.cs | 46 ++++++ Gameboard.ShogiUI.Sockets/Startup.cs | 147 +++++++++++------- .../Utilities/NotationHelper.cs | 1 - Gameboard.ShogiUI.Sockets/appsettings.json | 6 + .../Gameboard.ShogiUI.UnitTests.csproj | 8 +- .../Gameboard.ShogiUI.xUnitTests.csproj | 6 +- .../MoveRequestValidatorShould.cs | 76 --------- PathFinding/PathFinder2D.cs | 1 - 40 files changed, 456 insertions(+), 738 deletions(-) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs create mode 100644 Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs rename Gameboard.ShogiUI.Sockets/Managers/{SocketTokenManager.cs => SocketTokenCache.cs} (83%) create mode 100644 Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs delete mode 100644 Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index 4e77937..98fe81e 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -7,7 +7,7 @@ - + diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs new file mode 100644 index 0000000..c15dae4 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs @@ -0,0 +1,17 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api +{ + public class GetGameResponse + { + public Game Game { get; set; } + public WhichPlayer PlayerPerspective { get; set; } + public BoardState BoardState { get; set; } + public IList MoveHistory { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs index 153b1d9..012c6ea 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs @@ -5,9 +5,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { public class PostMove { - [Required] - public string GameName { get; set; } - [Required] public Move Move { get; set; } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs index de71f67..ebe9665 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs @@ -3,8 +3,6 @@ public class PostSession { public string Name { get; set; } - public string Player1 { get; set; } - public string Player2 { get; set; } public bool IsPrivate { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs index 6aa082d..e1b59bb 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs @@ -1,27 +1,21 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using System; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { - public class CreateGameRequest : IRequest - { - public ClientAction Action { get; set; } - public string GameName { get; set; } = string.Empty; - public bool IsPrivate { get; set; } - } - public class CreateGameResponse : IResponse { public string Action { get; } - public string Error { get; set; } public Game Game { get; set; } + + /// + /// The player who created the game. + /// public string PlayerName { get; set; } public CreateGameResponse() { Action = ClientAction.CreateGame.ToString(); - Error = string.Empty; - Game = new Game(); - PlayerName = string.Empty; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs index 77f0dc2..69d18c6 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs @@ -3,6 +3,5 @@ public interface IResponse { string Action { get; } - string Error { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs deleted file mode 100644 index d8e5556..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/ListGames.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public class ListGamesRequest : IRequest - { - public ClientAction Action { get; set; } - } - - public class ListGamesResponse : IResponse - { - public string Action { get; } - public string Error { get; set; } - public IReadOnlyList Games { get; set; } - - public ListGamesResponse() - { - Action = ClientAction.ListGames.ToString(); - Error = ""; - Games = new Collection(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs deleted file mode 100644 index 09884cb..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/LoadGame.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public class LoadGameRequest : IRequest - { - public ClientAction Action { get; set; } - public string GameName { get; set; } = ""; - } - - public class LoadGameResponse : IResponse - { - public string Action { get; } - public Game Game { get; set; } - public WhichPlayer PlayerPerspective { get; set; } - public BoardState BoardState { get; set; } - public IList MoveHistory { get; set; } - public string Error { get; set; } - - public LoadGameResponse() - { - Action = ClientAction.LoadGame.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs index d7b9e91..0853998 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs @@ -1,29 +1,19 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { - public class MoveRequest : IRequest - { - public ClientAction Action { get; set; } - public string GameName { get; set; } = string.Empty; - public Move Move { get; set; } = new Move(); - } - public class MoveResponse : IResponse { - public string Action { get; } - public string Error { get; set; } - public string GameName { get; set; } - public string PlayerName { get; set; } - public Move Move { get; set; } + public string Action { get; protected set; } + public Game Game { get; set; } + public WhichPlayer PlayerPerspective { get; set; } + public BoardState BoardState { get; set; } + public IList MoveHistory { get; set; } public MoveResponse() { Action = ClientAction.Move.ToString(); - Error = string.Empty; - GameName = string.Empty; - PlayerName = string.Empty; - Move = new Move(); } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs index f3a655b..99b9446 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs @@ -2,11 +2,9 @@ { public enum ClientAction { - ListGames, CreateGame, JoinGame, JoinByCode, - LoadGame, Move } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 57fb268..718573f 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,7 +1,9 @@ -using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Api; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; @@ -10,16 +12,15 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers { - [Authorize] + [ApiController] [Route("[controller]")] + [Authorize(Roles = "Shogi")] public class GameController : ControllerBase { - private static readonly string UsernameClaim = "preferred_username"; private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; private readonly ISocketConnectionManager communicationManager; - private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value; public GameController( IGameboardRepository repository, @@ -68,22 +69,12 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpPost("{gameName}/Move")] public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request) { - Models.User? user = null; - if (Request.Cookies.ContainsKey(SocketController.WebSessionKey)) - { - var webSessionId = Guid.Parse(Request.Cookies[SocketController.WebSessionKey]!); - user = await gameboardManager.ReadUser(webSessionId); - } - else if (!string.IsNullOrEmpty(JwtUserName)) - { - user = await gameboardManager.ReadUser(JwtUserName); - } - - var session = await gameboardManager.ReadSession(gameName); + var user = await gameboardManager.ReadUser(User); + var session = await gameboardRepository.ReadSession(gameName); if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name)) { - throw new UnauthorizedAccessException("You are not seated at this game."); + throw new UnauthorizedAccessException("User is not seated at this game."); } var move = request.Move; @@ -94,13 +85,19 @@ namespace Gameboard.ShogiUI.Sockets.Controllers if (moveSuccess) { + var createSuccess = await gameboardRepository.CreateBoardState(session); + if (!createSuccess) + { + throw new ApplicationException("Unable to persist board state."); + } await communicationManager.BroadcastToPlayers(new MoveResponse { - GameName = session.Name, - PlayerName = user.Name, - Move = moveModel.ToServiceModel() + BoardState = session.Shogi.ToServiceModel(), + Game = session.ToServiceModel(), + MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(), + PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2 }, session.Player1, session.Player2); - return Created(string.Empty, null); + return Ok(); } throw new InvalidOperationException("Illegal move."); } @@ -124,5 +121,56 @@ namespace Gameboard.ShogiUI.Sockets.Controllers // } // return new ConflictResult(); //} + + [HttpPost] + public async Task PostSession([FromBody] PostSession request) + { + var user = await gameboardManager.ReadUser(User); + var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name); + var success = await gameboardRepository.CreateSession(session); + + if (success) + { + await communicationManager.BroadcastToAll(new CreateGameResponse + { + Game = session.ToServiceModel(), + PlayerName = user.Name + }); + return Ok(); + } + return Conflict(); + + } + + /// + /// Reads the board session and subscribes the caller to socket events for that session. + /// + [HttpGet("{gameName}")] + public async Task GetSession([FromRoute] string gameName) + { + var user = await gameboardManager.ReadUser(User); + var session = await gameboardRepository.ReadSession(gameName); + if (session == null) + { + return NotFound(); + } + + communicationManager.SubscribeToGame(session, user!.Name); + var response = new GetGameResponse() + { + Game = new Models.SessionMetadata(session).ToServiceModel(), + BoardState = session.Shogi.ToServiceModel(), + MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(), + PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2 + }; + return new JsonResult(response); + } + + [HttpGet] + public async Task GetSessions() + { + var sessions = await gameboardRepository.ReadSessionMetadatas(); + return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList()); + } } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index e5ee6db..0b9e941 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,96 +1,113 @@ -using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Api; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; -using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers { - [Authorize] - [Route("[controller]")] [ApiController] + [Route("[controller]")] + [Authorize(Roles = "Shogi")] public class SocketController : ControllerBase { - public static readonly string WebSessionKey = "session-id"; private readonly ILogger logger; - private readonly ISocketTokenManager tokenManager; + private readonly ISocketTokenCache tokenCache; private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; - private readonly CookieOptions createSessionOptions; - private readonly CookieOptions deleteSessionOptions; + private readonly AuthenticationProperties authenticationProps; public SocketController( ILogger logger, - ISocketTokenManager tokenManager, + ISocketTokenCache tokenCache, IGameboardManager gameboardManager, IGameboardRepository gameboardRepository) { this.logger = logger; - this.tokenManager = tokenManager; + this.tokenCache = tokenCache; this.gameboardManager = gameboardManager; this.gameboardRepository = gameboardRepository; - createSessionOptions = new CookieOptions + authenticationProps = new AuthenticationProperties { - Secure = true, - HttpOnly = true, - SameSite = SameSiteMode.None, - Expires = DateTimeOffset.Now.AddYears(5) + AllowRefresh = true, + IsPersistent = true }; - deleteSessionOptions = new CookieOptions(); } - [HttpGet("Yep")] + [HttpGet("GuestLogout")] [AllowAnonymous] - public IActionResult Yep() + public async Task GuestLogout() { - deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1); - Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions); + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Ok(); } [HttpGet("Token")] - public IActionResult GetToken() + public async Task GetToken() { - var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; - var token = tokenManager.GenerateToken(userName); + var identityId = User.UserId(); + if (string.IsNullOrWhiteSpace(identityId)) + { + return Unauthorized(); + } + + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + user = new User(identityId); + var success = await gameboardRepository.CreateUser(user); + if (!success) + { + return Unauthorized(); + } + } + + var token = tokenCache.GenerateToken(user.Name); return new JsonResult(new GetTokenResponse(token)); } - /// - /// Builds a token for guest users to send when requesting a socket connection. - /// Sends a HttpOnly cookie to the client with which to identify guest users. - /// - /// - /// - [AllowAnonymous] [HttpGet("GuestToken")] + [AllowAnonymous] public async Task GetGuestToken() { - var cookies = Request.Cookies; - var webSessionId = cookies.ContainsKey(WebSessionKey) - ? Guid.Parse(cookies[WebSessionKey]!) - : Guid.NewGuid(); - var webSessionIdAsString = webSessionId.ToString(); - - var user = await gameboardRepository.ReadGuestUser(webSessionId); - if (user == null) + if (Guid.TryParse(User.UserId(), out Guid webSessionId)) { - var userName = await gameboardManager.CreateGuestUser(webSessionId); - var token = tokenManager.GenerateToken(webSessionIdAsString); - Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions); - return new JsonResult(new GetGuestTokenResponse(userName, token)); + var user = await gameboardRepository.ReadGuestUser(webSessionId); + if (user != null) + { + var token = tokenCache.GenerateToken(webSessionId.ToString()); + return new JsonResult(new GetGuestTokenResponse(user.Name, token)); + } } else { - var token = tokenManager.GenerateToken(webSessionIdAsString); - Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions); - return new JsonResult(new GetGuestTokenResponse(user.Name, token)); + // Setup a guest user. + var newSessionId = Guid.NewGuid(); + var user = new User(Guid.NewGuid().ToString(), newSessionId); + if (await gameboardRepository.CreateUser(user)) + { + var identity = user.CreateGuestUserIdentity(); + await this.HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + authenticationProps + ); + + var token = tokenCache.GenerateToken(newSessionId.ToString()); + return new JsonResult(new GetGuestTokenResponse(user.Name, token)); + } } + + return Unauthorized(); } } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs new file mode 100644 index 0000000..30517ad --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Security.Claims; + +namespace Gameboard.ShogiUI.Sockets.Extensions +{ + public static class Extensions + { + public static string? UserId(this ClaimsPrincipal self) + { + return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + } + + public static bool IsGuest(this ClaimsPrincipal self) + { + return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest"); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 241f8e8..bd64225 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,13 +8,15 @@ - - - - - - - + + + + + + + + + diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs deleted file mode 100644 index 2041ef8..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface ICreateGameHandler - { - Task Handle(CreateGameRequest request, string userName); - } - - // TODO: This doesn't need to be a socket action. - // It can be an API route and still tell socket connections about the new session. - public class CreateGameHandler : ICreateGameHandler - { - private readonly IGameboardManager manager; - private readonly ISocketConnectionManager connectionManager; - - public CreateGameHandler( - ISocketConnectionManager communicationManager, - IGameboardManager manager) - { - this.manager = manager; - this.connectionManager = communicationManager; - } - - public async Task Handle(CreateGameRequest request, string userName) - { - var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null); - var success = await manager.CreateSession(model); - - if (!success) - { - var error = new CreateGameResponse() - { - Error = "Unable to create game with this name." - }; - await connectionManager.BroadcastToPlayers(error, userName); - } - - var response = new CreateGameResponse() - { - PlayerName = userName, - Game = model.ToServiceModel() - }; - - var task = request.IsPrivate - ? connectionManager.BroadcastToPlayers(response, userName) - : connectionManager.BroadcastToAll(response); - - await task; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs deleted file mode 100644 index b92c8ca..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Linq; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IListGamesHandler - { - Task Handle(ListGamesRequest request, string userName); - } - - public class ListGamesHandler : IListGamesHandler - { - private readonly ISocketConnectionManager communicationManager; - private readonly IGameboardRepository repository; - - public ListGamesHandler( - ISocketConnectionManager communicationManager, - IGameboardRepository repository) - { - this.communicationManager = communicationManager; - this.repository = repository; - } - - public async Task Handle(ListGamesRequest _, string userName) - { - var sessions = await repository.ReadSessionMetadatas(); - var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList(); - - var response = new ListGamesResponse() - { - Games = games - }; - - await communicationManager.BroadcastToPlayers(response, userName); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs deleted file mode 100644 index d82c880..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Microsoft.Extensions.Logging; -using System.Linq; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface ILoadGameHandler - { - Task Handle(LoadGameRequest request, string userName); - } - - /// - /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. - /// - public class LoadGameHandler : ILoadGameHandler - { - private readonly ILogger logger; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketConnectionManager communicationManager; - - public LoadGameHandler( - ILogger logger, - ISocketConnectionManager communicationManager, - IGameboardRepository gameboardRepository) - { - this.logger = logger; - this.gameboardRepository = gameboardRepository; - this.communicationManager = communicationManager; - } - - public async Task Handle(LoadGameRequest request, string userName) - { - var sessionModel = await gameboardRepository.ReadSession(request.GameName); - if (sessionModel == null) - { - logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); - var error = new LoadGameResponse() { Error = "Game not found." }; - await communicationManager.BroadcastToPlayers(error, userName); - return; - } - - communicationManager.SubscribeToGame(sessionModel, userName); - - var response = new LoadGameResponse() - { - Game = new SessionMetadata(sessionModel).ToServiceModel(), - BoardState = sessionModel.Shogi.ToServiceModel(), - MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList() - }; - await communicationManager.BroadcastToPlayers(response, userName); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs deleted file mode 100644 index 706e125..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IMoveHandler - { - Task Handle(MoveRequest request, string userName); - } - public class MoveHandler : IMoveHandler - { - private readonly IGameboardManager gameboardManager; - private readonly ISocketConnectionManager connectionManager; - public MoveHandler( - ISocketConnectionManager connectionManager, - IGameboardManager gameboardManager) - { - this.gameboardManager = gameboardManager; - this.connectionManager = connectionManager; - } - - public async Task Handle(MoveRequest request, string userName) - { - Models.Move moveModel; - if (request.Move.PieceFromCaptured.HasValue) - { - moveModel = new Models.Move(request.Move.PieceFromCaptured.Value, request.Move.To); - } - else - { - moveModel = new Models.Move(request.Move.From!, request.Move.To, request.Move.IsPromotion); - } - - var session = await gameboardManager.ReadSession(request.GameName); - if (session != null) - { - var shogi = session.Shogi; - var moveSuccess = shogi.Move(moveModel); - if (moveSuccess) - { - await gameboardManager.CreateBoardState(session.Name, shogi); - var response = new MoveResponse() - { - GameName = request.GameName, - PlayerName = userName, - Move = moveModel.ToServiceModel() - }; - await connectionManager.BroadcastToPlayers(response, session.Player1, session.Player2); - } - else - { - var response = new MoveResponse() - { - Error = "Invalid move." - }; - await connectionManager.BroadcastToPlayers(response, userName); - } - } - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index 365722f..e4c4aa0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -1,26 +1,21 @@ -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using System; +using System.Security.Claims; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers { public interface IGameboardManager { - Task CreateGuestUser(Guid webSessionId); Task IsPlayer1(string sessionName, string playerName); - Task CreateSession(SessionMetadata session); - Task ReadSession(string gameName); - Task UpdateSession(SessionMetadata session); Task AssignPlayer2ToSession(string sessionName, string userName); - Task CreateBoardState(string sessionName, Shogi shogi); - Task ReadUser(string userName); - Task ReadUser(Guid webSessionId); + Task ReadUser(ClaimsPrincipal user); } public class GameboardManager : IGameboardManager { - private const int MaxTries = 3; private readonly IGameboardRepository repository; public GameboardManager(IGameboardRepository repository) @@ -28,30 +23,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers this.repository = repository; } - public async Task CreateGuestUser(Guid webSessionId) + public Task ReadUser(ClaimsPrincipal user) { - var count = 0; - while (count < MaxTries) + var userId = user.UserId(); + if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId)) { - count++; - var userName = $"Guest-{Guid.NewGuid()}"; - var isCreated = await repository.CreateUser(new User(userName, webSessionId)); - if (isCreated) - { - return userName; - } + return repository.ReadGuestUser(webSessionId); } - throw new OperationCanceledException($"Failed to create guest user after {count} tries."); + else if (!string.IsNullOrEmpty(userId)) + { + return repository.ReadUser(userId); + } + + return Task.FromResult(null); } - public Task ReadUser(Guid webSessionId) - { - return repository.ReadGuestUser(webSessionId); - } - public Task ReadUser(string userName) - { - return repository.ReadUser(userName); - } public async Task IsPlayer1(string sessionName, string playerName) { //var session = await repository.GetGame(sessionName); @@ -69,31 +55,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers return string.Empty; } - public Task CreateSession(SessionMetadata session) - { - return repository.CreateSession(session); - } - - public Task ReadSession(string sessionName) - { - return repository.ReadSession(sessionName); - } - - /// - /// Saves the session to storage. - /// - /// The session to save. - /// True if the session was saved successfully. - public Task UpdateSession(SessionMetadata session) - { - return repository.UpdateSession(session); - } - - public Task CreateBoardState(string sessionName, Shogi shogi) - { - return repository.CreateBoardState(sessionName, shogi); - } - public async Task AssignPlayer2ToSession(string sessionName, string userName) { var isSuccess = false; diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs similarity index 83% rename from Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs rename to Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs index d8c46ab..722dc33 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs @@ -6,20 +6,20 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers { - public interface ISocketTokenManager + public interface ISocketTokenCache { Guid GenerateToken(string s); - string GetUsername(Guid g); + string? GetUsername(Guid g); } - public class SocketTokenManager : ISocketTokenManager + public class SocketTokenCache : ISocketTokenCache { /// /// Key is userName or webSessionId /// private readonly ConcurrentDictionary Tokens; - public SocketTokenManager() + public SocketTokenCache() { Tokens = new ConcurrentDictionary(); } @@ -41,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers } /// User name associated to the guid or null. - public string GetUsername(Guid guid) + public string? GetUsername(Guid guid) { var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key; if (userName != null) diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index 32bd545..67240e0 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Newtonsoft.Json; using System.Collections.Concurrent; using System.Net.WebSockets; @@ -30,5 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Models { Player2 = userName; } + + public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 }; } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index f3cfade..d46b8af 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -10,7 +10,7 @@ public string? Player2 { get; private set; } public bool IsPrivate { get; } - public SessionMetadata(string name, bool isPrivate, string player1, string? player2) + public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null) { Name = name; IsPrivate = isPrivate; diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index ba6959d..acfd4a9 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -1,18 +1,57 @@ -using System; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System; +using System.Collections.Generic; +using System.Security.Claims; namespace Gameboard.ShogiUI.Sockets.Models { public class User { - public static readonly string GuestPrefix = "Guest-"; public string Name { get; } public Guid? WebSessionId { get; } - public bool IsGuest => Name.StartsWith(GuestPrefix) && WebSessionId.HasValue; - public User(string name, Guid? webSessionId = null) + public bool IsGuest => WebSessionId.HasValue; + + public User(string name) + { + Name = name; + } + + /// + /// Constructor for guest user. + /// + public User(string name, Guid webSessionId) { Name = name; WebSessionId = webSessionId; } + + public ClaimsIdentity CreateMsalUserIdentity() + { + var claims = new List() + { + new Claim(ClaimTypes.NameIdentifier, Name), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + }; + return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); + } + + public ClaimsIdentity CreateGuestUserIdentity() + { + // TODO: Make this method static and factory-like. + if (!WebSessionId.HasValue) + { + throw new InvalidOperationException("Cannot create guest identity without a session identifier."); + } + + var claims = new List() + { + new Claim(ClaimTypes.NameIdentifier, WebSessionId.Value.ToString()), + new Claim(ClaimTypes.Role, "Guest"), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + }; + return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs new file mode 100644 index 0000000..5923404 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.Sockets.Models +{ + public enum WhichLoginPlatform + { + Microsoft, + Guest + } +} diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json index 4a087fc..230be61 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json +++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json @@ -18,11 +18,12 @@ }, "Kestrel": { "commandName": "Project", - "launchUrl": "Socket/Token", + "launchBrowser": true, + "launchUrl": "/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://127.0.0.1:5100" + "applicationUrl": "http://localhost:5100" } } } \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index 843094e..c41b60b 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,17 +1,13 @@ -using System; +using Gameboard.ShogiUI.Sockets.Models; +using System; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public class UserDocument : CouchDocument { - public enum LoginPlatform - { - Microsoft, - Guest - } public string Name { get; set; } - public LoginPlatform Platform { get; set; } + public WhichLoginPlatform Platform { get; set; } /// /// The browser session ID saved via Set-Cookie headers. /// Only used with guest accounts. @@ -31,8 +27,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels Name = name; WebSessionId = webSessionId; Platform = WebSessionId.HasValue - ? LoginPlatform.Guest - : LoginPlatform.Microsoft; + ? WhichLoginPlatform.Guest + : WhichLoginPlatform.Microsoft; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index fce2aaa..89ee28c 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -7,12 +7,13 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using System.Web; namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task CreateBoardState(string sessionName, Models.Shogi shogi); + Task CreateBoardState(Models.Session session); Task CreateSession(Models.SessionMetadata session); Task CreateUser(Models.User user); Task> ReadSessionMetadatas(); @@ -46,11 +47,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync("_find", content); var responseContent = await response.Content.ReadAsStringAsync(); - var sessions = JsonConvert.DeserializeObject>(responseContent).docs; + var results = JsonConvert.DeserializeObject>(responseContent); + if (results != null) + { + return results + .docs + .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2)) + .ToList(); + } - return sessions - .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2)) - .ToList(); + return new List(0); } public async Task ReadSession(string name) @@ -113,9 +119,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return new Models.Shogi(moves); } - public async Task CreateBoardState(string sessionName, Models.Shogi shogi) + /// + /// Saves a snapshot of board state and the most recent move. + /// + public async Task CreateBoardState(Models.Session session) { - var boardStateDocument = new BoardStateDocument(sessionName, shogi); + var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi); var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); return response.IsSuccessStatusCode; @@ -198,14 +207,23 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task ReadUser(string userName) { - var document = new UserDocument(userName); - var response = await client.GetAsync(document.Id); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) + try { - var user = JsonConvert.DeserializeObject(responseContent); + var document = new UserDocument(userName); + var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id)); + var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id)); + var response2 = await client.GetAsync(uri); + var responseContent = await response.Content.ReadAsStringAsync(); + if (response.IsSuccessStatusCode) + { + var user = JsonConvert.DeserializeObject(responseContent); - return new Models.User(user.Name); + return new Models.User(user.Name); + } + } + catch (Exception e) + { + Console.WriteLine(e); } return null; } diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs deleted file mode 100644 index 033b218..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class CreateGameRequestValidator : AbstractValidator - { - public CreateGameRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.CreateGame); - RuleFor(_ => _.GameName).NotEmpty(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs deleted file mode 100644 index 38ecc66..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class ListGamesRequestValidator : AbstractValidator - { - public ListGamesRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.ListGames); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs deleted file mode 100644 index 3fb477d..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class LoadGameRequestValidator : AbstractValidator - { - public LoadGameRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.LoadGame); - RuleFor(_ => _.GameName).NotEmpty(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs deleted file mode 100644 index 29a0cb9..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class MoveRequestValidator : AbstractValidator - { - public MoveRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.Move); - RuleFor(_ => _.GameName).NotEmpty(); - RuleFor(_ => _.Move.From) - .Null() - .When(_ => _.Move.PieceFromCaptured.HasValue) - .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); - RuleFor(_ => _.Move.From) - .NotEmpty() - .When(_ => !_.Move.PieceFromCaptured.HasValue) - .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs index 925c018..30fff7f 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -31,65 +31,44 @@ namespace Gameboard.ShogiUI.Sockets.Services private readonly ILogger logger; private readonly ISocketConnectionManager communicationManager; private readonly IGameboardRepository gameboardRepository; - private readonly ISocketTokenManager tokenManager; - private readonly ICreateGameHandler createGameHandler; + private readonly IGameboardManager gameboardManager; + private readonly ISocketTokenCache tokenManager; private readonly IJoinByCodeHandler joinByCodeHandler; private readonly IJoinGameHandler joinGameHandler; - private readonly IListGamesHandler listGamesHandler; - private readonly ILoadGameHandler loadGameHandler; - private readonly IMoveHandler moveHandler; - private readonly IValidator createGameRequestValidator; private readonly IValidator joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; - private readonly IValidator listGamesRequestValidator; - private readonly IValidator loadGameRequestValidator; - private readonly IValidator moveRequestValidator; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, - ISocketTokenManager tokenManager, - ICreateGameHandler createGameHandler, + IGameboardManager gameboardManager, + ISocketTokenCache tokenManager, IJoinByCodeHandler joinByCodeHandler, IJoinGameHandler joinGameHandler, - IListGamesHandler listGamesHandler, - ILoadGameHandler loadGameHandler, - IMoveHandler moveHandler, - IValidator createGameRequestValidator, IValidator joinByCodeRequestValidator, - IValidator joinGameRequestValidator, - IValidator listGamesRequestValidator, - IValidator loadGameRequestValidator, - IValidator moveRequestValidator + IValidator joinGameRequestValidator ) : base() { this.logger = logger; this.communicationManager = communicationManager; this.gameboardRepository = gameboardRepository; + this.gameboardManager = gameboardManager; this.tokenManager = tokenManager; - this.createGameHandler = createGameHandler; this.joinByCodeHandler = joinByCodeHandler; this.joinGameHandler = joinGameHandler; - this.listGamesHandler = listGamesHandler; - this.loadGameHandler = loadGameHandler; - this.moveHandler = moveHandler; - this.createGameRequestValidator = createGameRequestValidator; this.joinByCodeRequestValidator = joinByCodeRequestValidator; this.joinGameRequestValidator = joinGameRequestValidator; - this.listGamesRequestValidator = listGamesRequestValidator; - this.loadGameRequestValidator = loadGameRequestValidator; - this.moveRequestValidator = moveRequestValidator; } public async Task HandleSocketRequest(HttpContext context) { string? userName = null; - if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey)) + var user = await gameboardManager.ReadUser(context.User); + if (user?.WebSessionId != null) { // Guest account - var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!); - userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name; + userName = tokenManager.GetUsername(user.WebSessionId.Value); } else if (context.Request.Query.Keys.Contains("token")) { @@ -123,24 +102,6 @@ namespace Gameboard.ShogiUI.Sockets.Services } switch (request.Action) { - case ClientAction.ListGames: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) - { - await listGamesHandler.Handle(req, userName); - } - break; - } - case ClientAction.CreateGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) - { - await createGameHandler.Handle(req, userName); - } - break; - } case ClientAction.JoinGame: { var req = JsonConvert.DeserializeObject(message); @@ -159,24 +120,6 @@ namespace Gameboard.ShogiUI.Sockets.Services } break; } - case ClientAction.LoadGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) - { - await loadGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.Move: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) - { - await moveHandler.Handle(req, userName); - } - break; - } } } catch (OperationCanceledException ex) diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs new file mode 100644 index 0000000..1cbc725 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs @@ -0,0 +1,46 @@ +using Gameboard.ShogiUI.Sockets.Repositories; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets +{ + /// + /// Standardizes the claims from third party issuers. Also registers new msal users in the database. + /// + public class ShogiUserClaimsTransformer : IClaimsTransformation + { + private static readonly string MsalUsernameClaim = "preferred_username"; + private readonly IGameboardRepository gameboardRepository; + + public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository) + { + this.gameboardRepository = gameboardRepository; + } + + public async Task TransformAsync(ClaimsPrincipal principal) + { + var nameClaim = principal.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim); + if (nameClaim != default) + { + var user = await gameboardRepository.ReadUser(nameClaim.Value); + if (user == null) + { + var newUser = new Models.User(nameClaim.Value); + var success = await gameboardRepository.CreateUser(newUser); + if (success) user = newUser; + } + + if (user != null) + { + return new ClaimsPrincipal(user.CreateMsalUserIdentity()); + } + } + return principal; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 32cd211..92f88e5 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -6,17 +6,23 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Gameboard.ShogiUI.Sockets.Services; using Gameboard.ShogiUI.Sockets.Services.RequestValidators; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System; +using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Text; using System.Threading.Tasks; @@ -34,29 +40,16 @@ namespace Gameboard.ShogiUI.Sockets // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - // Socket ActionHandlers - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Managers services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - - // Services - services.AddSingleton, CreateGameRequestValidator>(); services.AddSingleton, JoinByCodeRequestValidator>(); services.AddSingleton, JoinGameRequestValidator>(); - services.AddSingleton, ListGamesRequestValidator>(); - services.AddSingleton, LoadGameRequestValidator>(); - services.AddSingleton, MoveRequestValidator>(); services.AddSingleton(); - - // Repositories + services.AddTransient(); + services.AddSingleton(); services.AddHttpClient("couchdb", c => { var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); @@ -66,37 +59,56 @@ namespace Gameboard.ShogiUI.Sockets var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; c.BaseAddress = new Uri(baseUrl); }); - services.AddTransient(); - //services.AddSingleton(); - //services.AddSingleton(provider => new CouchClient(databaseName, couchUrl)); - - services.AddControllers(); services - .AddAuthentication(options => + .AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.ContractResolver = new DefaultContractResolver { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"; - options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122"; - options.TokenValidationParameters.ValidateIssuer = true; - options.TokenValidationParameters.ValidateAudience = true; + NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } + }; + options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); - options.Events = new JwtBearerEvents - { - OnMessageReceived = (context) => - { - if (context.HttpContext.WebSockets.IsWebSocketRequest) - { - Console.WriteLine("Yep"); - } - return Task.FromResult(0); - } - }; - }); + services.AddAuthentication("CookieOrJwt") + .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options => + { + options.ForwardDefaultSelector = context => + { + var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false; + return bearerAuth + ? JwtBearerDefaults.AuthenticationScheme + : CookieAuthenticationDefaults.AuthenticationScheme; + }; + }) + .AddCookie(options => + { + options.Cookie.Name = "session-id"; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; + options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; + options.SlidingExpiration = true; + }) + .AddMicrosoftIdentityWebApi(Configuration); + + services.AddSwaggerDocument(config => + { + config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme + { + Type = NSwag.OpenApiSecuritySchemeType.OAuth2, + Flow = NSwag.OpenApiOAuth2Flow.AccessCode, + AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token", + Scopes = new Dictionary { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } }, + Scheme = "Bearer" + }); + config.PostProcess = document => + { + document.Info.Title = "Gameboard.ShogiUI.Sockets"; + }; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -114,30 +126,45 @@ namespace Gameboard.ShogiUI.Sockets if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + var client = PublicClientApplicationBuilder + .Create(Configuration["AzureAd:ClientId"]) + .WithLogging( + (level, message, pii) => + { + + }, + LogLevel.Verbose, + true, + true + ) + .Build(); } else { app.UseHsts(); } app - .UseRequestResponseLogging() - .UseCors( - opt => opt - .WithOrigins(origins) - .AllowAnyMethod() - .AllowAnyHeader() - .WithExposedHeaders("Set-Cookie") - .AllowCredentials() - ) - .UseRouting() - .UseAuthentication() - .UseAuthorization() - .UseWebSockets(socketOptions) - .UseEndpoints(endpoints => + .UseRequestResponseLogging() + .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseOpenApi() + .UseSwaggerUi3(config => + { + config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings() { - endpoints.MapControllers(); - }) - .Use(async (context, next) => + ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff", + UsePkceWithAuthorizationCodeGrant = true + }; + //config.WithCredentials = true; + }) + .UseWebSockets(socketOptions) + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }) + .Use(async (context, next) => { if (context.WebSockets.IsWebSocketRequest) { diff --git a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs index 8294dd6..c6831db 100644 --- a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs +++ b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs @@ -17,7 +17,6 @@ namespace Gameboard.ShogiUI.Sockets.Utilities { var file = (char)(x + A); var rank = y + 1; - Console.WriteLine($"({x},{y}) - {file}{rank}"); return $"{file}{rank}"; } diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 9d9f40f..060e229 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -12,5 +12,11 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff", + "TenantId": "common", + "Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff" + }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index a15e786..3848edc 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj index c86f2d1..d47800f 100644 --- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -8,14 +8,14 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs deleted file mode 100644 index 8541ea4..0000000 --- a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs +++ /dev/null @@ -1,76 +0,0 @@ -using AutoFixture; -using FluentAssertions; -using FluentAssertions.Execution; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Gameboard.ShogiUI.Sockets.Services.RequestValidators; -using Xunit; - -namespace Gameboard.ShogiUI.xUnitTests.RequestValidators -{ - public class MoveRequestValidatorShould - { - private readonly Fixture fixture; - private readonly MoveRequestValidator validator; - public MoveRequestValidatorShould() - { - fixture = new Fixture(); - validator = new MoveRequestValidator(); - } - - [Fact] - public void PreventInvalidPropertyCombinations() - { - // Arrange - var request = fixture.Create(); - - // Act - var results = validator.Validate(request); - - // Assert - using (new AssertionScope()) - { - results.IsValid.Should().BeFalse(); - } - } - - [Fact] - public void AllowValidPropertyCombinations() - { - // Arrange - var requestWithoutFrom = new MoveRequest() - { - Action = ClientAction.Move, - GameName = "Some game name", - Move = new Move() - { - IsPromotion = false, - PieceFromCaptured = WhichPiece.Bishop, - To = "A4" - } - }; - var requestWithoutPieceFromCaptured = new MoveRequest() - { - Action = ClientAction.Move, - GameName = "Some game name", - Move = new Move() - { - From = "A1", - IsPromotion = false, - To = "A4" - } - }; - - // Act - var results = validator.Validate(requestWithoutFrom); - var results2 = validator.Validate(requestWithoutPieceFromCaptured); - - // Assert - using (new AssertionScope()) - { - results.IsValid.Should().BeTrue(); - results2.IsValid.Should().BeTrue(); - } - } - } -} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index e5ded9f..aa3f3b4 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -72,7 +72,6 @@ namespace PathFinding var element = collection[from]; if (element == null) { - Console.WriteLine("Null element in PathEvery"); return; } foreach (var path in element.MoveSet.GetMoves()) From 20f44c8b90f2ab0cb061fe598c6a9cb9a2cc75a3 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 10 Nov 2021 18:46:29 -0600 Subject: [PATCH 19/27] checkpoint --- .../Api/GetGuestToken.cs | 12 +- .../Api/{GetGame.cs => GetSession.cs} | 6 +- .../Api/GetSessionsResponse.cs | 11 + .../Socket/JoinGame.cs | 5 +- .../Socket/Move.cs | 15 +- .../Controllers/GameController.cs | 103 +++++-- .../Controllers/SocketController.cs | 64 ++--- .../Extensions/Extensions.cs | 10 + .../ClientActionHandlers/JoinGameHandler.cs | 43 --- .../Managers/GameboardManager.cs | 45 ++-- .../Managers/SocketConnectionManager.cs | 28 +- Gameboard.ShogiUI.Sockets/Models/Session.cs | 13 +- .../Models/SessionMetadata.cs | 28 +- Gameboard.ShogiUI.Sockets/Models/User.cs | 92 ++++--- .../Models/WhichLoginPlatform.cs | 1 + .../CouchModels/BoardStateDocument.cs | 3 +- .../Repositories/CouchModels/CouchDocument.cs | 1 + .../CouchModels/CouchViewResult.cs | 28 ++ .../CouchModels/SessionDocument.cs | 20 +- .../Repositories/CouchModels/UserDocument.cs | 23 +- .../Repositories/GameboardRepository.cs | 254 ++++++++++-------- .../Services/SocketService.cs | 106 +++----- .../Services/Utility/Response.cs | 1 - .../ShogiUserClaimsTransformer.cs | 4 +- Gameboard.ShogiUI.Sockets/Startup.cs | 6 +- Gameboard.ShogiUI.Sockets/appsettings.json | 4 +- 26 files changed, 519 insertions(+), 407 deletions(-) rename Gameboard.ShogiUI.Sockets.ServiceModels/Api/{GetGame.cs => GetSession.cs} (75%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs index 41f93ec..90fc455 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs @@ -2,18 +2,16 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { - public class GetGuestToken - { - } - public class GetGuestTokenResponse { - public string PlayerName { get; } + public string UserId { get; } + public string DisplayName { get; } public Guid OneTimeToken { get; } - public GetGuestTokenResponse(string playerName, Guid token) + public GetGuestTokenResponse(string id, string displayName, Guid token) { - PlayerName = playerName; + UserId = id; + DisplayName = displayName; OneTimeToken = token; } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs similarity index 75% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs index c15dae4..d1e3cf7 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs @@ -1,13 +1,9 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { - public class GetGameResponse + public class GetSessionResponse { public Game Game { get; set; } public WhichPlayer PlayerPerspective { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs new file mode 100644 index 0000000..cc972bc --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs @@ -0,0 +1,11 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using System.Collections.ObjectModel; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api +{ + public class GetSessionsResponse + { + public Collection PlayerHasJoinedSessions { get; set; } + public Collection AllOtherSessions { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs index 48d1b34..22fac9c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs @@ -17,14 +17,15 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket public class JoinGameResponse : IResponse { public string Action { get; protected set; } - public string Error { get; set; } public string GameName { get; set; } + /// + /// The player who joined the game. + /// public string PlayerName { get; set; } public JoinGameResponse() { Action = ClientAction.JoinGame.ToString(); - Error = ""; GameName = ""; PlayerName = ""; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs index 0853998..3faf4b5 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs @@ -1,15 +1,16 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Collections.Generic; +using Gameboard.ShogiUI.Sockets.ServiceModels.Api; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { public class MoveResponse : IResponse { - public string Action { get; protected set; } - public Game Game { get; set; } - public WhichPlayer PlayerPerspective { get; set; } - public BoardState BoardState { get; set; } - public IList MoveHistory { get; set; } + public string Action { get; } + public string GameName { get; set; } + /// + /// The player that made the move. + /// + public string PlayerName { get; set; } public MoveResponse() { diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 718573f..e5ebad0 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -7,7 +7,10 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers @@ -71,10 +74,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers { var user = await gameboardManager.ReadUser(User); var session = await gameboardRepository.ReadSession(gameName); - - if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name)) + if (session == null) { - throw new UnauthorizedAccessException("User is not seated at this game."); + return NotFound(); + } + if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id)) + { + return Forbid("User is not seated at this game."); } var move = request.Move; @@ -92,15 +98,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } await communicationManager.BroadcastToPlayers(new MoveResponse { - BoardState = session.Shogi.ToServiceModel(), - Game = session.ToServiceModel(), - MoveHistory = session.Shogi.MoveHistory.Select(h => h.ToServiceModel()).ToList(), - PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2 - }, session.Player1, session.Player2); + GameName = session.Name, + PlayerName = user.Id + }, session.Player1.Id, session.Player2?.Id); return Ok(); } - throw new InvalidOperationException("Illegal move."); + return Conflict("Illegal move."); } + // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. //[Route("")] //public async Task PostSession([FromBody] PostSession request) @@ -125,8 +130,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpPost] public async Task PostSession([FromBody] PostSession request) { - var user = await gameboardManager.ReadUser(User); - var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!.Name); + var user = await ReadUserOrThrow(); + var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!); var success = await gameboardRepository.CreateSession(session); if (success) @@ -134,7 +139,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers await communicationManager.BroadcastToAll(new CreateGameResponse { Game = session.ToServiceModel(), - PlayerName = user.Name + PlayerName = user.Id + }).ContinueWith(cont => + { + if (cont.Exception != null) + { + Console.Error.WriteLine("Yep"); + } }); return Ok(); } @@ -148,29 +159,83 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpGet("{gameName}")] public async Task GetSession([FromRoute] string gameName) { - var user = await gameboardManager.ReadUser(User); + var user = await ReadUserOrThrow(); var session = await gameboardRepository.ReadSession(gameName); if (session == null) { return NotFound(); } - communicationManager.SubscribeToGame(session, user!.Name); - var response = new GetGameResponse() + communicationManager.SubscribeToGame(session, user!.Id); + var response = new GetSessionResponse() { - Game = new Models.SessionMetadata(session).ToServiceModel(), + Game = new Models.SessionMetadata(session).ToServiceModel(user), BoardState = session.Shogi.ToServiceModel(), MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(), - PlayerPerspective = user.Name == session.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2 + PlayerPerspective = user.Id == session.Player1.Id ? WhichPlayer.Player1 : WhichPlayer.Player2 }; return new JsonResult(response); } [HttpGet] - public async Task GetSessions() + public async Task GetSessions() { + var user = await ReadUserOrThrow(); var sessions = await gameboardRepository.ReadSessionMetadatas(); - return new JsonResult(sessions.Select(s => s.ToServiceModel()).ToList()); + + var sessionsJoinedByUser = sessions + .Where(s => s.IsSeated(user)) + .Select(s => s.ToServiceModel()) + .ToList(); + var sessionsNotJoinedByUser = sessions + .Where(s => !s.IsSeated(user)) + .Select(s => s.ToServiceModel()) + .ToList(); + + return new GetSessionsResponse + { + PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser), + AllOtherSessions = new Collection(sessionsNotJoinedByUser) + }; + } + + [HttpPut("{gameName}")] + public async Task PutJoinSession([FromRoute] string gameName) + { + var user = await ReadUserOrThrow(); + var session = await gameboardRepository.ReadSessionMetaData(gameName); + if (session == null) + { + return NotFound(); + } + if (session.Player2 != null) + { + return this.Conflict("This session already has two seated players and is full."); + } + + session.SetPlayer2(user); + var success = await gameboardRepository.UpdateSession(session); + if (!success) return this.Problem(detail: "Unable to update session."); + + var opponentName = user.Id == session.Player1.Id + ? session.Player2!.Id + : session.Player1.Id; + await communicationManager.BroadcastToPlayers(new JoinGameResponse + { + GameName = session.Name, + PlayerName = user.Id + }, opponentName); + return Ok(); + } + + private async Task ReadUserOrThrow() + { + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + throw new UnauthorizedAccessException("Unknown user claims."); + } + return user; } } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 0b9e941..5b99eb3 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -54,24 +54,21 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [HttpGet("Token")] public async Task GetToken() { - var identityId = User.UserId(); - if (string.IsNullOrWhiteSpace(identityId)) + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + if (await gameboardManager.CreateUser(User)) + { + user = await gameboardManager.ReadUser(User); + } + } + + if (user == null) { return Unauthorized(); } - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - user = new User(identityId); - var success = await gameboardRepository.CreateUser(user); - if (!success) - { - return Unauthorized(); - } - } - - var token = tokenCache.GenerateToken(user.Name); + var token = tokenCache.GenerateToken(user.Id); return new JsonResult(new GetTokenResponse(token)); } @@ -79,35 +76,28 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [AllowAnonymous] public async Task GetGuestToken() { - if (Guid.TryParse(User.UserId(), out Guid webSessionId)) + var user = await gameboardManager.ReadUser(User); + if (user == null) { - var user = await gameboardRepository.ReadGuestUser(webSessionId); - if (user != null) + // Create a guest user. + var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString()); + var success = await gameboardRepository.CreateUser(newUser); + if (!success) { - var token = tokenCache.GenerateToken(webSessionId.ToString()); - return new JsonResult(new GetGuestTokenResponse(user.Name, token)); + return Conflict(); } - } - else - { - // Setup a guest user. - var newSessionId = Guid.NewGuid(); - var user = new User(Guid.NewGuid().ToString(), newSessionId); - if (await gameboardRepository.CreateUser(user)) - { - var identity = user.CreateGuestUserIdentity(); - await this.HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - authenticationProps - ); - var token = tokenCache.GenerateToken(newSessionId.ToString()); - return new JsonResult(new GetGuestTokenResponse(user.Name, token)); - } + var identity = newUser.CreateClaimsIdentity(); + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + authenticationProps + ); + user = newUser; } - return Unauthorized(); + var token = tokenCache.GenerateToken(user.Id.ToString()); + return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token)); } } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs index 30517ad..ca6dc5c 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs @@ -10,9 +10,19 @@ namespace Gameboard.ShogiUI.Sockets.Extensions return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; } + public static string? DisplayName(this ClaimsPrincipal self) + { + return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value; + } + public static bool IsGuest(this ClaimsPrincipal self) { return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest"); } + + public static string ToCamelCase(this string self) + { + return char.ToLowerInvariant(self[0]) + self[1..]; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs deleted file mode 100644 index 52415ee..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IJoinGameHandler - { - Task Handle(JoinGameRequest request, string userName); - } - public class JoinGameHandler : IJoinGameHandler - { - private readonly IGameboardManager gameboardManager; - private readonly ISocketConnectionManager connectionManager; - public JoinGameHandler( - ISocketConnectionManager communicationManager, - IGameboardManager gameboardManager) - { - this.gameboardManager = gameboardManager; - this.connectionManager = communicationManager; - } - - public async Task Handle(JoinGameRequest request, string userName) - { - var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName); - - var response = new JoinGameResponse() - { - PlayerName = userName, - GameName = request.GameName - }; - if (joinSucceeded) - { - await connectionManager.BroadcastToAll(response); - } - else - { - response.Error = "Game is full or does not exist."; - await connectionManager.BroadcastToPlayers(response, userName); - } - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index e4c4aa0..5cc9f04 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -9,9 +9,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface IGameboardManager { - Task IsPlayer1(string sessionName, string playerName); - Task AssignPlayer2ToSession(string sessionName, string userName); + Task AssignPlayer2ToSession(string sessionName, User user); Task ReadUser(ClaimsPrincipal user); + Task CreateUser(ClaimsPrincipal user); } public class GameboardManager : IGameboardManager @@ -23,14 +23,25 @@ namespace Gameboard.ShogiUI.Sockets.Managers this.repository = repository; } - public Task ReadUser(ClaimsPrincipal user) + public Task CreateUser(ClaimsPrincipal principal) { - var userId = user.UserId(); - if (user.IsGuest() && Guid.TryParse(userId, out var webSessionId)) + var id = principal.UserId(); + if (string.IsNullOrEmpty(id)) { - return repository.ReadGuestUser(webSessionId); + return Task.FromResult(false); } - else if (!string.IsNullOrEmpty(userId)) + + var user = principal.IsGuest() + ? User.CreateGuestUser(id) + : User.CreateMsalUser(id); + + return repository.CreateUser(user); + } + + public Task ReadUser(ClaimsPrincipal principal) + { + var userId = principal.UserId(); + if (!string.IsNullOrEmpty(userId)) { return repository.ReadUser(userId); } @@ -38,12 +49,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers return Task.FromResult(null); } - public async Task IsPlayer1(string sessionName, string playerName) - { - //var session = await repository.GetGame(sessionName); - //return session?.Player1 == playerName; - return true; - } public async Task CreateJoinCode(string sessionName, string playerName) { @@ -55,19 +60,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers return string.Empty; } - public async Task AssignPlayer2ToSession(string sessionName, string userName) + public async Task AssignPlayer2ToSession(string sessionName, User user) { - var isSuccess = false; var session = await repository.ReadSessionMetaData(sessionName); - if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2)) + if (session != null && !session.IsPrivate && session.Player2 == null) { - session.SetPlayer2(userName); - if (await repository.UpdateSession(session)) - { - isSuccess = true; - } + session.SetPlayer2(user); + return await repository.UpdateSession(session); } - return isSuccess; + return false; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 720381d..10fa790 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -3,6 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.WebSockets; @@ -106,9 +107,32 @@ namespace Gameboard.ShogiUI.Sockets.Managers foreach (var kvp in connections) { var socket = kvp.Value; - tasks.Add(socket.SendTextAsync(message)); + try + { + + tasks.Add(socket.SendTextAsync(message)); + } + catch (WebSocketException webSocketException) + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + UnsubscribeFromBroadcastAndGames(kvp.Key); + } + catch (Exception exception) + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + UnsubscribeFromBroadcastAndGames(kvp.Key); + } } - return Task.WhenAll(tasks); + try + { + var task = Task.WhenAll(tasks); + return task; + } + catch (Exception e) + { + Console.WriteLine("Yo"); + } + return Task.FromResult(0); } //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index 67240e0..cd5c5dd 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -10,13 +10,14 @@ namespace Gameboard.ShogiUI.Sockets.Models // TODO: Separate subscriptions to the Session from the Session. [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } public string Name { get; } - public string Player1 { get; } - public string? Player2 { get; private set; } + public User Player1 { get; } + public User? Player2 { get; private set; } public bool IsPrivate { get; } + // TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied. public Shogi Shogi { get; } - public Session(string name, bool isPrivate, Shogi shogi, string player1, string? player2 = null) + public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null) { Subscriptions = new ConcurrentDictionary(); @@ -27,11 +28,11 @@ namespace Gameboard.ShogiUI.Sockets.Models Shogi = shogi; } - public void SetPlayer2(string userName) + public void SetPlayer2(User user) { - Player2 = userName; + Player2 = user; } - public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1, Player2 = Player2 }; + public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1.DisplayName, Player2 = Player2?.DisplayName }; } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index d46b8af..ac35d6f 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -6,11 +6,11 @@ public class SessionMetadata { public string Name { get; } - public string Player1 { get; } - public string? Player2 { get; private set; } + public User Player1 { get; } + public User? Player2 { get; private set; } public bool IsPrivate { get; } - public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null) + public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null) { Name = name; IsPrivate = isPrivate; @@ -25,11 +25,27 @@ Player2 = sessionModel.Player2; } - public void SetPlayer2(string playerName) + public void SetPlayer2(User user) { - Player2 = playerName; + Player2 = user; } - public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1, Player2); + public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id; + + public ServiceModels.Types.Game ToServiceModel(User? user = null) + { + // TODO: Find a better way for the UI to know whether or not they are seated at a given game than client-side ID matching. + var player1 = Player1.DisplayName; + var player2 = Player2?.DisplayName; + if (user != null) + { + if (user.Id == Player1.Id) player1 = Player1.Id; + if (Player2 != null && user.Id == Player2.Id) + { + player2 = Player2.DisplayName; + } + } + return new(Name, player1, player2); + } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index acfd4a9..60f5b2b 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -1,57 +1,79 @@ -using Microsoft.AspNetCore.Authentication.Cookies; +using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Security.Claims; namespace Gameboard.ShogiUI.Sockets.Models { public class User { - public string Name { get; } - public Guid? WebSessionId { get; } - - public bool IsGuest => WebSessionId.HasValue; - - public User(string name) + public static readonly ReadOnlyCollection Adjectives = new(new[] { + "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" + }); + public static readonly ReadOnlyCollection Subjects = new(new[] { + "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato" + }); + public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft); + public static User CreateGuestUser(string id) { - Name = name; + var random = new Random(); + // Adjective + var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count); + var adj = Adjectives[index]; + // Subject + index = (int)Math.Floor(random.NextDouble() * Subjects.Count); + var subj = Subjects[index]; + + return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest); } - /// - /// Constructor for guest user. - /// - public User(string name, Guid webSessionId) + public string Id { get; } + public string DisplayName { get; } + + public WhichLoginPlatform LoginPlatform { get; } + + public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; + + public User(string id, string displayName, WhichLoginPlatform platform) { - Name = name; - WebSessionId = webSessionId; + Id = id; + DisplayName = displayName; + LoginPlatform = platform; } - public ClaimsIdentity CreateMsalUserIdentity() + public User(UserDocument document) { - var claims = new List() + Id = document.Id; + DisplayName = document.DisplayName; + LoginPlatform = document.Platform; + } + + public ClaimsIdentity CreateClaimsIdentity() + { + if (LoginPlatform == WhichLoginPlatform.Guest) { - new Claim(ClaimTypes.NameIdentifier, Name), - new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. - }; - return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); - } - - public ClaimsIdentity CreateGuestUserIdentity() - { - // TODO: Make this method static and factory-like. - if (!WebSessionId.HasValue) - { - throw new InvalidOperationException("Cannot create guest identity without a session identifier."); + var claims = new List(4) + { + new Claim(ClaimTypes.NameIdentifier, Id), + new Claim(ClaimTypes.Name, DisplayName), + new Claim(ClaimTypes.Role, "Guest"), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + }; + return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); } - - var claims = new List() + else { - new Claim(ClaimTypes.NameIdentifier, WebSessionId.Value.ToString()), - new Claim(ClaimTypes.Role, "Guest"), - new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. - }; - return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var claims = new List(3) + { + new Claim(ClaimTypes.NameIdentifier, Id), + new Claim(ClaimTypes.Name, DisplayName), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + }; + return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); + } } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs index 5923404..5d2378e 100644 --- a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs +++ b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs @@ -2,6 +2,7 @@ { public enum WhichLoginPlatform { + Unknown, Microsoft, Guest } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs index 08de6a5..ecd0450 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -1,5 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Gameboard.ShogiUI.Sockets.Utilities; +using Gameboard.ShogiUI.Sockets.Utilities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs index 4549de0..29d19c3 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -6,6 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public abstract class CouchDocument { [JsonProperty("_id")] public string Id { get; set; } + [JsonProperty("_rev")] public string? RevisionId { get; set; } public WhichDocumentType DocumentType { get; } public DateTimeOffset CreatedDate { get; set; } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs new file mode 100644 index 0000000..ca98d2e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs @@ -0,0 +1,28 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class CouchViewResult where T : class + { + public int total_rows; + public int offset; + public CouchViewResultRow[] rows; + + public CouchViewResult() + { + rows = Array.Empty>(); + } + } + + public class CouchViewResultRow + { + public string id; + public T doc; + + public CouchViewResultRow() + { + id = string.Empty; + doc = default!; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs index 59e8bf8..6db129e 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -5,8 +5,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public class SessionDocument : CouchDocument { public string Name { get; set; } - public string Player1 { get; set; } - public string? Player2 { get; set; } + public string Player1Id { get; set; } + public string? Player2Id { get; set; } public bool IsPrivate { get; set; } public IList History { get; set; } @@ -16,8 +16,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public SessionDocument() : base(WhichDocumentType.Session) { Name = string.Empty; - Player1 = string.Empty; - Player2 = string.Empty; + Player1Id = string.Empty; + Player2Id = string.Empty; History = new List(0); } @@ -25,8 +25,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels : base(session.Name, WhichDocumentType.Session) { Name = session.Name; - Player1 = session.Player1; - Player2 = session.Player2; + Player1Id = session.Player1.Id; + Player2Id = session.Player2?.Id; IsPrivate = session.IsPrivate; History = new List(0); } @@ -35,14 +35,10 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels : base(sessionMetaData.Name, WhichDocumentType.Session) { Name = sessionMetaData.Name; - Player1 = sessionMetaData.Player1; - Player2 = sessionMetaData.Player2; + Player1Id = sessionMetaData.Player1.Id; + Player2Id = sessionMetaData.Player2?.Id; IsPrivate = sessionMetaData.IsPrivate; History = new List(0); } - - public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2); - - public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index c41b60b..940a8bd 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,34 +1,27 @@ using Gameboard.ShogiUI.Sockets.Models; -using System; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public class UserDocument : CouchDocument { - - public string Name { get; set; } + public string DisplayName { get; set; } public WhichLoginPlatform Platform { get; set; } - /// - /// The browser session ID saved via Set-Cookie headers. - /// Only used with guest accounts. - /// - public Guid? WebSessionId { get; set; } /// /// Constructor for JSON deserializing. /// public UserDocument() : base(WhichDocumentType.User) { - Name = string.Empty; + DisplayName = string.Empty; } - public UserDocument(string name, Guid? webSessionId = null) : base($"org.couchdb.user:{name}", WhichDocumentType.User) + public UserDocument( + string id, + string displayName, + WhichLoginPlatform platform) : base(id, WhichDocumentType.User) { - Name = name; - WebSessionId = webSessionId; - Platform = WebSessionId.HasValue - ? WhichLoginPlatform.Guest - : WhichLoginPlatform.Microsoft; + DisplayName = displayName; + Platform = platform; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 89ee28c..3489693 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -1,8 +1,12 @@ -using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Text; @@ -16,10 +20,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories Task CreateBoardState(Models.Session session); Task CreateSession(Models.SessionMetadata session); Task CreateUser(Models.User user); - Task> ReadSessionMetadatas(); - Task ReadGuestUser(Guid webSessionId); + Task> ReadSessionMetadatas(); Task ReadSession(string name); - Task ReadShogi(string name); Task UpdateSession(Models.SessionMetadata session); Task ReadSessionMetaData(string name); Task ReadUser(string userName); @@ -27,6 +29,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public class GameboardRepository : IGameboardRepository { + /// + /// Returns session, board state, and user documents, grouped by session. + /// + private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; + /// + /// Returns session and user documents, grouped by session. + /// + private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; + private static readonly string View_User = "_design/user/_view/user"; private const string ApplicationJson = "application/json"; private readonly HttpClient client; private readonly ILogger logger; @@ -37,86 +48,123 @@ namespace Gameboard.ShogiUI.Sockets.Repositories this.logger = logger; } - public async Task> ReadSessionMetadatas() + public async Task> ReadSessionMetadatas() { - var selector = new Dictionary(2) - { - [nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session - }; - var q = new { Selector = selector }; - var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync("_find", content); + var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); + var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); var responseContent = await response.Content.ReadAsStringAsync(); - var results = JsonConvert.DeserializeObject>(responseContent); - if (results != null) + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null) { - return results - .docs - .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2)) - .ToList(); + var groupedBySession = result.rows.GroupBy(row => row.id); + var sessions = new List(result.total_rows / 3); + foreach (var group in groupedBySession) + { + /** + * A group contains 3 elements. + * 1) The session metadata. + * 2) User document of Player1. + * 3) User document of Player2. + */ + var session = group.FirstOrDefault()?.doc.ToObject(); + var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); + var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); + if (session != null && player1Doc != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2)); + } + } + return new Collection(sessions); } - - return new List(0); + return new Collection(Array.Empty()); } public async Task ReadSession(string name) { - var readShogiTask = ReadShogi(name); - var response = await client.GetAsync(name); - var responseContent = await response.Content.ReadAsStringAsync(); - var couchModel = JsonConvert.DeserializeObject(responseContent); - var shogi = await readShogiTask; - if (shogi == null) + var queryParams = new QueryBuilder { - return null; + { "include_docs", "true" }, + { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + }.ToQueryString(); + var query = $"{View_SessionWithBoardState}{queryParams}"; + logger.LogInformation("ReadSession() query: {query}", query); + var response = await client.GetAsync(query); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 2) + { + var group = result.rows; + /** + * A group contains 3 type of elements. + * 1) The session metadata. + * 2) User documents of Player1 and Player2. + * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :( + * 3) BoardState + */ + var session = group[0].doc.ToObject(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : null; + var moves = group + .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session. + // TODO: Deserialize just the Move property. + .Select(row => row.doc.ToObject()) + .Select(boardState => + { + var move = boardState!.Move!; + return move.PieceFromHand.HasValue + ? new Models.Move(move.PieceFromHand.Value, move.To) + : new Models.Move(move.From!, move.To, move.IsPromotion); + }) + .ToList(); + + var shogi = new Models.Shogi(moves); + if (session != null && player1Doc != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2); + } } - return couchModel.ToDomainModel(shogi); + return null; } public async Task ReadSessionMetaData(string name) { - var response = await client.GetAsync(name); + var queryParams = new QueryBuilder + { + { "include_docs", "true" }, + { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + }.ToQueryString(); + var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); var responseContent = await response.Content.ReadAsStringAsync(); - var couchModel = JsonConvert.DeserializeObject(responseContent); - return couchModel.ToDomainModel(); - } - - public async Task ReadShogi(string name) - { - var selector = new Dictionary(2) + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 2) { - [nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState, - [nameof(BoardStateDocument.Name)] = name - }; - var sort = new Dictionary(1) - { - [nameof(BoardStateDocument.CreatedDate)] = "asc" - }; - var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } }); - - var content = new StringContent(query, Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync("_find", content); - var responseContent = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query); - return null; + var group = result.rows; + /** + * A group contains 3 elements. + * 1) The session metadata. + * 2) User document of Player1. + * 3) User document of Player2. + */ + var session = group[0].doc.ToObject(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : null; + if (session != null && player1Doc != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2); + } } - var boardStates = JsonConvert - .DeserializeObject>(responseContent) - .docs; - if (boardStates.Length == 0) return null; - - // Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session. - var moves = boardStates.Skip(1).Select(couchModel => - { - var move = couchModel.Move; - Models.Move model = move!.PieceFromHand.HasValue - ? new Models.Move(move.PieceFromHand.Value, move.To) - : new Models.Move(move.From!, move.To, move.IsPromotion); - return model; - }).ToList(); - return new Models.Shogi(moves); + return null; } /// @@ -149,7 +197,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task UpdateSession(Models.SessionMetadata session) { - var couchModel = new SessionDocument(session); + // GET existing session to get revisionId. + var readResponse = await client.GetAsync(session.Name); + if (!readResponse.IsSuccessStatusCode) return false; + var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); + + // PUT the document with the revisionId. + var couchModel = new SessionDocument(session) + { + RevisionId = sessionDocument?.RevisionId + }; var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PutAsync(couchModel.Id, content); return response.IsSuccessStatusCode; @@ -205,66 +262,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories return string.Empty; } - public async Task ReadUser(string userName) + public async Task ReadUser(string id) { - try + var queryParams = new QueryBuilder { - var document = new UserDocument(userName); - var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id)); - var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id)); - var response2 = await client.GetAsync(uri); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - var user = JsonConvert.DeserializeObject(responseContent); + { "include_docs", "true" }, + { "key", JsonConvert.SerializeObject(id) }, + }.ToQueryString(); + var response = await client.GetAsync($"{View_User}{queryParams}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 0) + { + return new Models.User(result.rows[0].doc); + } - return new Models.User(user.Name); - } - } - catch (Exception e) - { - Console.WriteLine(e); - } return null; } public async Task CreateUser(Models.User user) { - var couchModel = new UserDocument(user.Name, user.WebSessionId); + var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); return response.IsSuccessStatusCode; } - public async Task ReadGuestUser(Guid webSessionId) - { - var selector = new Dictionary(2) - { - [nameof(UserDocument.DocumentType)] = WhichDocumentType.User, - [nameof(UserDocument.WebSessionId)] = webSessionId.ToString() - }; - var query = JsonConvert.SerializeObject(new { selector, limit = 1 }); - var content = new StringContent(query, Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync("_find", content); - var responseContent = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadGuestUser), responseContent, query); - return null; - } - - var result = JsonConvert.DeserializeObject>(responseContent); - if (!string.IsNullOrWhiteSpace(result.warning)) - { - logger.LogError(new InvalidOperationException(result.warning), result.warning); - return null; - } - var userDocument = result.docs.SingleOrDefault(); - if (userDocument != null) - { - return new Models.User(userDocument.Name); - } - return null; - } } } diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs index 30fff7f..0418950 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -1,5 +1,4 @@ using FluentValidation; -using Gameboard.ShogiUI.Sockets.Controllers; using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; @@ -34,7 +33,6 @@ namespace Gameboard.ShogiUI.Sockets.Services private readonly IGameboardManager gameboardManager; private readonly ISocketTokenCache tokenManager; private readonly IJoinByCodeHandler joinByCodeHandler; - private readonly IJoinGameHandler joinGameHandler; private readonly IValidator joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; @@ -45,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets.Services IGameboardManager gameboardManager, ISocketTokenCache tokenManager, IJoinByCodeHandler joinByCodeHandler, - IJoinGameHandler joinGameHandler, IValidator joinByCodeRequestValidator, IValidator joinGameRequestValidator ) : base() @@ -56,85 +53,68 @@ namespace Gameboard.ShogiUI.Sockets.Services this.gameboardManager = gameboardManager; this.tokenManager = tokenManager; this.joinByCodeHandler = joinByCodeHandler; - this.joinGameHandler = joinGameHandler; this.joinByCodeRequestValidator = joinByCodeRequestValidator; this.joinGameRequestValidator = joinGameRequestValidator; } public async Task HandleSocketRequest(HttpContext context) { - string? userName = null; - var user = await gameboardManager.ReadUser(context.User); - if (user?.WebSessionId != null) + if (!context.Request.Query.Keys.Contains("token")) { - // Guest account - userName = tokenManager.GetUsername(user.WebSessionId.Value); - } - else if (context.Request.Query.Keys.Contains("token")) - { - // Microsoft account - var token = Guid.Parse(context.Request.Query["token"][0]); - userName = tokenManager.GetUsername(token); + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; } + var token = Guid.Parse(context.Request.Query["token"][0]); + var userName = tokenManager.GetUsername(token); if (string.IsNullOrEmpty(userName)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } - else - { - var socket = await context.WebSockets.AcceptWebSocketAsync(); + var socket = await context.WebSockets.AcceptWebSocketAsync(); - communicationManager.SubscribeToBroadcast(socket, userName); - while (socket.State == WebSocketState.Open) + communicationManager.SubscribeToBroadcast(socket, userName); + while (socket.State == WebSocketState.Open) + { + try { - try + var message = await socket.ReceiveTextAsync(); + if (string.IsNullOrWhiteSpace(message)) continue; + logger.LogInformation("Request \n{0}\n", message); + var request = JsonConvert.DeserializeObject(message); + if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action)) { - 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)) - { - await socket.SendTextAsync("Error: Action not recognized."); - continue; - } - switch (request.Action) - { - case ClientAction.JoinGame: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) - { - await joinGameHandler.Handle(req, userName); - } - break; - } - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(message); - if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) - { - await joinByCodeHandler.Handle(req, userName); - } - break; - } - } + await socket.SendTextAsync("Error: Action not recognized."); + continue; } - catch (OperationCanceledException ex) + switch (request.Action) { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) + { + await joinByCodeHandler.Handle(req, userName); + } + break; + } + default: + await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work."); + break; } } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } communicationManager.UnsubscribeFromBroadcastAndGames(userName); - return; } } @@ -144,11 +124,7 @@ namespace Gameboard.ShogiUI.Sockets.Services if (!results.IsValid) { var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage)); - var message = JsonConvert.SerializeObject(new Response - { - Error = errors - }); - await socket.SendTextAsync(message); + await socket.SendTextAsync(errors); } return results.IsValid; } diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs index f33af9f..067d374 100644 --- a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs +++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs @@ -5,6 +5,5 @@ namespace Gameboard.ShogiUI.Sockets.Services.Utility public class Response : IResponse { public string Action { get; set; } - public string Error { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs index 1cbc725..a04a25b 100644 --- a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs +++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs @@ -30,14 +30,14 @@ namespace Gameboard.ShogiUI.Sockets var user = await gameboardRepository.ReadUser(nameClaim.Value); if (user == null) { - var newUser = new Models.User(nameClaim.Value); + var newUser = Models.User.CreateMsalUser(nameClaim.Value); var success = await gameboardRepository.CreateUser(newUser); if (success) user = newUser; } if (user != null) { - return new ClaimsPrincipal(user.CreateMsalUserIdentity()); + return new ClaimsPrincipal(user.CreateClaimsIdentity()); } } return principal; diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 92f88e5..5650206 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -13,7 +13,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Http; using Microsoft.Identity.Client; using Microsoft.Identity.Web; using Newtonsoft.Json; @@ -41,7 +43,6 @@ namespace Gameboard.ShogiUI.Sockets public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -109,6 +110,9 @@ namespace Gameboard.ShogiUI.Sockets document.Info.Title = "Gameboard.ShogiUI.Sockets"; }; }); + + // Remove default HttpClient logging. + services.RemoveAll(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 060e229..7abd699 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -7,9 +7,9 @@ }, "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Error" } }, "AzureAd": { From cf3fbbbc1d6b2b7a69da383516ad2638c9a24840 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 14 Nov 2021 11:30:59 -0600 Subject: [PATCH 20/27] nuget updates remove unused CouchDB project upgrade to .net 6 --- Benchmarking/Benchmarking.csproj | 2 +- CouchDB/CouchDB.csproj | 13 ------------- CouchDB/CouchDocument.cs | 16 ---------------- ...board.ShogiUI.Sockets.ServiceModels.csproj | 2 +- Gameboard.ShogiUI.Sockets.sln | 12 +++--------- .../Gameboard.ShogiUI.Sockets.csproj | 19 +++++++++---------- .../Gameboard.ShogiUI.UnitTests.csproj | 6 +++--- .../Gameboard.ShogiUI.xUnitTests.csproj | 6 +++--- PathFinding/PathFinding.csproj | 2 +- 9 files changed, 21 insertions(+), 57 deletions(-) delete mode 100644 CouchDB/CouchDB.csproj delete mode 100644 CouchDB/CouchDocument.cs diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index 98fe81e..a2ca11e 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true Exe diff --git a/CouchDB/CouchDB.csproj b/CouchDB/CouchDB.csproj deleted file mode 100644 index 168228b..0000000 --- a/CouchDB/CouchDB.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net5.0 - - - - - - - - - diff --git a/CouchDB/CouchDocument.cs b/CouchDB/CouchDocument.cs deleted file mode 100644 index 8f29f6c..0000000 --- a/CouchDB/CouchDocument.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace CouchDB -{ - public class CouchDocument - { - public readonly string _id; - public readonly string type; - public readonly T model; - - public CouchDocument(string id, T model) - { - _id = id; - this.model = model; - type = nameof(T); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj index 4466c3d..ec1c55c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true 5 enable diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index afad198..3ddc148 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}" EndProject @@ -15,9 +15,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarkin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,10 +43,6 @@ Global {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU - {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index bd64225..bf7b641 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -1,26 +1,25 @@  - net5.0 + net6.0 true 5 enable - - - - - - - + + + + + + + - + - diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index 3848edc..c11752d 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -1,13 +1,13 @@ - net5.0 + net6.0 - - + + diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj index d47800f..040179a 100644 --- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -1,15 +1,15 @@ - net5.0 + net6.0 false - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index 68ec955..6493b62 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 true 5 enable From 433ab2772aa6b73fa0dc805e994018f337b19848 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 21 Nov 2021 10:07:35 -0600 Subject: [PATCH 21/27] checkpoint --- .../Api/GetSession.cs | 5 +- ...{GetSessionsResponse.cs => GetSessions.cs} | 0 .../Socket/Move.cs | 3 +- .../Types/BoardState.cs | 4 +- .../Types/Game.cs | 59 +- .../Types/Piece.cs | 2 +- .../Types/User.cs | 9 + .../Types/WhichPerspective.cs | 9 + .../Types/WhichPlayer.cs | 8 - .../Controllers/GameController.cs | 415 ++++---- .../Controllers/SocketController.cs | 166 ++-- .../Extensions/LogMiddleware.cs | 71 +- .../Extensions/ModelExtensions.cs | 3 +- .../Managers/SocketConnectionManager.cs | 273 +++--- Gameboard.ShogiUI.Sockets/Models/Piece.cs | 12 +- Gameboard.ShogiUI.Sockets/Models/Session.cs | 50 +- .../Models/SessionMetadata.cs | 74 +- Gameboard.ShogiUI.Sockets/Models/Shogi.cs | 829 ++++++++-------- Gameboard.ShogiUI.Sockets/Models/User.cs | 124 +-- .../Repositories/CouchModels/Piece.cs | 2 +- .../ShogiUserClaimsTransformer.cs | 3 - .../Rules/ShogiBoardShould.cs | 2 +- Gameboard.ShogiUI.xUnitTests/ShogiShould.cs | 920 +++++++++--------- 23 files changed, 1556 insertions(+), 1487 deletions(-) rename Gameboard.ShogiUI.Sockets.ServiceModels/Api/{GetSessionsResponse.cs => GetSessions.cs} (100%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs index d1e3cf7..8fdbfe4 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs @@ -6,7 +6,10 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api public class GetSessionResponse { public Game Game { get; set; } - public WhichPlayer PlayerPerspective { get; set; } + /// + /// The perspective on the game of the requesting user. + /// + public WhichPerspective PlayerPerspective { get; set; } public BoardState BoardState { get; set; } public IList MoveHistory { get; set; } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs similarity index 100% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessionsResponse.cs rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs index 3faf4b5..0b19a44 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs @@ -1,5 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Api; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs index 398ba4a..b6d0a98 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs @@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types public Dictionary Board { get; set; } = new Dictionary(); public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); - public WhichPlayer? PlayerInCheck { get; set; } - public WhichPlayer WhoseTurn { get; set; } + public WhichPerspective? PlayerInCheck { get; set; } + public WhichPerspective WhoseTurn { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs index aa489ea..5b70ad8 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs @@ -2,32 +2,37 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { - public class Game - { - public string Player1 { get; set; } = string.Empty; - public string? Player2 { get; set; } = string.Empty; - public string GameName { get; set; } = string.Empty; - /// - /// Players[0] is the session owner, Players[1] is the other person. - /// - public IReadOnlyList Players - { - get - { - var list = new List(2) { Player1 }; - if (!string.IsNullOrEmpty(Player2)) list.Add(Player2); - return list; - } - } + public class Game + { + public string Player1 { get; set; } + public string? Player2 { get; set; } + public string GameName { get; set; } = string.Empty; - public Game() - { - } - public Game(string gameName, string player1, string? player2 = null) - { - GameName = gameName; - Player1 = player1; - Player2 = player2; - } - } + /// + /// Players[0] is the session owner, Players[1] is the other person. + /// + public IReadOnlyList Players + { + get + { + var list = new List(2) { Player1 }; + if (!string.IsNullOrEmpty(Player2)) list.Add(Player2); + return list; + } + } + + /// + /// Constructor for serialization. + /// + public Game() + { + } + + public Game(string gameName, string player1, string? player2 = null) + { + GameName = gameName; + Player1 = player1; + Player2 = player2; + } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs index 1c0ee78..8e28d04 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs @@ -4,6 +4,6 @@ { public bool IsPromoted { get; set; } public WhichPiece WhichPiece { get; set; } - public WhichPlayer Owner { get; set; } + public WhichPerspective Owner { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs new file mode 100644 index 0000000..8e9a72a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs @@ -0,0 +1,9 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +{ + public class User + { + public string Id { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs new file mode 100644 index 0000000..cf8a4c4 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs @@ -0,0 +1,9 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +{ + public enum WhichPerspective + { + Player1, + Player2, + Spectator + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs deleted file mode 100644 index 2ce7270..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index e5ebad0..db5f8b9 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -16,226 +16,239 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers { - [ApiController] - [Route("[controller]")] - [Authorize(Roles = "Shogi")] - public class GameController : ControllerBase - { - private readonly IGameboardManager gameboardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketConnectionManager communicationManager; + [ApiController] + [Route("[controller]")] + [Authorize(Roles = "Shogi")] + public class GameController : ControllerBase + { + private readonly IGameboardManager gameboardManager; + private readonly IGameboardRepository gameboardRepository; + private readonly ISocketConnectionManager communicationManager; - public GameController( - IGameboardRepository repository, - IGameboardManager manager, - ISocketConnectionManager communicationManager) - { - gameboardManager = manager; - gameboardRepository = repository; - this.communicationManager = communicationManager; - } + public GameController( + IGameboardRepository repository, + IGameboardManager manager, + ISocketConnectionManager communicationManager) + { + gameboardManager = manager; + gameboardRepository = repository; + this.communicationManager = communicationManager; + } - [HttpPost("JoinCode")] - public async Task PostGameInvitation([FromBody] PostGameInvitation request) - { + [HttpPost("JoinCode")] + public async Task PostGameInvitation([FromBody] PostGameInvitation request) + { - //var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName); - //if (isPlayer1) - //{ - // var code = await gameboardRepository.PostJoinCode(request.SessionName, userName); - // return new CreatedResult("", new PostGameInvitationResponse(code)); - //} - //else - //{ - return new UnauthorizedResult(); - //} - } + //var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName); + //if (isPlayer1) + //{ + // var code = await gameboardRepository.PostJoinCode(request.SessionName, userName); + // return new CreatedResult("", new PostGameInvitationResponse(code)); + //} + //else + //{ + return new UnauthorizedResult(); + //} + } - [AllowAnonymous] - [HttpPost("GuestJoinCode")] - public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) - { + [AllowAnonymous] + [HttpPost("GuestJoinCode")] + public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) + { - //var isGuest = gameboardManager.IsGuest(request.GuestId); - //var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId); - //if (isGuest && await isPlayer1) - //{ - // var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId); - // return new CreatedResult("", new PostGameInvitationResponse(code)); - //} - //else - //{ - return new UnauthorizedResult(); - //} - } + //var isGuest = gameboardManager.IsGuest(request.GuestId); + //var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId); + //if (isGuest && await isPlayer1) + //{ + // var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId); + // return new CreatedResult("", new PostGameInvitationResponse(code)); + //} + //else + //{ + return new UnauthorizedResult(); + //} + } - [HttpPost("{gameName}/Move")] - public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request) - { - var user = await gameboardManager.ReadUser(User); - var session = await gameboardRepository.ReadSession(gameName); - if (session == null) - { - return NotFound(); - } - if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id)) - { - return Forbid("User is not seated at this game."); - } + [HttpPost("{gameName}/Move")] + public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request) + { + var user = await gameboardManager.ReadUser(User); + var session = await gameboardRepository.ReadSession(gameName); + if (session == null) + { + return NotFound(); + } + if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id)) + { + return Forbid("User is not seated at this game."); + } - var move = request.Move; - var moveModel = move.PieceFromCaptured.HasValue - ? new Models.Move(move.PieceFromCaptured.Value, move.To, move.IsPromotion) - : new Models.Move(move.From!, move.To, move.IsPromotion); - var moveSuccess = session.Shogi.Move(moveModel); + var move = request.Move; + var moveModel = move.PieceFromCaptured.HasValue + ? new Models.Move(move.PieceFromCaptured.Value, move.To, move.IsPromotion) + : new Models.Move(move.From!, move.To, move.IsPromotion); + var moveSuccess = session.Shogi.Move(moveModel); - if (moveSuccess) - { - var createSuccess = await gameboardRepository.CreateBoardState(session); - if (!createSuccess) - { - throw new ApplicationException("Unable to persist board state."); - } - await communicationManager.BroadcastToPlayers(new MoveResponse - { - GameName = session.Name, - PlayerName = user.Id - }, session.Player1.Id, session.Player2?.Id); - return Ok(); - } - return Conflict("Illegal move."); - } + if (moveSuccess) + { + var createSuccess = await gameboardRepository.CreateBoardState(session); + if (!createSuccess) + { + throw new ApplicationException("Unable to persist board state."); + } + await communicationManager.BroadcastToPlayers(new MoveResponse + { + GameName = session.Name, + PlayerName = user.Id + }, session.Player1.Id, session.Player2?.Id); + return Ok(); + } + return Conflict("Illegal move."); + } - // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. - //[Route("")] - //public async Task PostSession([FromBody] PostSession request) - //{ - // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); - // var success = await repository.CreateSession(model); - // if (success) - // { - // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame) - // { - // Game = model.ToServiceModel(), - // PlayerName = - // } - // var task = request.IsPrivate - // ? communicationManager.BroadcastToPlayers(response, userName) - // : communicationManager.BroadcastToAll(response); - // return new CreatedResult("", null); - // } - // return new ConflictResult(); - //} + // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. + //[Route("")] + //public async Task PostSession([FromBody] PostSession request) + //{ + // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); + // var success = await repository.CreateSession(model); + // if (success) + // { + // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame) + // { + // Game = model.ToServiceModel(), + // PlayerName = + // } + // var task = request.IsPrivate + // ? communicationManager.BroadcastToPlayers(response, userName) + // : communicationManager.BroadcastToAll(response); + // return new CreatedResult("", null); + // } + // return new ConflictResult(); + //} - [HttpPost] - public async Task PostSession([FromBody] PostSession request) - { - var user = await ReadUserOrThrow(); - var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!); - var success = await gameboardRepository.CreateSession(session); + [HttpPost] + public async Task PostSession([FromBody] PostSession request) + { + var user = await ReadUserOrThrow(); + var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!); + var success = await gameboardRepository.CreateSession(session); - if (success) - { - await communicationManager.BroadcastToAll(new CreateGameResponse - { - Game = session.ToServiceModel(), - PlayerName = user.Id - }).ContinueWith(cont => - { - if (cont.Exception != null) - { - Console.Error.WriteLine("Yep"); - } - }); - return Ok(); - } - return Conflict(); + if (success) + { + try + { - } + await communicationManager.BroadcastToAll(new CreateGameResponse + { + Game = session.ToServiceModel(), + PlayerName = user.Id + }); + } + catch (Exception e) + { + Console.Error.WriteLine("Error broadcasting during PostSession"); + } - /// - /// Reads the board session and subscribes the caller to socket events for that session. - /// - [HttpGet("{gameName}")] - public async Task GetSession([FromRoute] string gameName) - { - var user = await ReadUserOrThrow(); - var session = await gameboardRepository.ReadSession(gameName); - if (session == null) - { - return NotFound(); - } + return Ok(); + } + return Conflict(); - communicationManager.SubscribeToGame(session, user!.Id); - var response = new GetSessionResponse() - { - Game = new Models.SessionMetadata(session).ToServiceModel(user), - BoardState = session.Shogi.ToServiceModel(), - MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(), - PlayerPerspective = user.Id == session.Player1.Id ? WhichPlayer.Player1 : WhichPlayer.Player2 - }; - return new JsonResult(response); - } + } - [HttpGet] - public async Task GetSessions() - { - var user = await ReadUserOrThrow(); - var sessions = await gameboardRepository.ReadSessionMetadatas(); + /// + /// Reads the board session and subscribes the caller to socket events for that session. + /// + [HttpGet("{gameName}")] + public async Task GetSession([FromRoute] string gameName) + { + var user = await ReadUserOrThrow(); + var session = await gameboardRepository.ReadSession(gameName); + if (session == null) + { + return NotFound(); + } - var sessionsJoinedByUser = sessions - .Where(s => s.IsSeated(user)) - .Select(s => s.ToServiceModel()) - .ToList(); - var sessionsNotJoinedByUser = sessions - .Where(s => !s.IsSeated(user)) - .Select(s => s.ToServiceModel()) - .ToList(); + var playerPerspective = WhichPerspective.Spectator; + if (session.Player1.Id == user.Id) + { + playerPerspective = WhichPerspective.Player1; + } + else if (session.Player2?.Id == user.Id) + { + playerPerspective = WhichPerspective.Player2; + } - return new GetSessionsResponse - { - PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser), - AllOtherSessions = new Collection(sessionsNotJoinedByUser) - }; - } + communicationManager.SubscribeToGame(session, user!.Id); + var response = new GetSessionResponse() + { + Game = new Models.SessionMetadata(session).ToServiceModel(), + BoardState = session.Shogi.ToServiceModel(), + MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(), + PlayerPerspective = playerPerspective + }; + return new JsonResult(response); + } - [HttpPut("{gameName}")] - public async Task PutJoinSession([FromRoute] string gameName) - { - var user = await ReadUserOrThrow(); - var session = await gameboardRepository.ReadSessionMetaData(gameName); - if (session == null) - { - return NotFound(); - } - if (session.Player2 != null) - { - return this.Conflict("This session already has two seated players and is full."); - } + [HttpGet] + public async Task GetSessions() + { + var user = await ReadUserOrThrow(); + var sessions = await gameboardRepository.ReadSessionMetadatas(); - session.SetPlayer2(user); - var success = await gameboardRepository.UpdateSession(session); - if (!success) return this.Problem(detail: "Unable to update session."); + var sessionsJoinedByUser = sessions + .Where(s => s.IsSeated(user)) + .Select(s => s.ToServiceModel()) + .ToList(); + var sessionsNotJoinedByUser = sessions + .Where(s => !s.IsSeated(user)) + .Select(s => s.ToServiceModel()) + .ToList(); - var opponentName = user.Id == session.Player1.Id - ? session.Player2!.Id - : session.Player1.Id; - await communicationManager.BroadcastToPlayers(new JoinGameResponse - { - GameName = session.Name, - PlayerName = user.Id - }, opponentName); - return Ok(); - } + return new GetSessionsResponse + { + PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser), + AllOtherSessions = new Collection(sessionsNotJoinedByUser) + }; + } - private async Task ReadUserOrThrow() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - throw new UnauthorizedAccessException("Unknown user claims."); - } - return user; - } - } + [HttpPut("{gameName}")] + public async Task PutJoinSession([FromRoute] string gameName) + { + var user = await ReadUserOrThrow(); + var session = await gameboardRepository.ReadSessionMetaData(gameName); + if (session == null) + { + return NotFound(); + } + if (session.Player2 != null) + { + return this.Conflict("This session already has two seated players and is full."); + } + + session.SetPlayer2(user); + var success = await gameboardRepository.UpdateSession(session); + if (!success) return this.Problem(detail: "Unable to update session."); + + var opponentName = user.Id == session.Player1.Id + ? session.Player2!.Id + : session.Player1.Id; + await communicationManager.BroadcastToPlayers(new JoinGameResponse + { + GameName = session.Name, + PlayerName = user.Id + }, opponentName); + return Ok(); + } + + private async Task ReadUserOrThrow() + { + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + throw new UnauthorizedAccessException("Unknown user claims."); + } + return user; + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 5b99eb3..6f442bd 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -15,89 +15,101 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers { - [ApiController] - [Route("[controller]")] - [Authorize(Roles = "Shogi")] - public class SocketController : ControllerBase - { - private readonly ILogger logger; - private readonly ISocketTokenCache tokenCache; - private readonly IGameboardManager gameboardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly AuthenticationProperties authenticationProps; + [ApiController] + [Route("[controller]")] + [Authorize(Roles = "Shogi")] + public class SocketController : ControllerBase + { + private readonly ILogger logger; + private readonly ISocketTokenCache tokenCache; + private readonly IGameboardManager gameboardManager; + private readonly IGameboardRepository gameboardRepository; + private readonly ISocketConnectionManager connectionManager; + private readonly AuthenticationProperties authenticationProps; - public SocketController( - ILogger logger, - ISocketTokenCache tokenCache, - IGameboardManager gameboardManager, - IGameboardRepository gameboardRepository) - { - this.logger = logger; - this.tokenCache = tokenCache; - this.gameboardManager = gameboardManager; - this.gameboardRepository = gameboardRepository; - authenticationProps = new AuthenticationProperties - { - AllowRefresh = true, - IsPersistent = true - }; - } + public SocketController( + ILogger logger, + ISocketTokenCache tokenCache, + IGameboardManager gameboardManager, + IGameboardRepository gameboardRepository, + ISocketConnectionManager connectionManager) + { + this.logger = logger; + this.tokenCache = tokenCache; + this.gameboardManager = gameboardManager; + this.gameboardRepository = gameboardRepository; + this.connectionManager = connectionManager; - [HttpGet("GuestLogout")] - [AllowAnonymous] - public async Task GuestLogout() - { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Ok(); - } + authenticationProps = new AuthenticationProperties + { + AllowRefresh = true, + IsPersistent = true + }; + } - [HttpGet("Token")] - public async Task GetToken() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - if (await gameboardManager.CreateUser(User)) - { - user = await gameboardManager.ReadUser(User); - } - } + [HttpGet("GuestLogout")] + [AllowAnonymous] + public async Task GuestLogout() + { + var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + var userId = User?.UserId(); + if (!string.IsNullOrEmpty(userId)) + { + connectionManager.UnsubscribeFromBroadcastAndGames(userId); + } + + await signoutTask; + return Ok(); + } - if (user == null) - { - return Unauthorized(); - } + [HttpGet("Token")] + public async Task GetToken() + { + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + if (await gameboardManager.CreateUser(User)) + { + user = await gameboardManager.ReadUser(User); + } + } - var token = tokenCache.GenerateToken(user.Id); - return new JsonResult(new GetTokenResponse(token)); - } + if (user == null) + { + return Unauthorized(); + } - [HttpGet("GuestToken")] - [AllowAnonymous] - public async Task GetGuestToken() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - // Create a guest user. - var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString()); - var success = await gameboardRepository.CreateUser(newUser); - if (!success) - { - return Conflict(); - } + var token = tokenCache.GenerateToken(user.Id); + return new JsonResult(new GetTokenResponse(token)); + } - var identity = newUser.CreateClaimsIdentity(); - await HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - authenticationProps - ); - user = newUser; - } + [HttpGet("GuestToken")] + [AllowAnonymous] + public async Task GetGuestToken() + { + var user = await gameboardManager.ReadUser(User); + if (user == null) + { + // Create a guest user. + var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString()); + var success = await gameboardRepository.CreateUser(newUser); + if (!success) + { + return Conflict(); + } - var token = tokenCache.GenerateToken(user.Id.ToString()); - return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token)); - } - } + var identity = newUser.CreateClaimsIdentity(); + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + authenticationProps + ); + user = newUser; + } + + var token = tokenCache.GenerateToken(user.Id.ToString()); + return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token)); + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs index 3c39341..d4860c4 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs @@ -1,43 +1,50 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using System.IO; +using System.Text; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Extensions { - public class LogMiddleware - { - private readonly RequestDelegate next; - private readonly ILogger logger; + public class LogMiddleware + { + private readonly RequestDelegate next; + private readonly ILogger logger; - public LogMiddleware(RequestDelegate next, ILoggerFactory factory) - { - this.next = next; - logger = factory.CreateLogger(); - } - public async Task Invoke(HttpContext context) - { - try - { - await next(context); - } - finally - { - logger.LogInformation("Request {method} {url} => {statusCode}", - context.Request?.Method, - context.Request?.Path.Value, - context.Response?.StatusCode); - } - } - } + public LogMiddleware(RequestDelegate next, ILoggerFactory factory) + { + this.next = next; + logger = factory.CreateLogger(); + } - public static class IApplicationBuilderExtensions - { - public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder) - { - builder.UseMiddleware(); - return builder; - } - } + public async Task Invoke(HttpContext context) + { + try + { + await next(context); + } + finally + { + using var stream = new MemoryStream(); + context.Request?.Body.CopyToAsync(stream); + + logger.LogInformation("Request {method} {url} => {statusCode} \n Body: {body}", + context.Request?.Method, + context.Request?.Path.Value, + context.Response?.StatusCode, + Encoding.UTF8.GetString(stream.ToArray())); + } + } + } + + public static class IApplicationBuilderExtensions + { + public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder) + { + builder.UseMiddleware(); + return builder; + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index 3820ae7..d5333f4 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System; using System.Text; using System.Text.RegularExpressions; @@ -21,7 +20,7 @@ namespace Gameboard.ShogiUI.Sockets.Extensions WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ", _ => " ? ", }; - if (self.Owner == WhichPlayer.Player2) + if (self.Owner == WhichPerspective.Player2) name = Regex.Replace(name, @"([^\s]+)\s", "$1."); return name; } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 10fa790..0b2a5b8 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -11,151 +11,152 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers { - public interface ISocketConnectionManager - { - Task BroadcastToAll(IResponse response); - //Task BroadcastToGame(string gameName, IResponse response); - //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); - void SubscribeToGame(Session session, string playerName); - void SubscribeToBroadcast(WebSocket socket, string playerName); - void UnsubscribeFromBroadcastAndGames(string playerName); - void UnsubscribeFromGame(string gameName, string playerName); - Task BroadcastToPlayers(IResponse response, params string?[] playerNames); - } + public interface ISocketConnectionManager + { + Task BroadcastToAll(IResponse response); + //Task BroadcastToGame(string gameName, IResponse response); + //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + void SubscribeToGame(Session session, string playerName); + void SubscribeToBroadcast(WebSocket socket, string playerName); + void UnsubscribeFromBroadcastAndGames(string playerName); + void UnsubscribeFromGame(string gameName, string playerName); + Task BroadcastToPlayers(IResponse response, params string?[] playerNames); + } - /// - /// Retains all active socket connections and provides convenient methods for sending messages to clients. - /// - public class SocketConnectionManager : ISocketConnectionManager - { - /// Dictionary key is player name. - private readonly ConcurrentDictionary connections; - /// Dictionary key is game name. - private readonly ConcurrentDictionary sessions; - private readonly ILogger logger; + /// + /// Retains all active socket connections and provides convenient methods for sending messages to clients. + /// + public class SocketConnectionManager : ISocketConnectionManager + { + /// Dictionary key is player name. + private readonly ConcurrentDictionary connections; + /// Dictionary key is game name. + private readonly ConcurrentDictionary sessions; + private readonly ILogger logger; - public SocketConnectionManager(ILogger logger) - { - this.logger = logger; - connections = new ConcurrentDictionary(); - sessions = new ConcurrentDictionary(); - } + public SocketConnectionManager(ILogger logger) + { + this.logger = logger; + connections = new ConcurrentDictionary(); + sessions = new ConcurrentDictionary(); + } - public void SubscribeToBroadcast(WebSocket socket, string playerName) - { - connections.TryAdd(playerName, socket); - } + public void SubscribeToBroadcast(WebSocket socket, string playerName) + { + connections.TryRemove(playerName, out var _); + connections.TryAdd(playerName, socket); + } - public void UnsubscribeFromBroadcastAndGames(string playerName) - { - connections.TryRemove(playerName, out _); - foreach (var kvp in sessions) - { - var sessionName = kvp.Key; - UnsubscribeFromGame(sessionName, playerName); - } - } + public void UnsubscribeFromBroadcastAndGames(string playerName) + { + connections.TryRemove(playerName, out _); + foreach (var kvp in sessions) + { + var sessionName = kvp.Key; + UnsubscribeFromGame(sessionName, playerName); + } + } - /// - /// Unsubscribes the player from their current game, then subscribes to the new game. - /// - public void SubscribeToGame(Session session, string playerName) - { - // Unsubscribe from any other games - foreach (var kvp in sessions) - { - var gameNameKey = kvp.Key; - UnsubscribeFromGame(gameNameKey, playerName); - } + /// + /// Unsubscribes the player from their current game, then subscribes to the new game. + /// + public void SubscribeToGame(Session session, string playerName) + { + // Unsubscribe from any other games + foreach (var kvp in sessions) + { + var gameNameKey = kvp.Key; + UnsubscribeFromGame(gameNameKey, playerName); + } - // Subscribe - if (connections.TryGetValue(playerName, out var socket)) - { - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); - } - } + // Subscribe + if (connections.TryGetValue(playerName, out var socket)) + { + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); + } + } - public void UnsubscribeFromGame(string gameName, string playerName) - { - if (sessions.TryGetValue(gameName, out var s)) - { - s.Subscriptions.TryRemove(playerName, out _); - if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); - } - } + public void UnsubscribeFromGame(string gameName, string playerName) + { + if (sessions.TryGetValue(gameName, out var s)) + { + s.Subscriptions.TryRemove(playerName, out _); + if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); + } + } - public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames) - { - var tasks = new List(playerNames.Length); - foreach (var name in playerNames) - { - if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket)) - { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", name, serialized); - tasks.Add(socket.SendTextAsync(serialized)); - } - } - await Task.WhenAll(tasks); - } - public Task BroadcastToAll(IResponse response) - { - var message = JsonConvert.SerializeObject(response); - logger.LogInformation($"Broadcasting\n{0}", message); - var tasks = new List(connections.Count); - foreach (var kvp in connections) - { - var socket = kvp.Value; - try - { + public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket)) + { + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); + } + } + await Task.WhenAll(tasks); + } + public Task BroadcastToAll(IResponse response) + { + var message = JsonConvert.SerializeObject(response); + logger.LogInformation($"Broadcasting\n{0}", message); + var tasks = new List(connections.Count); + foreach (var kvp in connections) + { + var socket = kvp.Value; + try + { - tasks.Add(socket.SendTextAsync(message)); - } - catch (WebSocketException webSocketException) - { - logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - UnsubscribeFromBroadcastAndGames(kvp.Key); - } - catch (Exception exception) - { - logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - UnsubscribeFromBroadcastAndGames(kvp.Key); - } - } - try - { - var task = Task.WhenAll(tasks); - return task; - } - catch (Exception e) - { - Console.WriteLine("Yo"); - } - return Task.FromResult(0); - } + tasks.Add(socket.SendTextAsync(message)); + } + catch (WebSocketException webSocketException) + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + UnsubscribeFromBroadcastAndGames(kvp.Key); + } + catch (Exception exception) + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + UnsubscribeFromBroadcastAndGames(kvp.Key); + } + } + try + { + var task = Task.WhenAll(tasks); + return task; + } + catch (Exception e) + { + Console.WriteLine("Yo"); + } + return Task.FromResult(0); + } - //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized1 = JsonConvert.SerializeObject(forPlayer1); - // var serialized2 = JsonConvert.SerializeObject(forPlayer2); - // return Task.WhenAll( - // session.SendToPlayer1(serialized1), - // session.SendToPlayer2(serialized2)); - // } - // return Task.CompletedTask; - //} + //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized1 = JsonConvert.SerializeObject(forPlayer1); + // var serialized2 = JsonConvert.SerializeObject(forPlayer2); + // return Task.WhenAll( + // session.SendToPlayer1(serialized1), + // session.SendToPlayer2(serialized2)); + // } + // return Task.CompletedTask; + //} - //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); - // return session.Broadcast(serialized); - // } - // return Task.CompletedTask; - //} - } + //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); + // return session.Broadcast(serialized); + // } + // return Task.CompletedTask; + //} + } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index 3c830e6..7b86808 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -8,11 +8,11 @@ namespace Gameboard.ShogiUI.Sockets.Models public class Piece : IPlanarElement { public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } + public WhichPerspective Owner { get; private set; } public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPlayer.Player2; + public bool IsUpsideDown => Owner == WhichPerspective.Player2; - public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + public Piece(WhichPiece piece, WhichPerspective owner, bool isPromoted = false) { WhichPiece = piece; Owner = owner; @@ -28,9 +28,9 @@ namespace Gameboard.ShogiUI.Sockets.Models public void ToggleOwnership() { - Owner = Owner == WhichPlayer.Player1 - ? WhichPlayer.Player2 - : WhichPlayer.Player1; + Owner = Owner == WhichPerspective.Player1 + ? WhichPerspective.Player2 + : WhichPerspective.Player1; } public void Promote() => IsPromoted = CanPromote; diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index cd5c5dd..fd73b40 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -5,34 +5,34 @@ using System.Net.WebSockets; namespace Gameboard.ShogiUI.Sockets.Models { - public class Session - { - // TODO: Separate subscriptions to the Session from the Session. - [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } - public string Name { get; } - public User Player1 { get; } - public User? Player2 { get; private set; } - public bool IsPrivate { get; } + public class Session + { + // TODO: Separate subscriptions to the Session from the Session. + [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } + public string Name { get; } + public User Player1 { get; } + public User? Player2 { get; private set; } + public bool IsPrivate { get; } - // TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied. - public Shogi Shogi { get; } + // TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied. + public Shogi Shogi { get; } - public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null) - { - Subscriptions = new ConcurrentDictionary(); + public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null) + { + Subscriptions = new ConcurrentDictionary(); - Name = name; - Player1 = player1; - Player2 = player2; - IsPrivate = isPrivate; - Shogi = shogi; - } + Name = name; + Player1 = player1; + Player2 = player2; + IsPrivate = isPrivate; + Shogi = shogi; + } - public void SetPlayer2(User user) - { - Player2 = user; - } + public void SetPlayer2(User user) + { + Player2 = user; + } - public Game ToServiceModel() => new() { GameName = Name, Player1 = Player1.DisplayName, Player2 = Player2?.DisplayName }; - } + public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName); + } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index ac35d6f..350b273 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -1,51 +1,37 @@ namespace Gameboard.ShogiUI.Sockets.Models { - /// - /// A representation of a Session without the board and game-rules. - /// - public class SessionMetadata - { - public string Name { get; } - public User Player1 { get; } - public User? Player2 { get; private set; } - public bool IsPrivate { get; } + /// + /// A representation of a Session without the board and game-rules. + /// + public class SessionMetadata + { + public string Name { get; } + public User Player1 { get; } + public User? Player2 { get; private set; } + public bool IsPrivate { get; } - public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null) - { - Name = name; - IsPrivate = isPrivate; - Player1 = player1; - Player2 = player2; - } - public SessionMetadata(Session sessionModel) - { - Name = sessionModel.Name; - IsPrivate = sessionModel.IsPrivate; - Player1 = sessionModel.Player1; - Player2 = sessionModel.Player2; - } + public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null) + { + Name = name; + IsPrivate = isPrivate; + Player1 = player1; + Player2 = player2; + } + public SessionMetadata(Session sessionModel) + { + Name = sessionModel.Name; + IsPrivate = sessionModel.IsPrivate; + Player1 = sessionModel.Player1; + Player2 = sessionModel.Player2; + } - public void SetPlayer2(User user) - { - Player2 = user; - } + public void SetPlayer2(User user) + { + Player2 = user; + } - public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id; + public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id; - public ServiceModels.Types.Game ToServiceModel(User? user = null) - { - // TODO: Find a better way for the UI to know whether or not they are seated at a given game than client-side ID matching. - var player1 = Player1.DisplayName; - var player2 = Player2?.DisplayName; - if (user != null) - { - if (user.Id == Player1.Id) player1 = Player1.Id; - if (Player2 != null && user.Id == Player2.Id) - { - player2 = Player2.DisplayName; - } - } - return new(Name, player1, player2); - } - } + public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName); + } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs index 121b0d6..ec944a2 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs @@ -8,451 +8,456 @@ using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Models { - /// - /// Facilitates Shogi board state transitions, cognisant of Shogi rules. - /// The board is always from Player1's perspective. - /// [0,0] is the lower-left position, [8,8] is the higher-right position - /// - public class Shogi - { - private delegate void MoveSetCallback(Piece piece, Vector2 position); - private readonly PathFinder2D pathFinder; - private Shogi? validationBoard; - private Vector2 player1King; - private Vector2 player2King; - private List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - public List Player1Hand { get; } - public List Player2Hand { get; } - public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method - public List MoveHistory { get; } - public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; - public WhichPlayer? InCheck { get; private set; } - public bool IsCheckmate { get; private set; } + /// + /// Facilitates Shogi board state transitions, cognisant of Shogi rules. + /// The board is always from Player1's perspective. + /// [0,0] is the lower-left position, [8,8] is the higher-right position + /// + public class Shogi + { + private delegate void MoveSetCallback(Piece piece, Vector2 position); + private readonly PathFinder2D pathFinder; + private Shogi? validationBoard; + private Vector2 player1King; + private Vector2 player2King; + private List Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand; + public List Player1Hand { get; } + public List Player2Hand { get; } + public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method + public List MoveHistory { get; } + public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2; + public WhichPerspective? InCheck { get; private set; } + public bool IsCheckmate { get; private set; } - public string Error { get; private set; } + public string Error { get; private set; } - public Shogi() - { - Board = new CoordsToNotationCollection(); - MoveHistory = new List(20); - Player1Hand = new List(); - Player2Hand = new List(); - pathFinder = new PathFinder2D(Board, 9, 9); - player1King = new Vector2(4, 0); - player2King = new Vector2(4, 8); - Error = string.Empty; + public Shogi() + { + Board = new CoordsToNotationCollection(); + MoveHistory = new List(20); + Player1Hand = new List(); + Player2Hand = new List(); + pathFinder = new PathFinder2D(Board, 9, 9); + player1King = new Vector2(4, 0); + player2King = new Vector2(4, 8); + Error = string.Empty; - InitializeBoardState(); - } + InitializeBoardState(); + } - public Shogi(IList moves) : this() - { - for (var i = 0; i < moves.Count; i++) - { - if (!Move(moves[i])) - { - // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); - } - } - } + public Shogi(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!Move(moves[i])) + { + // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); + } + } + } - private Shogi(Shogi toCopy) - { - Board = new CoordsToNotationCollection(); - foreach (var kvp in toCopy.Board) - { - Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); - } + private Shogi(Shogi toCopy) + { + Board = new CoordsToNotationCollection(); + foreach (var kvp in toCopy.Board) + { + Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); + } - pathFinder = new PathFinder2D(Board, 9, 9); - MoveHistory = new List(toCopy.MoveHistory); - Player1Hand = new List(toCopy.Player1Hand); - Player2Hand = new List(toCopy.Player2Hand); - player1King = toCopy.player1King; - player2King = toCopy.player2King; - Error = toCopy.Error; - } + pathFinder = new PathFinder2D(Board, 9, 9); + MoveHistory = new List(toCopy.MoveHistory); + Player1Hand = new List(toCopy.Player1Hand); + Player2Hand = new List(toCopy.Player2Hand); + player1King = toCopy.player1King; + player2King = toCopy.player2King; + Error = toCopy.Error; + } - public bool Move(Move move) - { - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - var moveSuccess = TryMove(move); + public bool Move(Move move) + { + var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1; + var moveSuccess = TryMove(move); - if (!moveSuccess) - { - return false; - } + if (!moveSuccess) + { + return false; + } - // Evaluate check - if (EvaluateCheckAfterMove(move, otherPlayer)) - { - InCheck = otherPlayer; - IsCheckmate = EvaluateCheckmate(); - } - return true; - } - /// - /// Attempts a given move. Returns false if the move is illegal. - /// - private bool TryMove(Move move) - { - // Try making the move in a "throw away" board. - if (validationBoard == null) - { - validationBoard = new Shogi(this); - } + // Evaluate check + if (EvaluateCheckAfterMove(move, otherPlayer)) + { + InCheck = otherPlayer; + IsCheckmate = EvaluateCheckmate(); + } + else + { + InCheck = null; + } + return true; + } + /// + /// Attempts a given move. Returns false if the move is illegal. + /// + private bool TryMove(Move move) + { + // Try making the move in a "throw away" board. + if (validationBoard == null) + { + validationBoard = new Shogi(this); + } - var isValid = move.PieceFromHand.HasValue - ? validationBoard.PlaceFromHand(move) - : validationBoard.PlaceFromBoard(move); - if (!isValid) - { - // Surface the error description. - Error = validationBoard.Error; - // Invalidate the "throw away" board. - validationBoard = null; - return false; - } - // If already in check, assert the move that resulted in check no longer results in check. - if (InCheck == WhoseTurn) - { - if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) - { - // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - return false; - } - } + var isValid = move.PieceFromHand.HasValue + ? validationBoard.PlaceFromHand(move) + : validationBoard.PlaceFromBoard(move); + if (!isValid) + { + // Surface the error description. + Error = validationBoard.Error; + // Invalidate the "throw away" board. + validationBoard = null; + return false; + } + // If already in check, assert the move that resulted in check no longer results in check. + if (InCheck == WhoseTurn) + { + if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) + { + // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + return false; + } + } - // The move is valid and legal; update board state. - if (move.PieceFromHand.HasValue) PlaceFromHand(move); - else PlaceFromBoard(move); - return true; - } - /// True if the move was successful. - private bool PlaceFromHand(Move move) - { - var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); - if (index < 0) - { - Error = $"{move.PieceFromHand} does not exist in the hand."; - return false; - } - if (Board[move.To] != null) - { - Error = $"Illegal move - attempting to capture while playing a piece from the hand."; - return false; - } + // The move is valid and legal; update board state. + if (move.PieceFromHand.HasValue) PlaceFromHand(move); + else PlaceFromBoard(move); + return true; + } + /// True if the move was successful. + private bool PlaceFromHand(Move move) + { + var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); + if (index < 0) + { + Error = $"{move.PieceFromHand} does not exist in the hand."; + return false; + } + if (Board[move.To] != null) + { + Error = $"Illegal move - attempting to capture while playing a piece from the hand."; + return false; + } - switch (move.PieceFromHand!.Value) - { - case WhichPiece.Knight: - { - // Knight cannot be placed onto the farthest two ranks from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2)) - { - Error = $"Knight has no valid moves after placed."; - return false; - } - break; - } - case WhichPiece.Lance: - case WhichPiece.Pawn: - { - // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0)) - { - Error = $"{move.PieceFromHand} has no valid moves after placed."; - return false; - } - break; - } - } + switch (move.PieceFromHand!.Value) + { + case WhichPiece.Knight: + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6) + || (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2)) + { + Error = $"Knight has no valid moves after placed."; + return false; + } + break; + } + case WhichPiece.Lance: + case WhichPiece.Pawn: + { + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8) + || (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0)) + { + Error = $"{move.PieceFromHand} has no valid moves after placed."; + return false; + } + break; + } + } - // Mutate the board. - Board[move.To] = Hand[index]; - Hand.RemoveAt(index); + // Mutate the board. + Board[move.To] = Hand[index]; + Hand.RemoveAt(index); + MoveHistory.Add(move); - return true; - } - /// True if the move was successful. - private bool PlaceFromBoard(Move move) - { - var fromPiece = Board[move.From!.Value]; - if (fromPiece == null) - { - Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; - return false; // Invalid move - } - if (fromPiece.Owner != WhoseTurn) - { - Error = "Not allowed to move the opponents piece"; - return false; // Invalid move; cannot move other players pieces. - } - if (IsPathable(move.From.Value, move.To) == false) - { - Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; - return false; // Invalid move; move not part of move-set. - } + return true; + } + /// True if the move was successful. + private bool PlaceFromBoard(Move move) + { + var fromPiece = Board[move.From!.Value]; + if (fromPiece == null) + { + Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; + return false; // Invalid move + } + if (fromPiece.Owner != WhoseTurn) + { + Error = "Not allowed to move the opponents piece"; + return false; // Invalid move; cannot move other players pieces. + } + if (IsPathable(move.From.Value, move.To) == false) + { + Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; + return false; // Invalid move; move not part of move-set. + } - var captured = Board[move.To]; - if (captured != null) - { - if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. - captured.Capture(); - Hand.Add(captured); - } + var captured = Board[move.To]; + if (captured != null) + { + if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. + captured.Capture(); + Hand.Add(captured); + } - //Mutate the board. - if (move.IsPromotion) - { - if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5)) - { - fromPiece.Promote(); - } - else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3)) - { - fromPiece.Promote(); - } - } - Board[move.To] = fromPiece; - Board[move.From!.Value] = null; - if (fromPiece.WhichPiece == WhichPiece.King) - { - if (fromPiece.Owner == WhichPlayer.Player1) - { - player1King.X = move.To.X; - player1King.Y = move.To.Y; - } - else if (fromPiece.Owner == WhichPlayer.Player2) - { - player2King.X = move.To.X; - player2King.Y = move.To.Y; - } - } - MoveHistory.Add(move); - return true; - } + //Mutate the board. + if (move.IsPromotion) + { + if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5)) + { + fromPiece.Promote(); + } + else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3)) + { + fromPiece.Promote(); + } + } + Board[move.To] = fromPiece; + Board[move.From!.Value] = null; + if (fromPiece.WhichPiece == WhichPiece.King) + { + if (fromPiece.Owner == WhichPerspective.Player1) + { + player1King.X = move.To.X; + player1King.Y = move.To.Y; + } + else if (fromPiece.Owner == WhichPerspective.Player2) + { + player2King.X = move.To.X; + player2King.Y = move.To.Y; + } + } + MoveHistory.Add(move); + return true; + } - private bool IsPathable(Vector2 from, Vector2 to) - { - var piece = Board[from]; - if (piece == null) return false; + private bool IsPathable(Vector2 from, Vector2 to) + { + var piece = Board[from]; + if (piece == null) return false; - var isObstructed = false; - var isPathable = pathFinder.PathTo(from, to, (other, position) => - { - if (other.Owner == piece.Owner) isObstructed = true; - }); - return !isObstructed && isPathable; - } + var isObstructed = false; + var isPathable = pathFinder.PathTo(from, to, (other, position) => + { + if (other.Owner == piece.Owner) isObstructed = true; + }); + return !isObstructed && isPathable; + } - #region Rules Validation - private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) - { - if (whichPlayer == InCheck) return true; // If we already know the player is in check, don't bother. + #region Rules Validation + private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective) + { + if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. - var isCheck = false; - var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; + var isCheck = false; + var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King; - // Check if the move put the king in check. - if (pathFinder.PathTo(move.To, kingPosition)) return true; + // Check if the move put the king in check. + if (pathFinder.PathTo(move.To, kingPosition)) return true; - if (move.From.HasValue) - { - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From!.Value); - var slope = Math.Abs(direction.Y / direction.X); - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) - { - // if slope of the move is also infinity...can skip this? - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != whichPlayer) - { - switch (piece.WhichPiece) - { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; - } - } - }); - } - else if (slope == 1) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) - { - isCheck = true; - } - }); - } - else if (slope == 0) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) - { - isCheck = true; - } - }); - } - } - else - { - // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. - // Go read the shogi rules to be sure this is true. - } + if (move.From.HasValue) + { + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, move.From!.Value); + var slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) + { + // if slope of the move is also infinity...can skip this? + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective) + { + switch (piece.WhichPiece) + { + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; + } + } + }); + } + else if (slope == 1) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); + } + } + else + { + // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. + // Go read the shogi rules to be sure this is true. + } - return isCheck; - } - private bool EvaluateCheckmate() - { - if (!InCheck.HasValue) return false; + return isCheck; + } + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; - // Assume true and try to disprove. - var isCheckmate = true; - Board.ForEachNotNull((piece, from) => // For each piece... - { - // Short circuit - if (!isCheckmate) return; + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, from) => // For each piece... + { + // Short circuit + if (!isCheckmate) return; - if (piece.Owner == InCheck) // ...owned by the player in check... - { - // ...evaluate if any move gets the player out of check. - pathFinder.PathEvery(from, (other, position) => - { - if (validationBoard == null) validationBoard = new Shogi(this); - var moveToTry = new Move(from, position); - var moveSuccess = validationBoard.TryMove(moveToTry); - if (moveSuccess) - { - validationBoard = null; - if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) - { - isCheckmate = false; - } - } - }); - } - }); - return isCheckmate; - } - #endregion + if (piece.Owner == InCheck) // ...owned by the player in check... + { + // ...evaluate if any move gets the player out of check. + pathFinder.PathEvery(from, (other, position) => + { + if (validationBoard == null) validationBoard = new Shogi(this); + var moveToTry = new Move(from, position); + var moveSuccess = validationBoard.TryMove(moveToTry); + if (moveSuccess) + { + validationBoard = null; + if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) + { + isCheckmate = false; + } + } + }); + } + }); + return isCheckmate; + } + #endregion - private void InitializeBoardState() - { - Board["A1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); - Board["B1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); - Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); - Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); - Board["E1"] = new Piece(WhichPiece.King, WhichPlayer.Player1); - Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); - Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); - Board["H1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); - Board["I1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + private void InitializeBoardState() + { + Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); + Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); + Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); + Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); + Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1); + Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); + Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); + Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); + Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); - Board["A2"] = null; - Board["B2"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player1); - Board["C2"] = null; - Board["D2"] = null; - Board["E2"] = null; - Board["F2"] = null; - Board["G2"] = null; - Board["H2"] = new Piece(WhichPiece.Rook, WhichPlayer.Player1); - Board["I2"] = null; + Board["A2"] = null; + Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1); + Board["C2"] = null; + Board["D2"] = null; + Board["E2"] = null; + Board["F2"] = null; + Board["G2"] = null; + Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1); + Board["I2"] = null; - Board["A3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["B3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["C3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["D3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["E3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["F3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["G3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["H3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - Board["I3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); + Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["A4"] = null; - Board["B4"] = null; - Board["C4"] = null; - Board["D4"] = null; - Board["E4"] = null; - Board["F4"] = null; - Board["G4"] = null; - Board["H4"] = null; - Board["I4"] = null; + Board["A4"] = null; + Board["B4"] = null; + Board["C4"] = null; + Board["D4"] = null; + Board["E4"] = null; + Board["F4"] = null; + Board["G4"] = null; + Board["H4"] = null; + Board["I4"] = null; - Board["A5"] = null; - Board["B5"] = null; - Board["C5"] = null; - Board["D5"] = null; - Board["E5"] = null; - Board["F5"] = null; - Board["G5"] = null; - Board["H5"] = null; - Board["I5"] = null; + Board["A5"] = null; + Board["B5"] = null; + Board["C5"] = null; + Board["D5"] = null; + Board["E5"] = null; + Board["F5"] = null; + Board["G5"] = null; + Board["H5"] = null; + Board["I5"] = null; - Board["A6"] = null; - Board["B6"] = null; - Board["C6"] = null; - Board["D6"] = null; - Board["E6"] = null; - Board["F6"] = null; - Board["G6"] = null; - Board["H6"] = null; - Board["I6"] = null; + Board["A6"] = null; + Board["B6"] = null; + Board["C6"] = null; + Board["D6"] = null; + Board["E6"] = null; + Board["F6"] = null; + Board["G6"] = null; + Board["H6"] = null; + Board["I6"] = null; - Board["A7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["B7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["C7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["D7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["E7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["F7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["G7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["H7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - Board["I7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); + Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["A8"] = null; - Board["B8"] = new Piece(WhichPiece.Rook, WhichPlayer.Player2); - Board["C8"] = null; - Board["D8"] = null; - Board["E8"] = null; - Board["F8"] = null; - Board["G8"] = null; - Board["H8"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player2); - Board["I8"] = null; + Board["A8"] = null; + Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2); + Board["C8"] = null; + Board["D8"] = null; + Board["E8"] = null; + Board["F8"] = null; + Board["G8"] = null; + Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2); + Board["I8"] = null; - Board["A9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); - Board["B9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); - Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); - Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); - Board["E9"] = new Piece(WhichPiece.King, WhichPlayer.Player2); - Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); - Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); - Board["H9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); - Board["I9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); - } + Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); + Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); + Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); + Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); + Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2); + Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); + Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); + Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); + Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); + } - public BoardState ToServiceModel() - { - return new BoardState - { - Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()), - PlayerInCheck = InCheck, - WhoseTurn = WhoseTurn, - Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), - Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() - }; - } - } + public BoardState ToServiceModel() + { + return new BoardState + { + Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()), + PlayerInCheck = InCheck, + WhoseTurn = WhoseTurn, + Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), + Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() + }; + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index 60f5b2b..7c1a2fa 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -8,72 +8,78 @@ using System.Security.Claims; namespace Gameboard.ShogiUI.Sockets.Models { - public class User - { - public static readonly ReadOnlyCollection Adjectives = new(new[] { - "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" - }); - public static readonly ReadOnlyCollection Subjects = new(new[] { - "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato" - }); - public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft); - public static User CreateGuestUser(string id) - { - var random = new Random(); - // Adjective - var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count); - var adj = Adjectives[index]; - // Subject - index = (int)Math.Floor(random.NextDouble() * Subjects.Count); - var subj = Subjects[index]; + public class User + { + public static readonly ReadOnlyCollection Adjectives = new(new[] { + "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" + }); + public static readonly ReadOnlyCollection Subjects = new(new[] { + "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin" + }); + public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft); + public static User CreateGuestUser(string id) + { + var random = new Random(); + // Adjective + var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count); + var adj = Adjectives[index]; + // Subject + index = (int)Math.Floor(random.NextDouble() * Subjects.Count); + var subj = Subjects[index]; - return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest); - } + return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest); + } - public string Id { get; } - public string DisplayName { get; } + public string Id { get; } + public string DisplayName { get; } - public WhichLoginPlatform LoginPlatform { get; } + public WhichLoginPlatform LoginPlatform { get; } - public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; + public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; - public User(string id, string displayName, WhichLoginPlatform platform) - { - Id = id; - DisplayName = displayName; - LoginPlatform = platform; - } + public User(string id, string displayName, WhichLoginPlatform platform) + { + Id = id; + DisplayName = displayName; + LoginPlatform = platform; + } - public User(UserDocument document) - { - Id = document.Id; - DisplayName = document.DisplayName; - LoginPlatform = document.Platform; - } + public User(UserDocument document) + { + Id = document.Id; + DisplayName = document.DisplayName; + LoginPlatform = document.Platform; + } - public ClaimsIdentity CreateClaimsIdentity() - { - if (LoginPlatform == WhichLoginPlatform.Guest) - { - var claims = new List(4) - { - new Claim(ClaimTypes.NameIdentifier, Id), - new Claim(ClaimTypes.Name, DisplayName), - new Claim(ClaimTypes.Role, "Guest"), - new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + public ClaimsIdentity CreateClaimsIdentity() + { + if (LoginPlatform == WhichLoginPlatform.Guest) + { + var claims = new List(4) + { + new Claim(ClaimTypes.NameIdentifier, Id), + new Claim(ClaimTypes.Name, DisplayName), + new Claim(ClaimTypes.Role, "Guest"), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. }; - return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - } - else - { - var claims = new List(3) - { - new Claim(ClaimTypes.NameIdentifier, Id), - new Claim(ClaimTypes.Name, DisplayName), - new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. + return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + } + else + { + var claims = new List(3) + { + new Claim(ClaimTypes.NameIdentifier, Id), + new Claim(ClaimTypes.Name, DisplayName), + new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers. }; - return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); - } - } - } + return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); + } + } + + public ServiceModels.Types.User ToServiceModel() => new() + { + Id = Id, + Name = DisplayName + }; + } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs index 3f28f87..7f0be6f 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -5,7 +5,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public class Piece { public bool IsPromoted { get; set; } - public WhichPlayer Owner { get; set; } + public WhichPerspective Owner { get; set; } public WhichPiece WhichPiece { get; set; } /// diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs index a04a25b..fc00288 100644 --- a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs +++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs @@ -1,8 +1,5 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using System; -using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index 59f8695..1476dbb 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -3,7 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; using System.Numerics; -using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer; +using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective; using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; namespace Gameboard.ShogiUI.UnitTests.Rules { diff --git a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs index 37234f1..9c565f6 100644 --- a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs +++ b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs @@ -6,340 +6,366 @@ using System.Linq; using Xunit; using Xunit.Abstractions; using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; -using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer; +using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective; namespace Gameboard.ShogiUI.xUnitTests { - public class ShogiShould - { - private readonly ITestOutputHelper output; - public ShogiShould(ITestOutputHelper output) - { - this.output = output; - } + public class ShogiShould + { + private readonly ITestOutputHelper output; + public ShogiShould(ITestOutputHelper output) + { + this.output = output; + } - [Fact] - public void InitializeBoardState() - { - // Act - var board = new Shogi().Board; + [Fact] + public void InitializeBoardState() + { + // Act + var board = new Shogi().Board; - // Assert - board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - board["A1"].Owner.Should().Be(WhichPlayer.Player1); - board["A1"].IsPromoted.Should().Be(false); - board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); - board["B1"].Owner.Should().Be(WhichPlayer.Player1); - board["B1"].IsPromoted.Should().Be(false); - board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C1"].Owner.Should().Be(WhichPlayer.Player1); - board["C1"].IsPromoted.Should().Be(false); - board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D1"].Owner.Should().Be(WhichPlayer.Player1); - board["D1"].IsPromoted.Should().Be(false); - board["E1"].WhichPiece.Should().Be(WhichPiece.King); - board["E1"].Owner.Should().Be(WhichPlayer.Player1); - board["E1"].IsPromoted.Should().Be(false); - board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F1"].Owner.Should().Be(WhichPlayer.Player1); - board["F1"].IsPromoted.Should().Be(false); - board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G1"].Owner.Should().Be(WhichPlayer.Player1); - board["G1"].IsPromoted.Should().Be(false); - board["H1"].WhichPiece.Should().Be(WhichPiece.Knight); - board["H1"].Owner.Should().Be(WhichPlayer.Player1); - board["H1"].IsPromoted.Should().Be(false); - board["I1"].WhichPiece.Should().Be(WhichPiece.Lance); - board["I1"].Owner.Should().Be(WhichPlayer.Player1); - board["I1"].IsPromoted.Should().Be(false); + // Assert + board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + board["A1"].Owner.Should().Be(WhichPerspective.Player1); + board["A1"].IsPromoted.Should().Be(false); + board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + board["B1"].Owner.Should().Be(WhichPerspective.Player1); + board["B1"].IsPromoted.Should().Be(false); + board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C1"].Owner.Should().Be(WhichPerspective.Player1); + board["C1"].IsPromoted.Should().Be(false); + board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D1"].Owner.Should().Be(WhichPerspective.Player1); + board["D1"].IsPromoted.Should().Be(false); + board["E1"].WhichPiece.Should().Be(WhichPiece.King); + board["E1"].Owner.Should().Be(WhichPerspective.Player1); + board["E1"].IsPromoted.Should().Be(false); + board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F1"].Owner.Should().Be(WhichPerspective.Player1); + board["F1"].IsPromoted.Should().Be(false); + board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G1"].Owner.Should().Be(WhichPerspective.Player1); + board["G1"].IsPromoted.Should().Be(false); + board["H1"].WhichPiece.Should().Be(WhichPiece.Knight); + board["H1"].Owner.Should().Be(WhichPerspective.Player1); + board["H1"].IsPromoted.Should().Be(false); + board["I1"].WhichPiece.Should().Be(WhichPiece.Lance); + board["I1"].Owner.Should().Be(WhichPerspective.Player1); + board["I1"].IsPromoted.Should().Be(false); - board["A2"].Should().BeNull(); - board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop); - board["B2"].Owner.Should().Be(WhichPlayer.Player1); - board["B2"].IsPromoted.Should().Be(false); - board["C2"].Should().BeNull(); - board["D2"].Should().BeNull(); - board["E2"].Should().BeNull(); - board["F2"].Should().BeNull(); - board["G2"].Should().BeNull(); - board["H2"].WhichPiece.Should().Be(WhichPiece.Rook); - board["H2"].Owner.Should().Be(WhichPlayer.Player1); - board["H2"].IsPromoted.Should().Be(false); - board["I2"].Should().BeNull(); + board["A2"].Should().BeNull(); + board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop); + board["B2"].Owner.Should().Be(WhichPerspective.Player1); + board["B2"].IsPromoted.Should().Be(false); + board["C2"].Should().BeNull(); + board["D2"].Should().BeNull(); + board["E2"].Should().BeNull(); + board["F2"].Should().BeNull(); + board["G2"].Should().BeNull(); + board["H2"].WhichPiece.Should().Be(WhichPiece.Rook); + board["H2"].Owner.Should().Be(WhichPerspective.Player1); + board["H2"].IsPromoted.Should().Be(false); + board["I2"].Should().BeNull(); - board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["A3"].Owner.Should().Be(WhichPlayer.Player1); - board["A3"].IsPromoted.Should().Be(false); - board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["B3"].Owner.Should().Be(WhichPlayer.Player1); - board["B3"].IsPromoted.Should().Be(false); - board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["C3"].Owner.Should().Be(WhichPlayer.Player1); - board["C3"].IsPromoted.Should().Be(false); - board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["D3"].Owner.Should().Be(WhichPlayer.Player1); - board["D3"].IsPromoted.Should().Be(false); - board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["E3"].Owner.Should().Be(WhichPlayer.Player1); - board["E3"].IsPromoted.Should().Be(false); - board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["F3"].Owner.Should().Be(WhichPlayer.Player1); - board["F3"].IsPromoted.Should().Be(false); - board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["G3"].Owner.Should().Be(WhichPlayer.Player1); - board["G3"].IsPromoted.Should().Be(false); - board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["H3"].Owner.Should().Be(WhichPlayer.Player1); - board["H3"].IsPromoted.Should().Be(false); - board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["I3"].Owner.Should().Be(WhichPlayer.Player1); - board["I3"].IsPromoted.Should().Be(false); + board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["A3"].Owner.Should().Be(WhichPerspective.Player1); + board["A3"].IsPromoted.Should().Be(false); + board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["B3"].Owner.Should().Be(WhichPerspective.Player1); + board["B3"].IsPromoted.Should().Be(false); + board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["C3"].Owner.Should().Be(WhichPerspective.Player1); + board["C3"].IsPromoted.Should().Be(false); + board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["D3"].Owner.Should().Be(WhichPerspective.Player1); + board["D3"].IsPromoted.Should().Be(false); + board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["E3"].Owner.Should().Be(WhichPerspective.Player1); + board["E3"].IsPromoted.Should().Be(false); + board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["F3"].Owner.Should().Be(WhichPerspective.Player1); + board["F3"].IsPromoted.Should().Be(false); + board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["G3"].Owner.Should().Be(WhichPerspective.Player1); + board["G3"].IsPromoted.Should().Be(false); + board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["H3"].Owner.Should().Be(WhichPerspective.Player1); + board["H3"].IsPromoted.Should().Be(false); + board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["I3"].Owner.Should().Be(WhichPerspective.Player1); + board["I3"].IsPromoted.Should().Be(false); - board["A4"].Should().BeNull(); - board["B4"].Should().BeNull(); - board["C4"].Should().BeNull(); - board["D4"].Should().BeNull(); - board["E4"].Should().BeNull(); - board["F4"].Should().BeNull(); - board["G4"].Should().BeNull(); - board["H4"].Should().BeNull(); - board["I4"].Should().BeNull(); + board["A4"].Should().BeNull(); + board["B4"].Should().BeNull(); + board["C4"].Should().BeNull(); + board["D4"].Should().BeNull(); + board["E4"].Should().BeNull(); + board["F4"].Should().BeNull(); + board["G4"].Should().BeNull(); + board["H4"].Should().BeNull(); + board["I4"].Should().BeNull(); - board["A5"].Should().BeNull(); - board["B5"].Should().BeNull(); - board["C5"].Should().BeNull(); - board["D5"].Should().BeNull(); - board["E5"].Should().BeNull(); - board["F5"].Should().BeNull(); - board["G5"].Should().BeNull(); - board["H5"].Should().BeNull(); - board["I5"].Should().BeNull(); + board["A5"].Should().BeNull(); + board["B5"].Should().BeNull(); + board["C5"].Should().BeNull(); + board["D5"].Should().BeNull(); + board["E5"].Should().BeNull(); + board["F5"].Should().BeNull(); + board["G5"].Should().BeNull(); + board["H5"].Should().BeNull(); + board["I5"].Should().BeNull(); - board["A6"].Should().BeNull(); - board["B6"].Should().BeNull(); - board["C6"].Should().BeNull(); - board["D6"].Should().BeNull(); - board["E6"].Should().BeNull(); - board["F6"].Should().BeNull(); - board["G6"].Should().BeNull(); - board["H6"].Should().BeNull(); - board["I6"].Should().BeNull(); + board["A6"].Should().BeNull(); + board["B6"].Should().BeNull(); + board["C6"].Should().BeNull(); + board["D6"].Should().BeNull(); + board["E6"].Should().BeNull(); + board["F6"].Should().BeNull(); + board["G6"].Should().BeNull(); + board["H6"].Should().BeNull(); + board["I6"].Should().BeNull(); - board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["A7"].Owner.Should().Be(WhichPlayer.Player2); - board["A7"].IsPromoted.Should().Be(false); - board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["B7"].Owner.Should().Be(WhichPlayer.Player2); - board["B7"].IsPromoted.Should().Be(false); - board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["C7"].Owner.Should().Be(WhichPlayer.Player2); - board["C7"].IsPromoted.Should().Be(false); - board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["D7"].Owner.Should().Be(WhichPlayer.Player2); - board["D7"].IsPromoted.Should().Be(false); - board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["E7"].Owner.Should().Be(WhichPlayer.Player2); - board["E7"].IsPromoted.Should().Be(false); - board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["F7"].Owner.Should().Be(WhichPlayer.Player2); - board["F7"].IsPromoted.Should().Be(false); - board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["G7"].Owner.Should().Be(WhichPlayer.Player2); - board["G7"].IsPromoted.Should().Be(false); - board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["H7"].Owner.Should().Be(WhichPlayer.Player2); - board["H7"].IsPromoted.Should().Be(false); - board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["I7"].Owner.Should().Be(WhichPlayer.Player2); - board["I7"].IsPromoted.Should().Be(false); + board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["A7"].Owner.Should().Be(WhichPerspective.Player2); + board["A7"].IsPromoted.Should().Be(false); + board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["B7"].Owner.Should().Be(WhichPerspective.Player2); + board["B7"].IsPromoted.Should().Be(false); + board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["C7"].Owner.Should().Be(WhichPerspective.Player2); + board["C7"].IsPromoted.Should().Be(false); + board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["D7"].Owner.Should().Be(WhichPerspective.Player2); + board["D7"].IsPromoted.Should().Be(false); + board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["E7"].Owner.Should().Be(WhichPerspective.Player2); + board["E7"].IsPromoted.Should().Be(false); + board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["F7"].Owner.Should().Be(WhichPerspective.Player2); + board["F7"].IsPromoted.Should().Be(false); + board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["G7"].Owner.Should().Be(WhichPerspective.Player2); + board["G7"].IsPromoted.Should().Be(false); + board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["H7"].Owner.Should().Be(WhichPerspective.Player2); + board["H7"].IsPromoted.Should().Be(false); + board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn); + board["I7"].Owner.Should().Be(WhichPerspective.Player2); + board["I7"].IsPromoted.Should().Be(false); - board["A8"].Should().BeNull(); - board["B8"].WhichPiece.Should().Be(WhichPiece.Rook); - board["B8"].Owner.Should().Be(WhichPlayer.Player2); - board["B8"].IsPromoted.Should().Be(false); - board["C8"].Should().BeNull(); - board["D8"].Should().BeNull(); - board["E8"].Should().BeNull(); - board["F8"].Should().BeNull(); - board["G8"].Should().BeNull(); - board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); - board["H8"].Owner.Should().Be(WhichPlayer.Player2); - board["H8"].IsPromoted.Should().Be(false); - board["I8"].Should().BeNull(); + board["A8"].Should().BeNull(); + board["B8"].WhichPiece.Should().Be(WhichPiece.Rook); + board["B8"].Owner.Should().Be(WhichPerspective.Player2); + board["B8"].IsPromoted.Should().Be(false); + board["C8"].Should().BeNull(); + board["D8"].Should().BeNull(); + board["E8"].Should().BeNull(); + board["F8"].Should().BeNull(); + board["G8"].Should().BeNull(); + board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); + board["H8"].Owner.Should().Be(WhichPerspective.Player2); + board["H8"].IsPromoted.Should().Be(false); + board["I8"].Should().BeNull(); - board["A9"].WhichPiece.Should().Be(WhichPiece.Lance); - board["A9"].Owner.Should().Be(WhichPlayer.Player2); - board["A9"].IsPromoted.Should().Be(false); - board["B9"].WhichPiece.Should().Be(WhichPiece.Knight); - board["B9"].Owner.Should().Be(WhichPlayer.Player2); - board["B9"].IsPromoted.Should().Be(false); - board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C9"].Owner.Should().Be(WhichPlayer.Player2); - board["C9"].IsPromoted.Should().Be(false); - board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D9"].Owner.Should().Be(WhichPlayer.Player2); - board["D9"].IsPromoted.Should().Be(false); - board["E9"].WhichPiece.Should().Be(WhichPiece.King); - board["E9"].Owner.Should().Be(WhichPlayer.Player2); - board["E9"].IsPromoted.Should().Be(false); - board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F9"].Owner.Should().Be(WhichPlayer.Player2); - board["F9"].IsPromoted.Should().Be(false); - board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G9"].Owner.Should().Be(WhichPlayer.Player2); - board["G9"].IsPromoted.Should().Be(false); - board["H9"].WhichPiece.Should().Be(WhichPiece.Knight); - board["H9"].Owner.Should().Be(WhichPlayer.Player2); - board["H9"].IsPromoted.Should().Be(false); - board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - board["I9"].Owner.Should().Be(WhichPlayer.Player2); - board["I9"].IsPromoted.Should().Be(false); - } + board["A9"].WhichPiece.Should().Be(WhichPiece.Lance); + board["A9"].Owner.Should().Be(WhichPerspective.Player2); + board["A9"].IsPromoted.Should().Be(false); + board["B9"].WhichPiece.Should().Be(WhichPiece.Knight); + board["B9"].Owner.Should().Be(WhichPerspective.Player2); + board["B9"].IsPromoted.Should().Be(false); + board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C9"].Owner.Should().Be(WhichPerspective.Player2); + board["C9"].IsPromoted.Should().Be(false); + board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D9"].Owner.Should().Be(WhichPerspective.Player2); + board["D9"].IsPromoted.Should().Be(false); + board["E9"].WhichPiece.Should().Be(WhichPiece.King); + board["E9"].Owner.Should().Be(WhichPerspective.Player2); + board["E9"].IsPromoted.Should().Be(false); + board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F9"].Owner.Should().Be(WhichPerspective.Player2); + board["F9"].IsPromoted.Should().Be(false); + board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G9"].Owner.Should().Be(WhichPerspective.Player2); + board["G9"].IsPromoted.Should().Be(false); + board["H9"].WhichPiece.Should().Be(WhichPiece.Knight); + board["H9"].Owner.Should().Be(WhichPerspective.Player2); + board["H9"].IsPromoted.Should().Be(false); + board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + board["I9"].Owner.Should().Be(WhichPerspective.Player2); + board["I9"].IsPromoted.Should().Be(false); + } - [Fact] - public void InitializeBoardStateWithMoves() - { - var moves = new[] - { + [Fact] + public void InitializeBoardStateWithMoves() + { + var moves = new[] + { // P1 Pawn new Move("A3", "A4") - }; - var shogi = new Shogi(moves); - shogi.Board["A3"].Should().BeNull(); - shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); - } + }; + var shogi = new Shogi(moves); + shogi.Board["A3"].Should().BeNull(); + shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); + } - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = new Shogi(); - shogi.Board["D5"].Should().BeNull(); + [Fact] + public void AllowValidMoves_AfterCheck() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move("C3", "C4"), + // P2 Pawn + new Move("G7", "G6"), + // P1 Bishop puts P2 in check + new Move("B2", "G7"), + }; + var shogi = new Shogi(moves); + shogi.InCheck.Should().Be(WhichPerspective.Player2); - // Act - var moveSuccess = shogi.Move(new Move("D5", "D6")); + // Act - P2 is able to un-check theirself. + /// P2 King moves out of check + var moveSuccess = shogi.Move(new Move("E9", "E8")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["D5"].Should().BeNull(); - shogi.Board["D6"].Should().BeNull(); - } + // Assert + using var _ = new AssertionScope(); + moveSuccess.Should().BeTrue(); + shogi.InCheck.Should().BeNull(); + } - [Fact] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = new Shogi(); + shogi.Board["D5"].Should().BeNull(); - // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move("A3", "A3")); + // Act + var moveSuccess = shogi.Move(new Move("D5", "D6")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["D5"].Should().BeNull(); + shogi.Board["D6"].Should().BeNull(); + } - [Fact] - public void PreventInvalidMoves_MoveSet() - { - // Arrange - var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = new Shogi(); - // Act - Move Lance illegally - var moveSuccess = shogi.Move(new Move("A1", "D5")); + // Act - P1 "moves" pawn to the position it already exists at. + var moveSuccess = shogi.Move(new Move("A3", "A3")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["A5"].Should().BeNull(); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } - [Fact] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var shogi = new Shogi(); - shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); - shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2); + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var shogi = new Shogi(); - // Act - Move Player2 Pawn when it is Player1 turn. - var moveSuccess = shogi.Move(new Move("A7", "A6")); + // Act - Move Lance illegally + var moveSuccess = shogi.Move(new Move("A1", "D5")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board["A6"].Should().BeNull(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["A5"].Should().BeNull(); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } - [Fact] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Arrange - var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = new Shogi(); + shogi.WhoseTurn.Should().Be(WhichPerspective.Player1); + shogi.Board["A7"].Owner.Should().Be(WhichPerspective.Player2); - // Act - Move P1 Lance through P1 Pawn. - var moveSuccess = shogi.Move(new Move("A1", "A5")); + // Act - Move Player2 Pawn when it is Player1 turn. + var moveSuccess = shogi.Move(new Move("A7", "A6")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board["A5"].Should().BeNull(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board["A6"].Should().BeNull(); + } - [Fact] - public void PreventInvalidMoves_CaptureAlly() - { - // Arrange - var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveThroughAllies() + { + // Arrange + var shogi = new Shogi(); - // Act - P1 Knight tries to capture P1 Pawn. - var moveSuccess = shogi.Move(new Move("B1", "C3")); + // Act - Move P1 Lance through P1 Pawn. + var moveSuccess = shogi.Move(new Move("A1", "A5")); - // Arrange - moveSuccess.Should().BeFalse(); - shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); - shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board["A5"].Should().BeNull(); + } - [Fact] - public void PreventInvalidMoves_Check() - { - // Arrange - var moves = new[] - { + [Fact] + public void PreventInvalidMoves_CaptureAlly() + { + // Arrange + var shogi = new Shogi(); + + // Act - P1 Knight tries to capture P1 Pawn. + var moveSuccess = shogi.Move(new Move("B1", "C3")); + + // Arrange + moveSuccess.Should().BeFalse(); + shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Player1Hand.Should().BeEmpty(); + shogi.Player2Hand.Should().BeEmpty(); + } + + [Fact] + public void PreventInvalidMoves_Check() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4"), // P2 Pawn new Move("G7", "G6"), // P1 Bishop puts P2 in check new Move("B2", "G7") - }; - var shogi = new Shogi(moves); - shogi.InCheck.Should().Be(WhichPlayer.Player2); + }; + var shogi = new Shogi(moves); + shogi.InCheck.Should().Be(WhichPerspective.Player2); - // Act - P2 moves Lance while in check. - var moveSuccess = shogi.Move(new Move("I9", "I8")); + // Act - P2 moves Lance while in check. + var moveSuccess = shogi.Move(new Move("I9", "I8")); - // Assert - moveSuccess.Should().BeFalse(); - shogi.InCheck.Should().Be(WhichPlayer.Player2); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I8"].Should().BeNull(); - } + // Assert + moveSuccess.Should().BeFalse(); + shogi.InCheck.Should().Be(WhichPerspective.Player2); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I8"].Should().BeNull(); + } - [Fact] - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var moves = new[] - { + [Fact] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4"), // P2 Pawn @@ -360,53 +386,53 @@ namespace Gameboard.ShogiUI.xUnitTests new Move("H9", "I9"), // P2 Pawn captures P1 Pawn new Move("I4", "I3") - }; - var shogi = new Shogi(moves); - shogi.Player1Hand.Count.Should().Be(4); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + }; + var shogi = new Shogi(moves); + shogi.Player1Hand.Count.Should().Be(4); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.WhoseTurn.Should().Be(WhichPerspective.Player1); - // Act | Assert - Illegally placing Knight from the hand in farthest row. - shogi.Board["H9"].Should().BeNull(); - var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in farthest row. + shogi.Board["H9"].Should().BeNull(); + var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally placing Knight from the hand in second farthest row. - shogi.Board["H8"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); - dropSuccess.Should().BeFalse(); - shogi.Board["H8"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in second farthest row. + shogi.Board["H8"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); + dropSuccess.Should().BeFalse(); + shogi.Board["H8"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally place Lance from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + // Act | Assert - Illegally place Lance from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - // Act | Assert - Illegally place Pawn from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // Act | Assert - Illegally place Pawn from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // TODO - } + // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // TODO + } - [Fact] - public void PreventInvalidDrop_Check() - { - // Arrange - var moves = new[] - { + [Fact] + public void PreventInvalidDrop_Check() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4"), // P2 Pawn @@ -421,28 +447,28 @@ namespace Gameboard.ShogiUI.xUnitTests new Move("A7", "A6"), // P1 drop Bishop, place P2 in check new Move(WhichPiece.Bishop, "G7") - }; - var shogi = new Shogi(moves); - shogi.InCheck.Should().Be(WhichPlayer.Player2); - shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["E5"].Should().BeNull(); + }; + var shogi = new Shogi(moves); + shogi.InCheck.Should().Be(WhichPerspective.Player2); + shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["E5"].Should().BeNull(); - // Act - P2 places a Bishop while in check. - var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); + // Act - P2 places a Bishop while in check. + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); - // Assert - dropSuccess.Should().BeFalse(); - shogi.Board["E5"].Should().BeNull(); - shogi.InCheck.Should().Be(WhichPlayer.Player2); - shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - } + // Assert + dropSuccess.Should().BeFalse(); + shogi.Board["E5"].Should().BeNull(); + shogi.InCheck.Should().Be(WhichPerspective.Player2); + shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + } - [Fact] - public void PreventInvalidDrop_Capture() - { - // Arrange - var moves = new[] - { + [Fact] + public void PreventInvalidDrop_Capture() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4"), // P2 Pawn @@ -451,84 +477,84 @@ namespace Gameboard.ShogiUI.xUnitTests new Move("B2", "H8"), // P2 Pawn new Move("G6", "G5") - }; - var shogi = new Shogi(moves); - using (new AssertionScope()) - { - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["I9"].Should().NotBeNull(); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); - } + }; + var shogi = new Shogi(moves); + using (new AssertionScope()) + { + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["I9"].Should().NotBeNull(); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2); + } - // Act - P1 tries to place a piece where an opponent's piece resides. - var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); + // Act - P1 tries to place a piece where an opponent's piece resides. + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); - // Assert - using (new AssertionScope()) - { - dropSuccess.Should().BeFalse(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["I9"].Should().NotBeNull(); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); - } - } + // Assert + using (new AssertionScope()) + { + dropSuccess.Should().BeFalse(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.Board["I9"].Should().NotBeNull(); + shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2); + } + } - [Fact] - public void Check() - { - // Arrange - var moves = new[] - { + [Fact] + public void Check() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4"), // P2 Pawn new Move("G7", "G6"), - }; - var shogi = new Shogi(moves); + }; + var shogi = new Shogi(moves); - // Act - P1 Bishop, check - shogi.Move(new Move("B2", "G7")); + // Act - P1 Bishop, check + shogi.Move(new Move("B2", "G7")); - // Assert - shogi.InCheck.Should().Be(WhichPlayer.Player2); - } + // Assert + shogi.InCheck.Should().Be(WhichPerspective.Player2); + } - [Fact] - public void Promote() - { - // Arrange - var moves = new[] - { + [Fact] + public void Promote() + { + // Arrange + var moves = new[] + { // P1 Pawn new Move("C3", "C4" ), // P2 Pawn new Move("G7", "G6" ) - }; - var shogi = new Shogi(moves); + }; + var shogi = new Shogi(moves); - // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move("B2", "G7", true)); + // Act - P1 moves across promote threshold. + var moveSuccess = shogi.Move(new Move("B2", "G7", true)); - // Assert - using (new AssertionScope()) - { - moveSuccess.Should().BeTrue(); - shogi.Board["B2"].Should().BeNull(); - shogi.Board["G7"].Should().NotBeNull(); - shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Board["G7"].Owner.Should().Be(WhichPlayer.Player1); - shogi.Board["G7"].IsPromoted.Should().BeTrue(); - } - } + // Assert + using (new AssertionScope()) + { + moveSuccess.Should().BeTrue(); + shogi.Board["B2"].Should().BeNull(); + shogi.Board["G7"].Should().NotBeNull(); + shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board["G7"].Owner.Should().Be(WhichPerspective.Player1); + shogi.Board["G7"].IsPromoted.Should().BeTrue(); + } + } - [Fact] - public void CheckMate() - { - // Arrange - var moves = new[] - { + [Fact] + public void CheckMate() + { + // Arrange + var moves = new[] + { // P1 Rook new Move("H2", "E2"), // P2 Gold @@ -549,46 +575,46 @@ namespace Gameboard.ShogiUI.xUnitTests new Move("E6", "E7", true), // P2 King retreat new Move("E8", "E9"), - }; - var shogi = new Shogi(moves); - output.WriteLine(shogi.PrintStateAsAscii()); + }; + var shogi = new Shogi(moves); + output.WriteLine(shogi.PrintStateAsAscii()); - // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move("E7", "E8")); - output.WriteLine(shogi.PrintStateAsAscii()); + // Act - P1 Pawn wins by checkmate. + var moveSuccess = shogi.Move(new Move("E7", "E8")); + output.WriteLine(shogi.PrintStateAsAscii()); - // Assert - checkmate - moveSuccess.Should().BeTrue(); - shogi.IsCheckmate.Should().BeTrue(); - shogi.InCheck.Should().Be(WhichPlayer.Player2); - } + // Assert - checkmate + moveSuccess.Should().BeTrue(); + shogi.IsCheckmate.Should().BeTrue(); + shogi.InCheck.Should().Be(WhichPerspective.Player2); + } - [Fact] - public void Capture() - { - // Arrange - var moves = new[] - { - new Move("C3", "C4"), - new Move("G7", "G6") - }; - var shogi = new Shogi(moves); + [Fact] + public void Capture() + { + // Arrange + var moves = new[] + { + new Move("C3", "C4"), + new Move("G7", "G6") + }; + var shogi = new Shogi(moves); - // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move("B2", "H8")); + // Act - P1 Bishop captures P2 Bishop + var moveSuccess = shogi.Move(new Move("B2", "H8")); - // Assert - moveSuccess.Should().BeTrue(); - shogi.Board["B2"].Should().BeNull(); - shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Board["H8"].Owner.Should().Be(WhichPlayer.Player1); - shogi.Board.Values - .Where(p => p != null) - .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board["B2"].Should().BeNull(); + shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board["H8"].Owner.Should().Be(WhichPerspective.Player1); + shogi.Board.Values + .Where(p => p != null) + .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); - shogi.Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - } - } + shogi.Player1Hand + .Should() + .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPerspective.Player1); + } + } } From a18b7974c88559feeacbc6bc66e216701a8516c7 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 19 Dec 2021 17:52:23 -0600 Subject: [PATCH 22/27] checkpoint --- .../AnonymousSessionMiddleware.cs | 39 +++ .../Controllers/GameController.cs | 15 + Gameboard.ShogiUI.Sockets/Models/User.cs | 2 + Gameboard.ShogiUI.Sockets/Startup.cs | 327 +++++++++--------- Gameboard.ShogiUI.Sockets/appsettings.json | 3 +- 5 files changed, 231 insertions(+), 155 deletions(-) create mode 100644 Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs diff --git a/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs new file mode 100644 index 0000000..f39e649 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs @@ -0,0 +1,39 @@ +namespace Gameboard.ShogiUI.Sockets +{ + namespace anonymous_session.Middlewares + { + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Authentication; + using System.Security.Claims; + + /// + /// TODO: Use this example in the guest session logic instead of custom claims. + /// + public class AnonymousSessionMiddleware + { + private readonly RequestDelegate _next; + + public AnonymousSessionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async System.Threading.Tasks.Task InvokeAsync(HttpContext context) + { + if (!context.User.Identity.IsAuthenticated) + { + if (string.IsNullOrEmpty(context.User.FindFirstValue(ClaimTypes.Anonymous))) + { + var claim = new Claim(ClaimTypes.Anonymous, System.Guid.NewGuid().ToString()); + context.User.AddIdentity(new ClaimsIdentity(new[] { claim })); + + string scheme = Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme; + await context.SignInAsync(scheme, context.User, new AuthenticationProperties { IsPersistent = false }); + } + } + + await _next(context); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index db5f8b9..0f76e3b 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -241,6 +241,21 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return Ok(); } + [Authorize(Roles = "Admin, Shogi")] + [HttpDelete("{gameName}")] + public async Task DeleteSession([FromRoute] string gameName) + { + var user = await ReadUserOrThrow(); + if (user.IsAdmin) + { + return Ok(); + } + else + { + return Unauthorized(); + } + } + private async Task ReadUserOrThrow() { var user = await gameboardManager.ReadUser(User); diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index 7c1a2fa..e633524 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -37,6 +37,8 @@ namespace Gameboard.ShogiUI.Sockets.Models public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; + public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com"; + public User(string id, string displayName, WhichLoginPlatform platform) { Id = id; diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 5650206..c735d0e 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -30,169 +30,188 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets { - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton, JoinByCodeRequestValidator>(); - services.AddSingleton, JoinGameRequestValidator>(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(); - services.AddHttpClient("couchdb", c => - { - var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); - c.DefaultRequestHeaders.Add("Accept", "application/json"); - c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, JoinByCodeRequestValidator>(); + services.AddSingleton, JoinGameRequestValidator>(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddHttpClient("couchdb", c => + { + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); - var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; - c.BaseAddress = new Uri(baseUrl); - }); + var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; + c.BaseAddress = new Uri(baseUrl); + }); - services - .AddControllers() - .AddNewtonsoftJson(options => - { - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } - }; - options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); + services + .AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } + }; + options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); - services.AddAuthentication("CookieOrJwt") - .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options => - { - options.ForwardDefaultSelector = context => - { - var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false; - return bearerAuth - ? JwtBearerDefaults.AuthenticationScheme - : CookieAuthenticationDefaults.AuthenticationScheme; - }; - }) - .AddCookie(options => - { - options.Cookie.Name = "session-id"; - options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; - options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; - options.SlidingExpiration = true; - }) - .AddMicrosoftIdentityWebApi(Configuration); + services.AddAuthentication("CookieOrJwt") + .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options => + { + options.ForwardDefaultSelector = context => + { + var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false; + return bearerAuth + ? JwtBearerDefaults.AuthenticationScheme + : CookieAuthenticationDefaults.AuthenticationScheme; + }; + }) + .AddCookie(options => + { + options.Cookie.Name = "session-id"; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; + options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; + options.SlidingExpiration = true; + }) + .AddMicrosoftIdentityWebApi(Configuration); - services.AddSwaggerDocument(config => - { - config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme - { - Type = NSwag.OpenApiSecuritySchemeType.OAuth2, - Flow = NSwag.OpenApiOAuth2Flow.AccessCode, - AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token", - Scopes = new Dictionary { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } }, - Scheme = "Bearer" - }); - config.PostProcess = document => - { - document.Info.Title = "Gameboard.ShogiUI.Sockets"; - }; - }); + services.AddSwaggerDocument(config => + { + //config.AddSecurity("bearer", Enumerable.Empty(), new NSwag.OpenApiSecurityScheme + //{ + // Type = NSwag.OpenApiSecuritySchemeType.OAuth2, + // Flow = NSwag.OpenApiOAuth2Flow.Implicit, + // Flows = new NSwag.OpenApiOAuthFlows + // { + // Implicit = new NSwag.OpenApiOAuthFlow + // { + // Scopes = + // } + // } + //}); - // Remove default HttpClient logging. - services.RemoveAll(); - } + // This just ensures anyone with a microsoft account can make API calls. + config.AddSecurity("bearer", new NSwag.OpenApiSecurityScheme + { + Type = NSwag.OpenApiSecuritySchemeType.OAuth2, + Flow = NSwag.OpenApiOAuth2Flow.Implicit, + AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token", + Scopes = new Dictionary { + { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" }, + { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/ShogiAdmin", "Admin scope" } + }, + Scheme = "bearer", + BearerFormat = "JWT", + In = NSwag.OpenApiSecurityApiKeyLocation.Header, + }); + config.PostProcess = document => + { + document.Info.Title = "Gameboard.ShogiUI.Sockets"; + }; + }); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) - { - var origins = new[] { - "http://localhost:3000", "https://localhost:3000", - "http://127.0.0.1:3000", "https://127.0.0.1:3000", - "https://dev.lucaserver.space", "https://lucaserver.space" - }; - var socketOptions = new WebSocketOptions(); - foreach (var o in origins) - socketOptions.AllowedOrigins.Add(o); + // Remove default HttpClient logging. + services.RemoveAll(); + } - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - var client = PublicClientApplicationBuilder - .Create(Configuration["AzureAd:ClientId"]) - .WithLogging( - (level, message, pii) => - { + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) + { + var origins = new[] { + "http://localhost:3000", "https://localhost:3000", + "http://127.0.0.1:3000", "https://127.0.0.1:3000", + "https://dev.lucaserver.space", "https://lucaserver.space" + }; + var socketOptions = new WebSocketOptions(); + foreach (var o in origins) + socketOptions.AllowedOrigins.Add(o); - }, - LogLevel.Verbose, - true, - true - ) - .Build(); - } - else - { - app.UseHsts(); - } - app - .UseRequestResponseLogging() - .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) - .UseRouting() - .UseAuthentication() - .UseAuthorization() - .UseOpenApi() - .UseSwaggerUi3(config => - { - config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings() - { - ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff", - UsePkceWithAuthorizationCodeGrant = true - }; - //config.WithCredentials = true; - }) - .UseWebSockets(socketOptions) - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }) - .Use(async (context, next) => - { - if (context.WebSockets.IsWebSocketRequest) - { - await socketConnectionManager.HandleSocketRequest(context); - } - else - { - await next(); - } - }); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + var client = PublicClientApplicationBuilder + .Create(Configuration["AzureAd:ClientId"]) + .WithLogging( + (level, message, pii) => + { + Console.WriteLine(message); + }, + LogLevel.Verbose, + true, + true + ) + .Build(); + } + else + { + app.UseHsts(); + } + app + .UseRequestResponseLogging() + .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseOpenApi() + .UseSwaggerUi3(config => + { + config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings() + { + ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff", + UsePkceWithAuthorizationCodeGrant = true + }; + //config.WithCredentials = true; + }) + .UseWebSockets(socketOptions) + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }) + .Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + { + await socketConnectionManager.HandleSocketRequest(context); + } + else + { + await next(); + } + }); - JsonConvert.DefaultSettings = () => new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - ProcessDictionaryKeys = true - } - }, - Converters = new[] { new StringEnumConverter() }, - NullValueHandling = NullValueHandling.Ignore, - }; - } - } + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }, + Converters = new[] { new StringEnumConverter() }, + NullValueHandling = NullValueHandling.Ignore, + }; + } + } } diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 7abd699..119c533 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -16,7 +16,8 @@ "Instance": "https://login.microsoftonline.com/", "ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff", "TenantId": "common", - "Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff" + "Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff", + "ClientSecret": "" }, "AllowedHosts": "*" } \ No newline at end of file From aa4d5120e409fafdf1e152a810d42a3a6aa4dc48 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 19 Dec 2021 22:38:31 -0600 Subject: [PATCH 23/27] Split Shogi into ShogiBoardState and StandardRules --- Gameboard.ShogiUI.Sockets.sln | 6 + PathFinding/PathFinder2D.cs | 239 +++++++++++++-------------- Shogi.Domain/Move.cs | 53 ++++++ Shogi.Domain/MoveResult.cs | 14 ++ Shogi.Domain/Piece.cs | 44 +++++ Shogi.Domain/Shogi.Domain.csproj | 9 ++ Shogi.Domain/Shogi.cs | 90 +++++++++++ Shogi.Domain/ShogiBoardState.cs | 199 +++++++++++++++++++++++ Shogi.Domain/StandardRules.cs | 268 +++++++++++++++++++++++++++++++ Shogi.Domain/WhichPiece.cs | 14 ++ Shogi.Domain/WhichPlayer.cs | 8 + 11 files changed, 827 insertions(+), 117 deletions(-) create mode 100644 Shogi.Domain/Move.cs create mode 100644 Shogi.Domain/MoveResult.cs create mode 100644 Shogi.Domain/Piece.cs create mode 100644 Shogi.Domain/Shogi.Domain.csproj create mode 100644 Shogi.Domain/Shogi.cs create mode 100644 Shogi.Domain/ShogiBoardState.cs create mode 100644 Shogi.Domain/StandardRules.cs create mode 100644 Shogi.Domain/WhichPiece.cs create mode 100644 Shogi.Domain/WhichPlayer.cs diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 3ddc148..1469cf3 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU + {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index aa3f3b4..6a04823 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -5,132 +5,137 @@ using System.Numerics; namespace PathFinding { - public class PathFinder2D where T : IPlanarElement - { - /// Guaranteed to be non-null. - /// - public delegate void Callback(T collider, Vector2 position); + public class PathFinder2D where T : IPlanarElement + { + /// Guaranteed to be non-null. + /// + public delegate void Callback(T collider, Vector2 position); - private readonly IPlanarCollection collection; - private readonly int width; - private readonly int height; + private readonly IPlanarCollection collection; + private readonly int width; + private readonly int height; - /// Horizontal size, in steps, of the pathable plane. - /// Vertical size, in steps, of the pathable plane. - public PathFinder2D(IPlanarCollection collection, int width, int height) - { - this.collection = collection; - this.width = width; - this.height = height; - } + /// Horizontal size, in steps, of the pathable plane. + /// Vertical size, in steps, of the pathable plane. + public PathFinder2D(IPlanarCollection collection, int width, int height) + { + this.collection = collection; + this.width = width; + this.height = height; + } - /// - /// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin. - /// - /// The pathing element. - /// The starting location. - /// The destination. - /// Do cool stuff here. - /// True if the element reached the destination. - public bool PathTo(Vector2 origin, Vector2 destination, Callback? callback = null) - { - if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) - { - return false; - } - var element = collection[origin]; - if (element == null) return false; + /// + /// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin. + /// + /// The pathing element. + /// The starting location. + /// The destination. + /// Do cool stuff here. + /// True if the element reached the destination. + public bool PathTo(Vector2 origin, Vector2 destination, Callback? callback = null) + { + if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) + { + return false; + } + var element = collection[origin]; + if (element == null) return false; - var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); - if (!IsPathable(origin, destination, path.Direction)) - { - // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. - return false; - } + var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); + if (!IsPathable(origin, destination, path.Direction)) + { + // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. + return false; + } - var shouldPath = true; - var next = origin; - while (shouldPath && next != destination) - { - next = Vector2.Add(next, path.Direction); - var collider = collection[next]; - if (collider != null) - { - callback?.Invoke(collider, next); - shouldPath = false; - } - else if (path.Distance == Distance.OneStep) - { - shouldPath = false; - } - } - return next == destination; - } + var shouldPath = true; + var next = origin; + while (shouldPath && next != destination) + { + next = Vector2.Add(next, path.Direction); + var collider = collection[next]; + if (collider != null) + { + callback?.Invoke(collider, next); + shouldPath = false; + } + else if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + } + return next == destination; + } - public void PathEvery(Vector2 from, Callback callback) - { - var element = collection[from]; - if (element == null) - { - return; - } - foreach (var path in element.MoveSet.GetMoves()) - { - var shouldPath = true; - var next = Vector2.Add(from, path.Direction); ; - while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) - { - var collider = collection[(int)next.Y, (int)next.X]; - if (collider != null) - { - callback(collider, next); - shouldPath = false; - } - if (path.Distance == Distance.OneStep) - { - shouldPath = false; - } - next = Vector2.Add(next, path.Direction); - } - } - } + public void PathEvery(Vector2 from, Callback callback) + { + var element = collection[from]; + if (element == null) + { + return; + } + foreach (var path in element.MoveSet.GetMoves()) + { + var shouldPath = true; + var next = Vector2.Add(from, path.Direction); ; + while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) + { + var collider = collection[(int)next.Y, (int)next.X]; + if (collider != null) + { + callback(collider, next); + shouldPath = false; + } + if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + next = Vector2.Add(next, path.Direction); + } + } + } - /// - /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. - /// - public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback) - { - direction = Vector2.Normalize(direction); + /// + /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. + /// + public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback) + { + direction = Vector2.Normalize(direction); - var next = Vector2.Add(origin, direction); - while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) - { - var element = collection[next]; - if (element != null) callback(element, next); - next = Vector2.Add(next, direction); - } - } + var next = Vector2.Add(origin, direction); + while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) + { + var element = collection[next]; + if (element != null) callback(element, next); + next = Vector2.Add(next, direction); + } + } - public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => - paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); + public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => + paths.Aggregate((a, b) => + { + var distanceA = Vector2.Distance(destination, Vector2.Add(origin, a.Direction)); + var distanceB = Vector2.Distance(destination, Vector2.Add(origin, b.Direction)); + return distanceA < distanceB ? a : b; + }); - public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) - { - var next = Vector2.Add(origin, direction); - if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; + public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) + { + var next = Vector2.Add(origin, direction); + if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; - var slope = (destination.Y - origin.Y) / (destination.X - origin.X); - if (float.IsInfinity(slope)) - { - return next.X == destination.X; - } - else - { - // b = -mx + y - var yIntercept = -slope * origin.X + origin.Y; - // y = mx + b - return next.Y == slope * next.X + yIntercept; - } - } - } + var slope = (destination.Y - origin.Y) / (destination.X - origin.X); + if (float.IsInfinity(slope)) + { + return next.X == destination.X; + } + else + { + // b = -mx + y + var yIntercept = -slope * origin.X + origin.Y; + // y = mx + b + return next.Y == slope * next.X + yIntercept; + } + } + } } diff --git a/Shogi.Domain/Move.cs b/Shogi.Domain/Move.cs new file mode 100644 index 0000000..30d19d5 --- /dev/null +++ b/Shogi.Domain/Move.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Shogi.Domain +{ + [DebuggerDisplay("{From} - {To}")] + public class Move + { + public Vector2? From { get; } // TODO: Use string notation + public bool IsPromotion { get; } + public WhichPiece? PieceFromHand { get; } + public Vector2 To { get; } + + public Move(Vector2 from, Vector2 to, bool isPromotion = false) + { + From = from; + To = to; + IsPromotion = isPromotion; + } + public Move(WhichPiece pieceFromHand, Vector2 to) + { + PieceFromHand = pieceFromHand; + To = to; + } + + /// + /// Constructor to represent moving a piece on the Board to another position on the Board. + /// + /// Position the piece is being moved from. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(string fromNotation, string toNotation, bool isPromotion = false) + { + //From = NotationHelper.FromBoardNotation(fromNotation); + //To = NotationHelper.FromBoardNotation(toNotation); + //IsPromotion = isPromotion; + } + + /// + /// Constructor to represent moving a piece from the Hand to the Board. + /// + /// The piece being moved from the Hand to the Board. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false) + { + //From = null; + //PieceFromHand = pieceFromHand; + //To = NotationHelper.FromBoardNotation(toNotation); + //IsPromotion = isPromotion; + } + } +} diff --git a/Shogi.Domain/MoveResult.cs b/Shogi.Domain/MoveResult.cs new file mode 100644 index 0000000..fed07d6 --- /dev/null +++ b/Shogi.Domain/MoveResult.cs @@ -0,0 +1,14 @@ +namespace Shogi.Domain +{ + public class MoveResult + { + public bool Success { get; } + public string Reason { get; } + + public MoveResult(bool isSuccess, string reason = "") + { + Success = isSuccess; + Reason = reason; + } + } +} diff --git a/Shogi.Domain/Piece.cs b/Shogi.Domain/Piece.cs new file mode 100644 index 0000000..0ccc94f --- /dev/null +++ b/Shogi.Domain/Piece.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; + +namespace Shogi.Domain +{ + [DebuggerDisplay("{WhichPiece} {Owner}")] + public class Piece + { + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; + + public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = isPromoted; + } + public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted) + { + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void ToggleOwnership() + { + Owner = Owner == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; + } + + public void Promote() => IsPromoted = CanPromote; + + public void Demote() => IsPromoted = false; + + public void Capture() + { + ToggleOwnership(); + Demote(); + } + } +} diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj new file mode 100644 index 0000000..132c02c --- /dev/null +++ b/Shogi.Domain/Shogi.Domain.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs new file mode 100644 index 0000000..c7cbad6 --- /dev/null +++ b/Shogi.Domain/Shogi.cs @@ -0,0 +1,90 @@ +using System.Numerics; + +namespace Shogi.Domain +{ + /// + /// Facilitates Shogi board state transitions, cognisant of Shogi rules. + /// The board is always from Player1's perspective. + /// [0,0] is the lower-left position, [8,8] is the higher-right position + /// + public sealed class Shogi + { + private readonly ShogiBoardState board; + private readonly StandardRules rules; + public string Error { get; private set; } + + public Shogi(ShogiBoardState board) + { + this.board = board; + rules = new StandardRules(this.board); + Error = string.Empty; + } + + public Shogi(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!Move(moves[i])) + { + // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); + } + } + } + + public bool Move(Move move) + { + var moveSuccess = TryMove(move); + + if (!moveSuccess) + { + return false; + } + + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (EvaluateCheckAfterMove(move, otherPlayer)) + { + InCheck = otherPlayer; + IsCheckmate = EvaluateCheckmate(); + } + else + { + InCheck = null; + } + return true; + } + + /// + /// Attempts a given move. Returns false if the move is illegal. + /// + private bool TryMove(Move move) + { + // Try making the move in a "throw away" board. + var simulator = new StandardRules(new ShogiBoardState(this.board)); + + var simulatedMoveResults = move.PieceFromHand.HasValue + ? simulator.PlaceFromHand(move) + : simulator.PlaceFromBoard(move); + if (!simulatedMoveResults) + { + // Surface the error description. + Error = simulationBoard.Error; + return false; + } + // If already in check, assert the move that resulted in check no longer results in check. + if (InCheck == WhoseTurn) + { + if (simulationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) + { + // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + return false; + } + } + + // The move is valid and legal; update board state. + if (move.PieceFromHand.HasValue) PlaceFromHand(move); + else PlaceFromBoard(move); + return true; + } + } +} diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs new file mode 100644 index 0000000..83e70ae --- /dev/null +++ b/Shogi.Domain/ShogiBoardState.cs @@ -0,0 +1,199 @@ +using System.Numerics; +using System.Text.RegularExpressions; + +namespace Shogi.Domain +{ + // TODO: Avoid extending dictionary. Use composition instead. + // Then validation can occur when assigning a piece to a position. + public class ShogiBoardState + { + private static readonly string BoardNotationRegex = @"(?[a-iA-I])(?[1-9])"; + private static readonly char A = 'A'; + public delegate void ForEachDelegate(Piece element, Vector2 position); + /// + /// Key is position notation, such as "E4". + /// + private Dictionary board; + + public List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public List Player1Hand { get; } + public List Player2Hand { get; } + public List MoveHistory { get; } + public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; + public WhichPlayer? InCheck { get; private set; } + public bool IsCheckmate { get; private set; } + + public ShogiBoardState() + { + board = new Dictionary(81); + InitializeBoardState(); + Player1Hand = new List(); + Player2Hand = new List(); + MoveHistory = new List(); + } + + + /// + /// Copy constructor. + /// + public ShogiBoardState(ShogiBoardState other) : this() + { + foreach (var kvp in other.board) + { + board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); + } + MoveHistory.AddRange(other.MoveHistory); + Player1Hand.AddRange(other.Player1Hand); + Player2Hand.AddRange(other.Player2Hand); + } + + public Piece? this[string notation] + { + // TODO: Validate "notation" here and throw an exception if invalid. + get => board[notation.ToUpper()]; + set => board[notation.ToUpper()] = value; + } + + public Piece? this[Vector2 vector] + { + get => this[ToBoardNotation(vector)]; + set => this[ToBoardNotation(vector)] = value; + } + + public Piece? this[int x, int y] + { + get => this[ToBoardNotation(x, y)]; + set => this[ToBoardNotation(x, y)] = value; + } + + public void ForEachNotNull(ForEachDelegate callback) + { + for (var x = 0; x < 9; x++) + { + for (var y = 0; y < 9; y++) + { + var position = new Vector2(x, y); + var elem = this[position]; + if (elem != null) + callback(elem, position); + } + } + } + + public static string ToBoardNotation(Vector2 vector) + { + return ToBoardNotation((int)vector.X, (int)vector.Y); + } + public static string ToBoardNotation(int x, int y) + { + var file = (char)(x + A); + var rank = y + 1; + return $"{file}{rank}"; + } + public static Vector2 FromBoardNotation(string notation) + { + notation = notation.ToUpper(); + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank - 1); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); + } + + private void InitializeBoardState() + { + this["A1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + this["B1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); + this["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); + this["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); + this["E1"] = new Piece(WhichPiece.King, WhichPlayer.Player1); + this["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); + this["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); + this["H1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); + this["I1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + + this["A2"] = null; + this["B2"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player1); + this["C2"] = null; + this["D2"] = null; + this["E2"] = null; + this["F2"] = null; + this["G2"] = null; + this["H2"] = new Piece(WhichPiece.Rook, WhichPlayer.Player1); + this["I2"] = null; + + this["A3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["B3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["C3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["D3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["E3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["F3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["G3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["H3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["I3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + + this["A4"] = null; + this["B4"] = null; + this["C4"] = null; + this["D4"] = null; + this["E4"] = null; + this["F4"] = null; + this["G4"] = null; + this["H4"] = null; + this["I4"] = null; + + this["A5"] = null; + this["B5"] = null; + this["C5"] = null; + this["D5"] = null; + this["E5"] = null; + this["F5"] = null; + this["G5"] = null; + this["H5"] = null; + this["I5"] = null; + + this["A6"] = null; + this["B6"] = null; + this["C6"] = null; + this["D6"] = null; + this["E6"] = null; + this["F6"] = null; + this["G6"] = null; + this["H6"] = null; + this["I6"] = null; + + this["A7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["B7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["C7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["D7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["E7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["F7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["G7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["H7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["I7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + + this["A8"] = null; + this["B8"] = new Piece(WhichPiece.Rook, WhichPlayer.Player2); + this["C8"] = null; + this["D8"] = null; + this["E8"] = null; + this["F8"] = null; + this["G8"] = null; + this["H8"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player2); + this["I8"] = null; + + this["A9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); + this["B9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); + this["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); + this["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); + this["E9"] = new Piece(WhichPiece.King, WhichPlayer.Player2); + this["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); + this["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); + this["H9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); + this["I9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); + } + } +} diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs new file mode 100644 index 0000000..0f04c50 --- /dev/null +++ b/Shogi.Domain/StandardRules.cs @@ -0,0 +1,268 @@ +using System.Numerics; + +namespace Shogi.Domain +{ + internal class StandardRules + { + private readonly ShogiBoardState board; + private Vector2 player1KingPosition; + private Vector2 player2KingPosition; + + public StandardRules(ShogiBoardState board) + { + this.board = board; + CacheKingPositions(); + } + + private void CacheKingPositions() + { + this.board.ForEachNotNull((tile, position) => + { + if (tile.WhichPiece == WhichPiece.King) + { + if (tile.Owner == WhichPlayer.Player1) + { + player1KingPosition = position; + } + else if (tile.Owner == WhichPlayer.Player2) + { + player2KingPosition = position; + } + } + }); + } + + /// + /// Move a piece from a board tile to another board tile. + /// + /// The position of the piece being moved expressed in board notation. + /// The target position expressed in board notation. + /// A describing the success or failure of the simulation. + public MoveResult Move(string from, string to, bool isPromotion = false) + { + var fromPiece = board[from]; + if (fromPiece == null) + { + return new MoveResult(false, $"Tile [{from}] is empty. There is no piece to move."); + } + if (fromPiece.Owner != board.WhoseTurn) + { + return new MoveResult(false, "Not allowed to move the opponents piece"); + } + if (IsPathable(move.From.Value, move.To) == false) + { + return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); + } + + var captured = board[to]; + if (captured != null) + { + if (captured.Owner == board.WhoseTurn) + { + return new MoveResult(false, "Capturing your own piece is not allowed."); + } + captured.Capture(); + board.Hand.Add(captured); + } + + //Mutate the board. + if (isPromotion) + { + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); + if (board.WhoseTurn == WhichPlayer.Player1 && (toVector.Y > 5 || fromVector.Y > 5)) + { + fromPiece.Promote(); + } + else if (board.WhoseTurn == WhichPlayer.Player2 && (toVector.Y < 3 || fromVector.Y < 3)) + { + fromPiece.Promote(); + } + } + board[to] = fromPiece; + board[from] = null; + if (fromPiece.WhichPiece == WhichPiece.King) + { + if (fromPiece.Owner == WhichPlayer.Player1) + { + player1King.X = move.To.X; + player1King.Y = move.To.Y; + } + else if (fromPiece.Owner == WhichPlayer.Player2) + { + player2King.X = move.To.X; + player2King.Y = move.To.Y; + } + } + MoveHistory.Add(move); + return true; + } + + /// + /// Move a piece from the hand to the board. + /// + /// + /// The target position expressed in board notation. + /// A describing the success or failure of the simulation. + public void Move(WhichPiece pieceInHand, string to) + { + var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); + if (index == -1) + { + Error = $"{move.PieceFromHand} does not exist in the hand."; + return false; + } + if (Board[move.To] != null) + { + Error = $"Illegal move - attempting to capture while playing a piece from the hand."; + return false; + } + + switch (move.PieceFromHand!.Value) + { + case WhichPiece.Knight: + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6) + || (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2)) + { + Error = $"Knight has no valid moves after placed."; + return false; + } + break; + } + case WhichPiece.Lance: + case WhichPiece.Pawn: + { + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8) + || (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0)) + { + Error = $"{move.PieceFromHand} has no valid moves after placed."; + return false; + } + break; + } + } + + // Mutate the board. + Board[move.To] = Hand[index]; + Hand.RemoveAt(index); + MoveHistory.Add(move); + + return true; + } + + private bool IsPathable(Vector2 from, Vector2 to) + { + var piece = Board[from]; + if (piece == null) return false; + + var isObstructed = false; + var isPathable = pathFinder.PathTo(from, to, (other, position) => + { + if (other.Owner == piece.Owner) isObstructed = true; + }); + return !isObstructed && isPathable; + } + + private bool EvaluateCheckAfterMove(Move move, WhichPlayer WhichPerspective) + { + if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. + + var isCheck = false; + var kingPosition = WhichPerspective == WhichPlayer.Player1 ? player1King : player2King; + + // Check if the move put the king in check. + if (pathFinder.PathTo(move.To, kingPosition)) return true; + + if (move.From.HasValue) + { + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, move.From!.Value); + var slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) + { + // if slope of the move is also infinity...can skip this? + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective) + { + switch (piece.WhichPiece) + { + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; + } + } + }); + } + else if (slope == 1) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); + } + } + else + { + // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. + // Go read the shogi rules to be sure this is true. + } + + return isCheck; + } + + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; + + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, from) => // For each piece... + { + // Short circuit + if (!isCheckmate) return; + + if (piece.Owner == InCheck) // ...owned by the player in check... + { + // ...evaluate if any move gets the player out of check. + pathFinder.PathEvery(from, (other, position) => + { + var simulationBoard = new Shogi(this); + var moveToTry = new Move(from, position); + var moveSuccess = simulationBoard.TryMove(moveToTry); + if (moveSuccess) + { + if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) + { + isCheckmate = false; + } + } + }); + } + }); + return isCheckmate; + } + } +} diff --git a/Shogi.Domain/WhichPiece.cs b/Shogi.Domain/WhichPiece.cs new file mode 100644 index 0000000..58a1669 --- /dev/null +++ b/Shogi.Domain/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Shogi.Domain +{ + public enum WhichPiece + { + King, + GoldGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Shogi.Domain/WhichPlayer.cs b/Shogi.Domain/WhichPlayer.cs new file mode 100644 index 0000000..584d852 --- /dev/null +++ b/Shogi.Domain/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} From a2f3abb94e48646f2c7a3e1eae2a9afa8f989d5f Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 22 Dec 2021 17:43:32 -0600 Subject: [PATCH 24/27] yep --- Gameboard.ShogiUI.Sockets.sln | 9 +- .../Gameboard.ShogiUI.Sockets.csproj | 16 +- .../Gameboard.ShogiUI.UnitTests.csproj | 8 +- .../Gameboard.ShogiUI.xUnitTests.csproj | 4 +- .../Shogi.Domain.UnitTests.csproj | 28 ++ .../ShogiBoardStateShould.cs | 186 ++++++++ Shogi.Domain.UnitTests/ShogiShould.cs | 438 ++++++++++++++++++ Shogi.Domain/Direction.cs | 18 + Shogi.Domain/Distance.cs | 8 + Shogi.Domain/Move.cs | 58 +-- Shogi.Domain/MoveSet.cs | 106 +++++ Shogi.Domain/Shogi.cs | 119 ++--- Shogi.Domain/ShogiBoardState.cs | 6 +- Shogi.Domain/StandardRules.cs | 304 ++++++++---- 14 files changed, 1096 insertions(+), 212 deletions(-) create mode 100644 Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj create mode 100644 Shogi.Domain.UnitTests/ShogiBoardStateShould.cs create mode 100644 Shogi.Domain.UnitTests/ShogiShould.cs create mode 100644 Shogi.Domain/Direction.cs create mode 100644 Shogi.Domain/Distance.cs create mode 100644 Shogi.Domain/MoveSet.cs diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 1469cf3..3b54d4e 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -17,7 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,6 +55,10 @@ Global {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.Build.0 = Release|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +66,7 @@ Global GlobalSection(NestedProjects) = preSolution {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} {12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index bf7b641..e05ef31 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,15 +8,15 @@ - - - - - - - + + + + + + + - + diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index c11752d..16999c2 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj index 040179a..1de7d82 100644 --- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj b/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj new file mode 100644 index 0000000..724e08f --- /dev/null +++ b/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs new file mode 100644 index 0000000..470a3cb --- /dev/null +++ b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs @@ -0,0 +1,186 @@ +using FluentAssertions; +using Xunit; + +namespace Shogi.Domain.UnitTests +{ + public class ShogiBoardStateShould + { + [Fact] + public void InitializeBoardState() + { + // Act + var board = new ShogiBoardState(); + + // Assert + board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["A1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["A1"]?.IsPromoted.Should().Be(false); + board["B1"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["B1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B1"]?.IsPromoted.Should().Be(false); + board["C1"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["C1"]?.IsPromoted.Should().Be(false); + board["D1"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["D1"]?.IsPromoted.Should().Be(false); + board["E1"]?.WhichPiece.Should().Be(WhichPiece.King); + board["E1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["E1"]?.IsPromoted.Should().Be(false); + board["F1"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["F1"]?.IsPromoted.Should().Be(false); + board["G1"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["G1"]?.IsPromoted.Should().Be(false); + board["H1"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["H1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H1"]?.IsPromoted.Should().Be(false); + board["I1"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["I1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["I1"]?.IsPromoted.Should().Be(false); + + board["A2"].Should().BeNull(); + board["B2"]?.WhichPiece.Should().Be(WhichPiece.Bishop); + board["B2"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B2"]?.IsPromoted.Should().Be(false); + board["C2"].Should().BeNull(); + board["D2"].Should().BeNull(); + board["E2"].Should().BeNull(); + board["F2"].Should().BeNull(); + board["G2"].Should().BeNull(); + board["H2"]?.WhichPiece.Should().Be(WhichPiece.Rook); + board["H2"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H2"]?.IsPromoted.Should().Be(false); + board["I2"].Should().BeNull(); + + board["A3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["A3"]?.IsPromoted.Should().Be(false); + board["B3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["B3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B3"]?.IsPromoted.Should().Be(false); + board["C3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["C3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["C3"]?.IsPromoted.Should().Be(false); + board["D3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["D3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["D3"]?.IsPromoted.Should().Be(false); + board["E3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["E3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["E3"]?.IsPromoted.Should().Be(false); + board["F3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["F3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["F3"]?.IsPromoted.Should().Be(false); + board["G3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["G3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["G3"]?.IsPromoted.Should().Be(false); + board["H3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["H3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H3"]?.IsPromoted.Should().Be(false); + board["I3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["I3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["I3"]?.IsPromoted.Should().Be(false); + + board["A4"].Should().BeNull(); + board["B4"].Should().BeNull(); + board["C4"].Should().BeNull(); + board["D4"].Should().BeNull(); + board["E4"].Should().BeNull(); + board["F4"].Should().BeNull(); + board["G4"].Should().BeNull(); + board["H4"].Should().BeNull(); + board["I4"].Should().BeNull(); + + board["A5"].Should().BeNull(); + board["B5"].Should().BeNull(); + board["C5"].Should().BeNull(); + board["D5"].Should().BeNull(); + board["E5"].Should().BeNull(); + board["F5"].Should().BeNull(); + board["G5"].Should().BeNull(); + board["H5"].Should().BeNull(); + board["I5"].Should().BeNull(); + + board["A6"].Should().BeNull(); + board["B6"].Should().BeNull(); + board["C6"].Should().BeNull(); + board["D6"].Should().BeNull(); + board["E6"].Should().BeNull(); + board["F6"].Should().BeNull(); + board["G6"].Should().BeNull(); + board["H6"].Should().BeNull(); + board["I6"].Should().BeNull(); + + board["A7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["A7"]?.IsPromoted.Should().Be(false); + board["B7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["B7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B7"]?.IsPromoted.Should().Be(false); + board["C7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["C7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["C7"]?.IsPromoted.Should().Be(false); + board["D7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["D7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["D7"]?.IsPromoted.Should().Be(false); + board["E7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["E7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["E7"]?.IsPromoted.Should().Be(false); + board["F7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["F7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["F7"]?.IsPromoted.Should().Be(false); + board["G7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["G7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["G7"]?.IsPromoted.Should().Be(false); + board["H7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["H7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H7"]?.IsPromoted.Should().Be(false); + board["I7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["I7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["I7"]?.IsPromoted.Should().Be(false); + + board["A8"].Should().BeNull(); + board["B8"]?.WhichPiece.Should().Be(WhichPiece.Rook); + board["B8"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B8"]?.IsPromoted.Should().Be(false); + board["C8"].Should().BeNull(); + board["D8"].Should().BeNull(); + board["E8"].Should().BeNull(); + board["F8"].Should().BeNull(); + board["G8"].Should().BeNull(); + board["H8"]?.WhichPiece.Should().Be(WhichPiece.Bishop); + board["H8"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H8"]?.IsPromoted.Should().Be(false); + board["I8"].Should().BeNull(); + + board["A9"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["A9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["A9"]?.IsPromoted.Should().Be(false); + board["B9"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["B9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B9"]?.IsPromoted.Should().Be(false); + board["C9"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["C9"]?.IsPromoted.Should().Be(false); + board["D9"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["D9"]?.IsPromoted.Should().Be(false); + board["E9"]?.WhichPiece.Should().Be(WhichPiece.King); + board["E9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["E9"]?.IsPromoted.Should().Be(false); + board["F9"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["F9"]?.IsPromoted.Should().Be(false); + board["G9"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["G9"]?.IsPromoted.Should().Be(false); + board["H9"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["H9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H9"]?.IsPromoted.Should().Be(false); + board["I9"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["I9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["I9"]?.IsPromoted.Should().Be(false); + } + } +} diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs new file mode 100644 index 0000000..fc07cb5 --- /dev/null +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -0,0 +1,438 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Shogi.Domain.UnitTests +{ + public class ShogiShould + { + private readonly ITestOutputHelper output; + public ShogiShould(ITestOutputHelper output) + { + this.output = output; + } + + //[Fact] + //public void InitializeBoardStateWithMoves() + //{ + // var moves = new[] + // { + // // P1 Pawn + // new Move("A3", "A4") + // }; + // var shogi = new Shogi(moves); + // shogi.Board["A3"].Should().BeNull(); + // shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); + //} + + //[Fact] + //public void AllowValidMoves_AfterCheck() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7"), + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + + // // Act - P2 is able to un-check theirself. + // /// P2 King moves out of check + // var moveSuccess = shogi.Move(new Move("E9", "E8")); + + // // Assert + // using var _ = new AssertionScope(); + // moveSuccess.Should().BeTrue(); + // shogi.InCheck.Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveFromEmptyPosition() + //{ + // // Arrange + // var shogi = new Shogi(); + // shogi.Board["D5"].Should().BeNull(); + + // // Act + // var moveSuccess = shogi.Move(new Move("D5", "D6")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["D5"].Should().BeNull(); + // shogi.Board["D6"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveToCurrentPosition() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - P1 "moves" pawn to the position it already exists at. + // var moveSuccess = shogi.Move(new Move("A3", "A3")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveSet() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - Move Lance illegally + // var moveSuccess = shogi.Move(new Move("A1", "D5")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["A5"].Should().BeNull(); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_Ownership() + //{ + // // Arrange + // var shogi = new Shogi(); + // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + // shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2); + + // // Act - Move Player2 Pawn when it is Player1 turn. + // var moveSuccess = shogi.Move(new Move("A7", "A6")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Board["A6"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveThroughAllies() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - Move P1 Lance through P1 Pawn. + // var moveSuccess = shogi.Move(new Move("A1", "A5")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Board["A5"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_CaptureAlly() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - P1 Knight tries to capture P1 Pawn. + // var moveSuccess = shogi.Move(new Move("B1", "C3")); + + // // Arrange + // moveSuccess.Should().BeFalse(); + // shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + // shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7") + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + + // // Act - P2 moves Lance while in check. + // var moveSuccess = shogi.Move(new Move("I9", "I8")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["I8"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidDrops_MoveSet() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("I7", "I6"), + // // P1 Bishop takes P2 Pawn. + // new Move("B2", "G7"), + // // P2 Gold, block check from P1 Bishop. + // new Move("F9", "F8"), + // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + // new Move("G7", "H8", true), + // // P2 Pawn again + // new Move("I6", "I5"), + // // P1 Bishop takes P2 Knight + // new Move("H8", "H9"), + // // P2 Pawn again + // new Move("I5", "I4"), + // // P1 Bishop takes P2 Lance + // new Move("H9", "I9"), + // // P2 Pawn captures P1 Pawn + // new Move("I4", "I3") + // }; + // var shogi = new Shogi(moves); + // shogi.Player1Hand.Count.Should().Be(4); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + + // // Act | Assert - Illegally placing Knight from the hand in farthest row. + // shogi.Board["H9"].Should().BeNull(); + // var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + // dropSuccess.Should().BeFalse(); + // shogi.Board["H9"].Should().BeNull(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + + // // Act | Assert - Illegally placing Knight from the hand in second farthest row. + // shogi.Board["H8"].Should().BeNull(); + // dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); + // dropSuccess.Should().BeFalse(); + // shogi.Board["H8"].Should().BeNull(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + + // // Act | Assert - Illegally place Lance from the hand. + // shogi.Board["H9"].Should().BeNull(); + // dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + // dropSuccess.Should().BeFalse(); + // shogi.Board["H9"].Should().BeNull(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + + // // Act | Assert - Illegally place Pawn from the hand. + // shogi.Board["H9"].Should().BeNull(); + // dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); + // dropSuccess.Should().BeFalse(); + // shogi.Board["H9"].Should().BeNull(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + + // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // // TODO + //} + + //[Fact] + //public void PreventInvalidDrop_Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Pawn, arbitrary move. + // new Move("A3", "A4"), + // // P2 Bishop takes P1 Bishop + // new Move("H8", "B2"), + // // P1 Silver takes P2 Bishop + // new Move("C1", "B2"), + // // P2 Pawn, arbtrary move + // new Move("A7", "A6"), + // // P1 drop Bishop, place P2 in check + // new Move(WhichPiece.Bishop, "G7") + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.Board["E5"].Should().BeNull(); + + // // Act - P2 places a Bishop while in check. + // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); + + // // Assert + // dropSuccess.Should().BeFalse(); + // shogi.Board["E5"].Should().BeNull(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + //} + + //[Fact] + //public void PreventInvalidDrop_Capture() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop capture P2 Bishop + // new Move("B2", "H8"), + // // P2 Pawn + // new Move("G6", "G5") + // }; + // var shogi = new Shogi(moves); + // using (new AssertionScope()) + // { + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.Board["I9"].Should().NotBeNull(); + // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + // } + + // // Act - P1 tries to place a piece where an opponent's piece resides. + // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); + + // // Assert + // using (new AssertionScope()) + // { + // dropSuccess.Should().BeFalse(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.Board["I9"].Should().NotBeNull(); + // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + // } + //} + + //[Fact] + //public void Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // }; + // var shogi = new Shogi(moves); + + // // Act - P1 Bishop, check + // shogi.Move(new Move("B2", "G7")); + + // // Assert + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + //} + + //[Fact] + //public void Promote() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4" ), + // // P2 Pawn + // new Move("G7", "G6" ) + // }; + // var shogi = new Shogi(moves); + + // // Act - P1 moves across promote threshold. + // var moveSuccess = shogi.Move(new Move("B2", "G7", true)); + + // // Assert + // using (new AssertionScope()) + // { + // moveSuccess.Should().BeTrue(); + // shogi.Board["B2"].Should().BeNull(); + // shogi.Board["G7"].Should().NotBeNull(); + // shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); + // shogi.Board["G7"].Owner.Should().Be(WhichPlayer.Player1); + // shogi.Board["G7"].IsPromoted.Should().BeTrue(); + // } + //} + + //[Fact] + //public void CheckMate() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Rook + // new Move("H2", "E2"), + // // P2 Gold + // new Move("F9", "G8"), + // // P1 Pawn + // new Move("E3", "E4"), + // // P2 other Gold + // new Move("D9", "C8"), + // // P1 same Pawn + // new Move("E4", "E5"), + // // P2 Pawn + // new Move("E7", "E6"), + // // P1 Pawn takes P2 Pawn + // new Move("E5", "E6"), + // // P2 King + // new Move("E9", "E8"), + // // P1 Pawn promotes, threatens P2 King + // new Move("E6", "E7", true), + // // P2 King retreat + // new Move("E8", "E9"), + // }; + // var shogi = new Shogi(moves); + // output.WriteLine(shogi.PrintStateAsAscii()); + + // // Act - P1 Pawn wins by checkmate. + // var moveSuccess = shogi.Move(new Move("E7", "E8")); + // output.WriteLine(shogi.PrintStateAsAscii()); + + // // Assert - checkmate + // moveSuccess.Should().BeTrue(); + // shogi.IsCheckmate.Should().BeTrue(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + //} + + //[Fact] + //public void Capture() + //{ + // // Arrange + // var moves = new[] + // { + // new Move("C3", "C4"), + // new Move("G7", "G6") + // }; + // var shogi = new Shogi(moves); + + // // Act - P1 Bishop captures P2 Bishop + // var moveSuccess = shogi.Move(new Move("B2", "H8")); + + // // Assert + // moveSuccess.Should().BeTrue(); + // shogi.Board["B2"].Should().BeNull(); + // shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); + // shogi.Board["H8"].Owner.Should().Be(WhichPlayer.Player1); + // shogi.Board.Values + // .Where(p => p != null) + // .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); + + // shogi.Player1Hand + // .Should() + // .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + //} + } +} diff --git a/Shogi.Domain/Direction.cs b/Shogi.Domain/Direction.cs new file mode 100644 index 0000000..5316598 --- /dev/null +++ b/Shogi.Domain/Direction.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Shogi.Domain +{ + public static class Direction + { + public static readonly Vector2 Up = new(0, 1); + public static readonly Vector2 Down = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 UpLeft = new(-1, 1); + public static readonly Vector2 UpRight = new(1, 1); + public static readonly Vector2 DownLeft = new(-1, -1); + public static readonly Vector2 DownRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); + } +} diff --git a/Shogi.Domain/Distance.cs b/Shogi.Domain/Distance.cs new file mode 100644 index 0000000..9031228 --- /dev/null +++ b/Shogi.Domain/Distance.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain +{ + public enum Distance + { + OneStep, + MultiStep + } +} \ No newline at end of file diff --git a/Shogi.Domain/Move.cs b/Shogi.Domain/Move.cs index 30d19d5..1ad6f08 100644 --- a/Shogi.Domain/Move.cs +++ b/Shogi.Domain/Move.cs @@ -3,51 +3,15 @@ using System.Numerics; namespace Shogi.Domain { - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public Vector2? From { get; } // TODO: Use string notation - public bool IsPromotion { get; } - public WhichPiece? PieceFromHand { get; } - public Vector2 To { get; } - - public Move(Vector2 from, Vector2 to, bool isPromotion = false) - { - From = from; - To = to; - IsPromotion = isPromotion; - } - public Move(WhichPiece pieceFromHand, Vector2 to) - { - PieceFromHand = pieceFromHand; - To = to; - } - - /// - /// Constructor to represent moving a piece on the Board to another position on the Board. - /// - /// Position the piece is being moved from. - /// Position the piece is being moved to. - /// If the moving piece should be promoted. - public Move(string fromNotation, string toNotation, bool isPromotion = false) - { - //From = NotationHelper.FromBoardNotation(fromNotation); - //To = NotationHelper.FromBoardNotation(toNotation); - //IsPromotion = isPromotion; - } - - /// - /// Constructor to represent moving a piece from the Hand to the Board. - /// - /// The piece being moved from the Hand to the Board. - /// Position the piece is being moved to. - /// If the moving piece should be promoted. - public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false) - { - //From = null; - //PieceFromHand = pieceFromHand; - //To = NotationHelper.FromBoardNotation(toNotation); - //IsPromotion = isPromotion; - } - } + [DebuggerDisplay("{Direction} - {Distance}")] + public class Move + { + public Vector2 Direction { get; } + public Distance Distance { get; } + public Move(Vector2 direction, Distance distance = Distance.OneStep) + { + Direction = direction; + Distance = distance; + } + } } diff --git a/Shogi.Domain/MoveSet.cs b/Shogi.Domain/MoveSet.cs new file mode 100644 index 0000000..b1b2d2d --- /dev/null +++ b/Shogi.Domain/MoveSet.cs @@ -0,0 +1,106 @@ +using System.Numerics; + +namespace Shogi.Domain +{ + public class MoveSet + { + + public static readonly MoveSet King = new(new List(8) + { + new Move(Direction.Up), + new Move(Direction.Left), + new Move(Direction.Right), + new Move(Direction.Down), + new Move(Direction.UpLeft), + new Move(Direction.UpRight), + new Move(Direction.DownLeft), + new Move(Direction.DownRight) + }); + + public static readonly MoveSet Bishop = new(new List(4) + { + new Move(Direction.UpLeft, Distance.MultiStep), + new Move(Direction.UpRight, Distance.MultiStep), + new Move(Direction.DownLeft, Distance.MultiStep), + new Move(Direction.DownRight, Distance.MultiStep) + }); + + public static readonly MoveSet PromotedBishop = new(new List(8) + { + new Move(Direction.Up), + new Move(Direction.Left), + new Move(Direction.Right), + new Move(Direction.Down), + new Move(Direction.UpLeft, Distance.MultiStep), + new Move(Direction.UpRight, Distance.MultiStep), + new Move(Direction.DownLeft, Distance.MultiStep), + new Move(Direction.DownRight, Distance.MultiStep) + }); + + public static readonly MoveSet GoldGeneral = new(new List(6) + { + new Move(Direction.Up), + new Move(Direction.UpLeft), + new Move(Direction.UpRight), + new Move(Direction.Left), + new Move(Direction.Right), + new Move(Direction.Down) + }); + + public static readonly MoveSet Knight = new(new List(2) + { + new Move(Direction.KnightLeft), + new Move(Direction.KnightRight) + }); + + public static readonly MoveSet Lance = new(new List(1) + { + new Move(Direction.Up, Distance.MultiStep), + }); + + public static readonly MoveSet Pawn = new(new List(1) + { + new Move(Direction.Up) + }); + + public static readonly MoveSet Rook = new(new List(4) + { + new Move(Direction.Up, Distance.MultiStep), + new Move(Direction.Left, Distance.MultiStep), + new Move(Direction.Right, Distance.MultiStep), + new Move(Direction.Down, Distance.MultiStep) + }); + + public static readonly MoveSet PromotedRook = new(new List(8) + { + new Move(Direction.Up, Distance.MultiStep), + new Move(Direction.Left, Distance.MultiStep), + new Move(Direction.Right, Distance.MultiStep), + new Move(Direction.Down, Distance.MultiStep), + new Move(Direction.UpLeft), + new Move(Direction.UpRight), + new Move(Direction.DownLeft), + new Move(Direction.DownRight) + }); + + public static readonly MoveSet SilverGeneral = new(new List(4) + { + new Move(Direction.Up), + new Move(Direction.UpLeft), + new Move(Direction.UpRight), + new Move(Direction.DownLeft), + new Move(Direction.DownRight) + }); + + private readonly ICollection moves; + private readonly ICollection upsidedownMoves; + + private MoveSet(ICollection moves) + { + this.moves = moves; + upsidedownMoves = moves.Select(m => new Move(Vector2.Negate(m.Direction), m.Distance)).ToList(); + } + + public ICollection GetMoves(bool isUpsideDown) => isUpsideDown ? upsidedownMoves : moves; + } +} diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index c7cbad6..cb0f990 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -1,6 +1,4 @@ -using System.Numerics; - -namespace Shogi.Domain +namespace Shogi.Domain { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. @@ -17,74 +15,89 @@ namespace Shogi.Domain { this.board = board; rules = new StandardRules(this.board); - Error = string.Empty; } - public Shogi(IList moves) : this() + //public Shogi(IList moves) : this(new ShogiBoardState()) + //{ + // for (var i = 0; i < moves.Count; i++) + // { + // if (!Move(moves[i])) + // { + // throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + // } + // } + //} + + + public MoveResult CanMove(string from, string to, bool isPromotion) { - for (var i = 0; i < moves.Count; i++) - { - if (!Move(moves[i])) - { - // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); - } - } + // TODO: ShogiBoardState.FromBoardNotation should not throw an execption in this query method. + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); + var simulator = new StandardRules(new ShogiBoardState(board)); + return simulator.Move(fromVector, toVector, isPromotion); } - public bool Move(Move move) + public MoveResult CanMove(WhichPiece pieceInHand, string to) { - var moveSuccess = TryMove(move); + var toVector = ShogiBoardState.FromBoardNotation(to); + var simulator = new StandardRules(new ShogiBoardState(board)); + return simulator.Move(pieceInHand, toVector); + } - if (!moveSuccess) + public void Move(string from, string to, bool isPromotion) + { + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); + var moveResult = rules.Move(fromVector, toVector, isPromotion); + if (!moveResult.Success) { - return false; + throw new InvalidOperationException(moveResult.Reason); } - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (EvaluateCheckAfterMove(move, otherPlayer)) + var otherPlayer = board.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (rules.EvaluateCheckAfterMove(fromVector, toVector, otherPlayer)) { - InCheck = otherPlayer; - IsCheckmate = EvaluateCheckmate(); + board.InCheck = otherPlayer; + board.IsCheckmate = rules.EvaluateCheckmate(); } else { - InCheck = null; + board.InCheck = null; } - return true; } - /// - /// Attempts a given move. Returns false if the move is illegal. - /// - private bool TryMove(Move move) - { - // Try making the move in a "throw away" board. - var simulator = new StandardRules(new ShogiBoardState(this.board)); + ///// + ///// Attempts a given move. Returns false if the move is illegal. + ///// + //private bool TryMove(Move move) + //{ + // // Try making the move in a "throw away" board. + // var simulator = new StandardRules(new ShogiBoardState(this.board)); - var simulatedMoveResults = move.PieceFromHand.HasValue - ? simulator.PlaceFromHand(move) - : simulator.PlaceFromBoard(move); - if (!simulatedMoveResults) - { - // Surface the error description. - Error = simulationBoard.Error; - return false; - } - // If already in check, assert the move that resulted in check no longer results in check. - if (InCheck == WhoseTurn) - { - if (simulationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) - { - // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - return false; - } - } + // var simulatedMoveResults = move.PieceFromHand.HasValue + // ? simulator.PlaceFromHand(move) + // : simulator.PlaceFromBoard(move); + // if (!simulatedMoveResults) + // { + // // Surface the error description. + // Error = simulationBoard.Error; + // return false; + // } + // // If already in check, assert the move that resulted in check no longer results in check. + // if (InCheck == WhoseTurn) + // { + // // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + // if (simulationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) + // { + // return false; + // } + // } - // The move is valid and legal; update board state. - if (move.PieceFromHand.HasValue) PlaceFromHand(move); - else PlaceFromBoard(move); - return true; - } + // // The move is valid and legal; update board state. + // if (move.PieceFromHand.HasValue) PlaceFromHand(move); + // else PlaceFromBoard(move); + // return true; + //} } } diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index 83e70ae..49e9683 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -13,15 +13,15 @@ namespace Shogi.Domain /// /// Key is position notation, such as "E4". /// - private Dictionary board; + private readonly Dictionary board; public List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; public List Player1Hand { get; } public List Player2Hand { get; } public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; - public WhichPlayer? InCheck { get; private set; } - public bool IsCheckmate { get; private set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } public ShogiBoardState() { diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 0f04c50..aa9816c 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -4,6 +4,10 @@ namespace Shogi.Domain { internal class StandardRules { + /// Guaranteed to be non-null. + /// + public delegate void Callback(Piece collider, Vector2 position); + private readonly ShogiBoardState board; private Vector2 player1KingPosition; private Vector2 player2KingPosition; @@ -38,18 +42,20 @@ namespace Shogi.Domain /// The position of the piece being moved expressed in board notation. /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public MoveResult Move(string from, string to, bool isPromotion = false) + public MoveResult Move(Vector2 from, Vector2 to, bool isPromotion = false) { var fromPiece = board[from]; if (fromPiece == null) { return new MoveResult(false, $"Tile [{from}] is empty. There is no piece to move."); } + if (fromPiece.Owner != board.WhoseTurn) { return new MoveResult(false, "Not allowed to move the opponents piece"); } - if (IsPathable(move.From.Value, move.To) == false) + + if (IsPathable(from, to) == false) { return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); } @@ -68,13 +74,11 @@ namespace Shogi.Domain //Mutate the board. if (isPromotion) { - var fromVector = ShogiBoardState.FromBoardNotation(from); - var toVector = ShogiBoardState.FromBoardNotation(to); - if (board.WhoseTurn == WhichPlayer.Player1 && (toVector.Y > 5 || fromVector.Y > 5)) + if (board.WhoseTurn == WhichPlayer.Player1 && (to.Y > 5 || from.Y > 5)) { fromPiece.Promote(); } - else if (board.WhoseTurn == WhichPlayer.Player2 && (toVector.Y < 3 || fromVector.Y < 3)) + else if (board.WhoseTurn == WhichPlayer.Player2 && (to.Y < 3 || from.Y < 3)) { fromPiece.Promote(); } @@ -85,17 +89,15 @@ namespace Shogi.Domain { if (fromPiece.Owner == WhichPlayer.Player1) { - player1King.X = move.To.X; - player1King.Y = move.To.Y; + player1KingPosition = from; } else if (fromPiece.Owner == WhichPlayer.Player2) { - player2King.X = move.To.X; - player2King.Y = move.To.Y; + player2KingPosition = from; } } - MoveHistory.Add(move); - return true; + //MoveHistory.Add(move); + return new MoveResult(true); } /// @@ -104,30 +106,27 @@ namespace Shogi.Domain /// /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public void Move(WhichPiece pieceInHand, string to) + public MoveResult Move(WhichPiece pieceInHand, Vector2 to) { - var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); + var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { - Error = $"{move.PieceFromHand} does not exist in the hand."; - return false; + return new MoveResult(false, $"{pieceInHand} does not exist in the hand."); } - if (Board[move.To] != null) + if (board[to] != null) { - Error = $"Illegal move - attempting to capture while playing a piece from the hand."; - return false; + return new MoveResult(false, $"Illegal move - attempting to capture while playing a piece from the hand."); } - switch (move.PieceFromHand!.Value) + switch (pieceInHand) { case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2)) + if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) + || (board.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) { - Error = $"Knight has no valid moves after placed."; - return false; + return new MoveResult(false, "Knight has no valid moves after placed."); } break; } @@ -135,134 +134,251 @@ namespace Shogi.Domain case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0)) + if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y == 8) + || (board.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) { - Error = $"{move.PieceFromHand} has no valid moves after placed."; - return false; + return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); } break; } } // Mutate the board. - Board[move.To] = Hand[index]; - Hand.RemoveAt(index); - MoveHistory.Add(move); - - return true; + board[to] = board.Hand[index]; + board.Hand.RemoveAt(index); + //MoveHistory.Add(move); + return new MoveResult(true); } private bool IsPathable(Vector2 from, Vector2 to) { - var piece = Board[from]; + var piece = board[from]; if (piece == null) return false; var isObstructed = false; - var isPathable = pathFinder.PathTo(from, to, (other, position) => + var isPathable = PathTo(from, to, (other, position) => { if (other.Owner == piece.Owner) isObstructed = true; }); return !isObstructed && isPathable; } - private bool EvaluateCheckAfterMove(Move move, WhichPlayer WhichPerspective) + public bool EvaluateCheckAfterMove(WhichPiece pieceInHand, Vector2 to, WhichPlayer whichPlayer) { - if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. + if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. var isCheck = false; - var kingPosition = WhichPerspective == WhichPlayer.Player1 ? player1King : player2King; + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1KingPosition : player2KingPosition; // Check if the move put the king in check. - if (pathFinder.PathTo(move.To, kingPosition)) return true; + if (PathTo(to, kingPosition)) return true; - if (move.From.HasValue) + // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. + // Go read the shogi rules to be sure this is true. + + return isCheck; + } + + public bool EvaluateCheckAfterMove(Vector2 from, Vector2 to, WhichPlayer whichPlayer) + { + if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. + + var isCheck = false; + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1KingPosition : player2KingPosition; + + // Check if the move put the king in check. + if (PathTo(to, kingPosition)) return true; + + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, from); + var slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) { - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From!.Value); - var slope = Math.Abs(direction.Y / direction.X); - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) + // if slope of the move is also infinity...can skip this? + LinePathTo(kingPosition, direction, (piece, position) => { - // if slope of the move is also infinity...can skip this? - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + if (piece.Owner != whichPlayer) { - if (piece.Owner != WhichPerspective) + switch (piece.WhichPiece) { - switch (piece.WhichPiece) - { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; - } + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; } - }); - } - else if (slope == 1) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) - { - isCheck = true; - } - }); - } - else if (slope == 0) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) - { - isCheck = true; - } - }); - } + } + }); } - else + else if (slope == 1) { - // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. - // Go read the shogi rules to be sure this is true. + LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); } return isCheck; } - private bool EvaluateCheckmate() + public bool EvaluateCheckmate() { - if (!InCheck.HasValue) return false; + if (!board.InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; - Board.ForEachNotNull((piece, from) => // For each piece... + board.ForEachNotNull((piece, from) => // For each piece... { // Short circuit if (!isCheckmate) return; - if (piece.Owner == InCheck) // ...owned by the player in check... + if (piece.Owner == board.InCheck) // ...owned by the player in check... { // ...evaluate if any move gets the player out of check. - pathFinder.PathEvery(from, (other, position) => + PathEvery(from, (other, position) => { - var simulationBoard = new Shogi(this); - var moveToTry = new Move(from, position); - var moveSuccess = simulationBoard.TryMove(moveToTry); - if (moveSuccess) + var simulationBoard = new StandardRules(new ShogiBoardState(board)); + var simulationResult = simulationBoard.Move(from, position, false); + if (simulationResult.Success) { - if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) + if (!EvaluateCheckAfterMove(from, position, board.InCheck.Value)) { isCheckmate = false; } } }); } + // TODO: Assert that a player could not place a piece from their hand to avoid check. }); return isCheckmate; } + + /// + /// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin. + /// + /// The pathing element. + /// The starting location. + /// The destination. + /// Do cool stuff here. + /// True if the element reached the destination. + public bool PathTo(Vector2 origin, Vector2 destination, Callback? callback = null) + { + if (destination.X > 8 || destination.Y > 8 || destination.X < 0 || destination.Y < 0) + { + return false; + } + var piece = board[origin]; + if (piece == null) return false; + + var path = FindDirectionTowardsDestination(GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown), origin, destination); + if (!IsPathable(origin, destination)) + { + // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. + return false; + } + + var shouldPath = true; + var next = origin; + while (shouldPath && next != destination) + { + next = Vector2.Add(next, path.Direction); + var collider = board[next]; + if (collider != null) + { + callback?.Invoke(collider, next); + shouldPath = false; + } + else if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + } + return next == destination; + } + + public void PathEvery(Vector2 from, Callback callback) + { + var piece = board[from]; + if (piece == null) + { + return; + } + foreach (var path in GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown)) + { + var shouldPath = true; + var next = Vector2.Add(from, path.Direction); ; + while (shouldPath && next.X < 8 && next.Y < 8 && next.X >= 0 && next.Y >= 0) + { + var collider = board[(int)next.Y, (int)next.X]; + if (collider != null) + { + callback(collider, next); + shouldPath = false; + } + if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + next = Vector2.Add(next, path.Direction); + } + } + } + + /// + /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. + /// + public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback) + { + direction = Vector2.Normalize(direction); + + var next = Vector2.Add(origin, direction); + while (next.X >= 0 && next.X < 8 && next.Y >= 0 && next.Y < 8) + { + var element = board[next]; + if (element != null) callback(element, next); + next = Vector2.Add(next, direction); + } + } + + public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => + paths.Aggregate((a, b) => + { + var distanceA = Vector2.Distance(destination, Vector2.Add(origin, a.Direction)); + var distanceB = Vector2.Distance(destination, Vector2.Add(origin, b.Direction)); + return distanceA < distanceB ? a : b; + }); + + public static MoveSet GetMoveSet(WhichPiece whichPiece) + { + return whichPiece switch + { + WhichPiece.King => MoveSet.King, + WhichPiece.GoldGeneral => MoveSet.GoldGeneral, + WhichPiece.SilverGeneral => MoveSet.SilverGeneral, + WhichPiece.Bishop => MoveSet.Bishop, + WhichPiece.Rook => MoveSet.Rook, + WhichPiece.Knight => MoveSet.Knight, + WhichPiece.Lance => MoveSet.Lance, + WhichPiece.Pawn => MoveSet.Pawn, + _ => throw new ArgumentException($"{nameof(WhichPiece)} not recognized."), + }; + } } } From 9ec91615a3bbde714cc5ac95a94ce8f4393ee8bb Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 29 Dec 2021 16:27:43 -0600 Subject: [PATCH 25/27] yep --- .../Properties/launchSettings.json | 6 +- Shogi.Domain.UnitTests/ShogiShould.cs | 170 +++++++++--------- Shogi.Domain.UnitTests/StandardRulesShould.cs | 31 ++++ Shogi.Domain/Shogi.cs | 10 +- Shogi.Domain/StandardRules.cs | 42 ++++- 5 files changed, 157 insertions(+), 102 deletions(-) create mode 100644 Shogi.Domain.UnitTests/StandardRulesShould.cs diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json index 230be61..05ff5bf 100644 --- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json +++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json @@ -1,13 +1,13 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:63676", - "sslPort": 44396 + "applicationUrl": "http://localhost:50728/", + "sslPort": 44315 } }, - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs index fc07cb5..745e28f 100644 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -17,10 +17,12 @@ namespace Shogi.Domain.UnitTests //[Fact] //public void InitializeBoardStateWithMoves() //{ + // var board = new ShogiBoardState(); + // var rules = new StandardRules(board); // var moves = new[] // { - // // P1 Pawn - // new Move("A3", "A4") + // // P1 Pawn + // new Move("A3", "A4") // }; // var shogi = new Shogi(moves); // shogi.Board["A3"].Should().BeNull(); @@ -33,12 +35,12 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop puts P2 in check - // new Move("B2", "G7"), + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7"), // }; // var shogi = new Shogi(moves); // shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -158,12 +160,12 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop puts P2 in check - // new Move("B2", "G7") + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7") // }; // var shogi = new Shogi(moves); // shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -184,26 +186,26 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("I7", "I6"), - // // P1 Bishop takes P2 Pawn. - // new Move("B2", "G7"), - // // P2 Gold, block check from P1 Bishop. - // new Move("F9", "F8"), - // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - // new Move("G7", "H8", true), - // // P2 Pawn again - // new Move("I6", "I5"), - // // P1 Bishop takes P2 Knight - // new Move("H8", "H9"), - // // P2 Pawn again - // new Move("I5", "I4"), - // // P1 Bishop takes P2 Lance - // new Move("H9", "I9"), - // // P2 Pawn captures P1 Pawn - // new Move("I4", "I3") + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("I7", "I6"), + // // P1 Bishop takes P2 Pawn. + // new Move("B2", "G7"), + // // P2 Gold, block check from P1 Bishop. + // new Move("F9", "F8"), + // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + // new Move("G7", "H8", true), + // // P2 Pawn again + // new Move("I6", "I5"), + // // P1 Bishop takes P2 Knight + // new Move("H8", "H9"), + // // P2 Pawn again + // new Move("I5", "I4"), + // // P1 Bishop takes P2 Lance + // new Move("H9", "I9"), + // // P2 Pawn captures P1 Pawn + // new Move("I4", "I3") // }; // var shogi = new Shogi(moves); // shogi.Player1Hand.Count.Should().Be(4); @@ -251,20 +253,20 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Pawn, arbitrary move. - // new Move("A3", "A4"), - // // P2 Bishop takes P1 Bishop - // new Move("H8", "B2"), - // // P1 Silver takes P2 Bishop - // new Move("C1", "B2"), - // // P2 Pawn, arbtrary move - // new Move("A7", "A6"), - // // P1 drop Bishop, place P2 in check - // new Move(WhichPiece.Bishop, "G7") + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Pawn, arbitrary move. + // new Move("A3", "A4"), + // // P2 Bishop takes P1 Bishop + // new Move("H8", "B2"), + // // P1 Silver takes P2 Bishop + // new Move("C1", "B2"), + // // P2 Pawn, arbtrary move + // new Move("A7", "A6"), + // // P1 drop Bishop, place P2 in check + // new Move(WhichPiece.Bishop, "G7") // }; // var shogi = new Shogi(moves); // shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -287,14 +289,14 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop capture P2 Bishop - // new Move("B2", "H8"), - // // P2 Pawn - // new Move("G6", "G5") + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop capture P2 Bishop + // new Move("B2", "H8"), + // // P2 Pawn + // new Move("G6", "G5") // }; // var shogi = new Shogi(moves); // using (new AssertionScope()) @@ -325,10 +327,10 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), // }; // var shogi = new Shogi(moves); @@ -345,10 +347,10 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Pawn - // new Move("C3", "C4" ), - // // P2 Pawn - // new Move("G7", "G6" ) + // // P1 Pawn + // new Move("C3", "C4" ), + // // P2 Pawn + // new Move("G7", "G6" ) // }; // var shogi = new Shogi(moves); @@ -373,26 +375,26 @@ namespace Shogi.Domain.UnitTests // // Arrange // var moves = new[] // { - // // P1 Rook - // new Move("H2", "E2"), - // // P2 Gold - // new Move("F9", "G8"), - // // P1 Pawn - // new Move("E3", "E4"), - // // P2 other Gold - // new Move("D9", "C8"), - // // P1 same Pawn - // new Move("E4", "E5"), - // // P2 Pawn - // new Move("E7", "E6"), - // // P1 Pawn takes P2 Pawn - // new Move("E5", "E6"), - // // P2 King - // new Move("E9", "E8"), - // // P1 Pawn promotes, threatens P2 King - // new Move("E6", "E7", true), - // // P2 King retreat - // new Move("E8", "E9"), + // // P1 Rook + // new Move("H2", "E2"), + // // P2 Gold + // new Move("F9", "G8"), + // // P1 Pawn + // new Move("E3", "E4"), + // // P2 other Gold + // new Move("D9", "C8"), + // // P1 same Pawn + // new Move("E4", "E5"), + // // P2 Pawn + // new Move("E7", "E6"), + // // P1 Pawn takes P2 Pawn + // new Move("E5", "E6"), + // // P2 King + // new Move("E9", "E8"), + // // P1 Pawn promotes, threatens P2 King + // new Move("E6", "E7", true), + // // P2 King retreat + // new Move("E8", "E9"), // }; // var shogi = new Shogi(moves); // output.WriteLine(shogi.PrintStateAsAscii()); diff --git a/Shogi.Domain.UnitTests/StandardRulesShould.cs b/Shogi.Domain.UnitTests/StandardRulesShould.cs new file mode 100644 index 0000000..7f132f3 --- /dev/null +++ b/Shogi.Domain.UnitTests/StandardRulesShould.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace Shogi.Domain.UnitTests +{ + public class StandardRulesShould + { + [Fact] + public void AllowValidMoves_AfterCheck() + { + // Arrange + var board = new ShogiBoardState(); + var rules = new StandardRules(board); + rules.Move("C3", "C4"); // P1 Pawn + rules.Move("G7", "G6"); // P2 Pawn + rules.Move("B2", "G7"); // P1 Bishop puts P2 in check + board.InCheck.Should().Be(WhichPlayer.Player2); + + // Act - P2 is able to un-check theirself. + /// P2 King moves out of check + var moveSuccess = rules.Move("E9", "E8"); + + // Assert + using var _ = new AssertionScope(); + moveSuccess.Success.Should().BeTrue(); + moveSuccess.Reason.Should().BeEmpty(); + board.InCheck.Should().BeNull(); + } + } +} diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index cb0f990..1aca58a 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -31,25 +31,21 @@ public MoveResult CanMove(string from, string to, bool isPromotion) { - // TODO: ShogiBoardState.FromBoardNotation should not throw an execption in this query method. - var fromVector = ShogiBoardState.FromBoardNotation(from); - var toVector = ShogiBoardState.FromBoardNotation(to); var simulator = new StandardRules(new ShogiBoardState(board)); - return simulator.Move(fromVector, toVector, isPromotion); + return simulator.Move(from, to, isPromotion); } public MoveResult CanMove(WhichPiece pieceInHand, string to) { - var toVector = ShogiBoardState.FromBoardNotation(to); var simulator = new StandardRules(new ShogiBoardState(board)); - return simulator.Move(pieceInHand, toVector); + return simulator.Move(pieceInHand, to); } public void Move(string from, string to, bool isPromotion) { var fromVector = ShogiBoardState.FromBoardNotation(from); var toVector = ShogiBoardState.FromBoardNotation(to); - var moveResult = rules.Move(fromVector, toVector, isPromotion); + var moveResult = rules.Move(from, to, isPromotion); if (!moveResult.Success) { throw new InvalidOperationException(moveResult.Reason); diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index aa9816c..8b89f5d 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -1,5 +1,7 @@ using System.Numerics; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Shogi.Domain.UnitTests")] namespace Shogi.Domain { internal class StandardRules @@ -39,11 +41,13 @@ namespace Shogi.Domain /// /// Move a piece from a board tile to another board tile. /// - /// The position of the piece being moved expressed in board notation. - /// The target position expressed in board notation. + /// The position of the piece being moved expressed in board notation. + /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public MoveResult Move(Vector2 from, Vector2 to, bool isPromotion = false) + public MoveResult Move(string fromNotation, string toNotation, bool isPromotion = false) { + var from = ShogiBoardState.FromBoardNotation(fromNotation); + var to = ShogiBoardState.FromBoardNotation(toNotation); var fromPiece = board[from]; if (fromPiece == null) { @@ -55,7 +59,7 @@ namespace Shogi.Domain return new MoveResult(false, "Not allowed to move the opponents piece"); } - if (IsPathable(from, to) == false) + if (ShogiIsPathable(from, to) == false) { return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); } @@ -106,8 +110,9 @@ namespace Shogi.Domain /// /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public MoveResult Move(WhichPiece pieceInHand, Vector2 to) + public MoveResult Move(WhichPiece pieceInHand, string toNotation) { + var to = ShogiBoardState.FromBoardNotation(toNotation); var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { @@ -150,7 +155,7 @@ namespace Shogi.Domain return new MoveResult(true); } - private bool IsPathable(Vector2 from, Vector2 to) + private bool ShogiIsPathable(Vector2 from, Vector2 to) { var piece = board[from]; if (piece == null) return false; @@ -255,7 +260,9 @@ namespace Shogi.Domain PathEvery(from, (other, position) => { var simulationBoard = new StandardRules(new ShogiBoardState(board)); - var simulationResult = simulationBoard.Move(from, position, false); + var fromNotation = ShogiBoardState.ToBoardNotation(from); + var toNotation = ShogiBoardState.ToBoardNotation(position); + var simulationResult = simulationBoard.Move(fromNotation, toNotation, false); if (simulationResult.Success) { if (!EvaluateCheckAfterMove(from, position, board.InCheck.Value)) @@ -288,7 +295,7 @@ namespace Shogi.Domain if (piece == null) return false; var path = FindDirectionTowardsDestination(GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown), origin, destination); - if (!IsPathable(origin, destination)) + if (!IsPathable(origin, destination, path.Direction)) { // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. return false; @@ -341,6 +348,25 @@ namespace Shogi.Domain } } + public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) + { + var next = Vector2.Add(origin, direction); + if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; + + var slope = (destination.Y - origin.Y) / (destination.X - origin.X); + if (float.IsInfinity(slope)) + { + return next.X == destination.X; + } + else + { + // b = -mx + y + var yIntercept = -slope * origin.X + origin.Y; + // y = mx + b + return next.Y == slope * next.X + yIntercept; + } + } + /// /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. /// From 499e48085116910267250560e9da643d6c2ad44a Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 29 Dec 2021 22:11:49 -0600 Subject: [PATCH 26/27] yep --- Shogi.Domain.UnitTests/ShogiShould.cs | 104 +++++++++++++------------- Shogi.Domain/Shogi.cs | 1 + Shogi.Domain/ShogiBoardState.cs | 2 +- Shogi.Domain/StandardRules.cs | 2 +- 4 files changed, 57 insertions(+), 52 deletions(-) diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs index 745e28f..9e7724e 100644 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -1,5 +1,6 @@ using FluentAssertions; using FluentAssertions.Execution; +using System; using System.Linq; using Xunit; using Xunit.Abstractions; @@ -14,62 +15,65 @@ namespace Shogi.Domain.UnitTests this.output = output; } - //[Fact] - //public void InitializeBoardStateWithMoves() - //{ - // var board = new ShogiBoardState(); - // var rules = new StandardRules(board); - // var moves = new[] - // { - // // P1 Pawn - // new Move("A3", "A4") - // }; - // var shogi = new Shogi(moves); - // shogi.Board["A3"].Should().BeNull(); - // shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); - //} + [Fact] + public void MoveAPieceToAnEmptyPosition() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); + board["A4"].Should().BeNull(); + var expectedPiece = board["A3"]; + expectedPiece.Should().NotBeNull(); - //[Fact] - //public void AllowValidMoves_AfterCheck() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop puts P2 in check - // new Move("B2", "G7"), - // }; - // var shogi = new Shogi(moves); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // Act + shogi.Move("A3", "A4", false); - // // Act - P2 is able to un-check theirself. - // /// P2 King moves out of check - // var moveSuccess = shogi.Move(new Move("E9", "E8")); + // Assert + board["A3"].Should().BeNull(); + board["A4"].Should().Be(expectedPiece); + } - // // Assert - // using var _ = new AssertionScope(); - // moveSuccess.Should().BeTrue(); - // shogi.InCheck.Should().BeNull(); - //} + [Fact] + public void AllowValidMoves_AfterCheck() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop puts P2 in check + shogi.Move("B2", "G7", false); + board.InCheck.Should().Be(WhichPlayer.Player2); - //[Fact] - //public void PreventInvalidMoves_MoveFromEmptyPosition() - //{ - // // Arrange - // var shogi = new Shogi(); - // shogi.Board["D5"].Should().BeNull(); + // Act - P2 is able to un-check theirself. + /// P2 King moves out of check + shogi.Move("E9", "E8", false); - // // Act - // var moveSuccess = shogi.Move(new Move("D5", "D6")); + // Assert + using (new AssertionScope()) + { + board.InCheck.Should().BeNull(); + } + } - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.Board["D5"].Should().BeNull(); - // shogi.Board["D6"].Should().BeNull(); - //} + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); + board["D5"].Should().BeNull(); + + // Act + var act = () => shogi.Move("D5", "D6", false); + + // Assert + act.Should().Throw(); + board["D5"].Should().BeNull(); + board["D6"].Should().BeNull(); + } //[Fact] //public void PreventInvalidMoves_MoveToCurrentPosition() diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index 1aca58a..2cf501c 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -61,6 +61,7 @@ { board.InCheck = null; } + board.WhoseTurn = otherPlayer; } ///// diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index 49e9683..216b4b3 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -19,7 +19,7 @@ namespace Shogi.Domain public List Player1Hand { get; } public List Player2Hand { get; } public List MoveHistory { get; } - public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; + public WhichPlayer WhoseTurn { get; set; } public WhichPlayer? InCheck { get; set; } public bool IsCheckmate { get; set; } diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 8b89f5d..9d8d908 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -51,7 +51,7 @@ namespace Shogi.Domain var fromPiece = board[from]; if (fromPiece == null) { - return new MoveResult(false, $"Tile [{from}] is empty. There is no piece to move."); + return new MoveResult(false, $"Tile [{fromNotation}] is empty. There is no piece to move."); } if (fromPiece.Owner != board.WhoseTurn) From f79a1312c7c20dbe3cfcd91bc642dbd1e35f6913 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 29 Jan 2022 10:07:53 -0600 Subject: [PATCH 27/27] yep --- Shogi.Domain.UnitTests/ShogiShould.cs | 318 ++++++++++++++------------ Shogi.Domain/Shogi.cs | 20 +- Shogi.Domain/ShogiBoardState.cs | 3 + Shogi.Domain/StandardRules.cs | 10 +- 4 files changed, 190 insertions(+), 161 deletions(-) diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs index 9e7724e..2e8f734 100644 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -75,181 +75,195 @@ namespace Shogi.Domain.UnitTests board["D6"].Should().BeNull(); } - //[Fact] - //public void PreventInvalidMoves_MoveToCurrentPosition() - //{ - // // Arrange - // var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); - // // Act - P1 "moves" pawn to the position it already exists at. - // var moveSuccess = shogi.Move(new Move("A3", "A3")); + // Act - P1 "moves" pawn to the position it already exists at. + var act = () => shogi.Move("A3", "A3", false); - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - // shogi.Player1Hand.Should().BeEmpty(); - // shogi.Player2Hand.Should().BeEmpty(); - //} + // Assert + act.Should().Throw(); + board["A3"].Should().NotBeNull(); + board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } - //[Fact] - //public void PreventInvalidMoves_MoveSet() - //{ - // // Arrange - // var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); - // // Act - Move Lance illegally - // var moveSuccess = shogi.Move(new Move("A1", "D5")); + // Act - Move Lance illegally + var act = () => shogi.Move("A1", "D5", false); - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - // shogi.Board["A5"].Should().BeNull(); - // shogi.Player1Hand.Should().BeEmpty(); - // shogi.Player2Hand.Should().BeEmpty(); - //} + // Assert + act.Should().Throw(); + board["A1"].Should().NotBeNull(); + board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); + board["A5"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } - //[Fact] - //public void PreventInvalidMoves_Ownership() - //{ - // // Arrange - // var shogi = new Shogi(); - // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); - // shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2); + [Fact] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); + board["A7"].Should().NotBeNull(); + board["A7"]!.Owner.Should().Be(WhichPlayer.Player2); - // // Act - Move Player2 Pawn when it is Player1 turn. - // var moveSuccess = shogi.Move(new Move("A7", "A6")); + // Act - Move Player2 Pawn when it is Player1 turn. + var act = () => shogi.Move("A7", "A6", false); - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); - // shogi.Board["A6"].Should().BeNull(); - //} + // Assert + act.Should().Throw(); + board["A7"].Should().NotBeNull(); + board["A7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A6"].Should().BeNull(); + } - //[Fact] - //public void PreventInvalidMoves_MoveThroughAllies() - //{ - // // Arrange - // var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_MoveThroughAllies() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); - // // Act - Move P1 Lance through P1 Pawn. - // var moveSuccess = shogi.Move(new Move("A1", "A5")); + // Act - Move P1 Lance through P1 Pawn. + var act = () => shogi.Move("A1", "A5", false); - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - // shogi.Board["A5"].Should().BeNull(); - //} + // Assert + act.Should().Throw(); + board["A1"].Should().NotBeNull(); + board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); + board["A3"].Should().NotBeNull(); + board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A5"].Should().BeNull(); + } - //[Fact] - //public void PreventInvalidMoves_CaptureAlly() - //{ - // // Arrange - // var shogi = new Shogi(); + [Fact] + public void PreventInvalidMoves_CaptureAlly() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); - // // Act - P1 Knight tries to capture P1 Pawn. - // var moveSuccess = shogi.Move(new Move("B1", "C3")); + // Act - P1 Knight tries to capture P1 Pawn. + var act = () => shogi.Move("B1", "C3", false); - // // Arrange - // moveSuccess.Should().BeFalse(); - // shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); - // shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); - // shogi.Player1Hand.Should().BeEmpty(); - // shogi.Player2Hand.Should().BeEmpty(); - //} + // Arrange + act.Should().Throw(); + board["B1"].Should().NotBeNull(); + board["B1"]!.WhichPiece.Should().Be(WhichPiece.Knight); + board["C3"].Should().NotBeNull(); + board["C3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } - //[Fact] - //public void PreventInvalidMoves_Check() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop puts P2 in check - // new Move("B2", "G7") - // }; - // var shogi = new Shogi(moves); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); + [Fact] + public void PreventInvalidMoves_Check() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop puts P2 in check + shogi.Move("B2", "G7", false); + board.InCheck.Should().Be(WhichPlayer.Player2); - // // Act - P2 moves Lance while in check. - // var moveSuccess = shogi.Move(new Move("I9", "I8")); + // Act - P2 moves Lance while in check. + var act = () => shogi.Move("I9", "I8", false); - // // Assert - // moveSuccess.Should().BeFalse(); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // shogi.Board["I8"].Should().BeNull(); - //} + // Assert + act.Should().Throw(); + board.InCheck.Should().Be(WhichPlayer.Player2); + board["I9"].Should().NotBeNull(); + board["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); + board["I8"].Should().BeNull(); + } - //[Fact] - //public void PreventInvalidDrops_MoveSet() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("I7", "I6"), - // // P1 Bishop takes P2 Pawn. - // new Move("B2", "G7"), - // // P2 Gold, block check from P1 Bishop. - // new Move("F9", "F8"), - // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - // new Move("G7", "H8", true), - // // P2 Pawn again - // new Move("I6", "I5"), - // // P1 Bishop takes P2 Knight - // new Move("H8", "H9"), - // // P2 Pawn again - // new Move("I5", "I4"), - // // P1 Bishop takes P2 Lance - // new Move("H9", "I9"), - // // P2 Pawn captures P1 Pawn - // new Move("I4", "I3") - // }; - // var shogi = new Shogi(moves); - // shogi.Player1Hand.Count.Should().Be(4); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + [Fact] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var board = new ShogiBoardState(); + var shogi = new Shogi(board); - // // Act | Assert - Illegally placing Knight from the hand in farthest row. - // shogi.Board["H9"].Should().BeNull(); - // var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - // dropSuccess.Should().BeFalse(); - // shogi.Board["H9"].Should().BeNull(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // // Act | Assert - Illegally placing Knight from the hand in second farthest row. - // shogi.Board["H8"].Should().BeNull(); - // dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); - // dropSuccess.Should().BeFalse(); - // shogi.Board["H8"].Should().BeNull(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("I7", "I6", false); + // P1 Bishop takes P2 Pawn. + shogi.Move("B2", "G7", false); + // P2 Gold, block check from P1 Bishop. + shogi.Move("F9", "F8", false); + // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + shogi.Move("G7", "H8", true); + // P2 Pawn again + shogi.Move("I6", "I5", false); + // P1 Bishop takes P2 Knight + shogi.Move("H8", "H9", false); + // P2 Pawn again + shogi.Move("I5", "I4", false); + // P1 Bishop takes P2 Lance + shogi.Move("H9", "I9", false); + // P2 Pawn captures P1 Pawn + shogi.Move("I4", "I3", false); - // // Act | Assert - Illegally place Lance from the hand. - // shogi.Board["H9"].Should().BeNull(); - // dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - // dropSuccess.Should().BeFalse(); - // shogi.Board["H9"].Should().BeNull(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + board.Player1Hand.Count.Should().Be(4); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); - // // Act | Assert - Illegally place Pawn from the hand. - // shogi.Board["H9"].Should().BeNull(); - // dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); - // dropSuccess.Should().BeFalse(); - // shogi.Board["H9"].Should().BeNull(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // Act | Assert - Illegally placing Knight from the hand in farthest row. + board["H9"].Should().BeNull(); + shogi.Move() + var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // // TODO - //} + // Act | Assert - Illegally placing Knight from the hand in second farthest row. + shogi.Board["H8"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); + dropSuccess.Should().BeFalse(); + shogi.Board["H8"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + + // Act | Assert - Illegally place Lance from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + + // Act | Assert - Illegally place Pawn from the hand. + shogi.Board["H9"].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); + dropSuccess.Should().BeFalse(); + shogi.Board["H9"].Should().BeNull(); + shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + + // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // TODO + } //[Fact] //public void PreventInvalidDrop_Check() diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index 2cf501c..b895ffa 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -43,16 +43,24 @@ public void Move(string from, string to, bool isPromotion) { - var fromVector = ShogiBoardState.FromBoardNotation(from); - var toVector = ShogiBoardState.FromBoardNotation(to); - var moveResult = rules.Move(from, to, isPromotion); + var tempBoard = new ShogiBoardState(board); + var simulation = new StandardRules(tempBoard); + var moveResult = simulation.Move(from, to, isPromotion); if (!moveResult.Success) { throw new InvalidOperationException(moveResult.Reason); } + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); var otherPlayer = board.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (rules.EvaluateCheckAfterMove(fromVector, toVector, otherPlayer)) + if (simulation.IsPlayerInCheckAfterMove(fromVector, toVector, board.WhoseTurn)) + { + throw new InvalidOperationException("Illegal move. This move places you in check."); + } + + rules.Move(from, to, isPromotion); + if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) { board.InCheck = otherPlayer; board.IsCheckmate = rules.EvaluateCheckmate(); @@ -64,6 +72,10 @@ board.WhoseTurn = otherPlayer; } + public void Move(WhichPiece pieceInHand, string to) + { + + } ///// ///// Attempts a given move. Returns false if the move is illegal. ///// diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index 216b4b3..3d240bf 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -42,6 +42,9 @@ namespace Shogi.Domain { board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); } + WhoseTurn = other.WhoseTurn; + InCheck = other.InCheck; + IsCheckmate = other.IsCheckmate; MoveHistory.AddRange(other.MoveHistory); Player1Hand.AddRange(other.Player1Hand); Player2Hand.AddRange(other.Player2Hand); diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 9d8d908..27e1f70 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -39,11 +39,11 @@ namespace Shogi.Domain } /// - /// Move a piece from a board tile to another board tile. + /// Move a piece from a board tile to another board tile ignorant of check or check-mate. /// /// The position of the piece being moved expressed in board notation. /// The target position expressed in board notation. - /// A describing the success or failure of the simulation. + /// A describing the success or failure of the move. public MoveResult Move(string fromNotation, string toNotation, bool isPromotion = false) { var from = ShogiBoardState.FromBoardNotation(fromNotation); @@ -105,7 +105,7 @@ namespace Shogi.Domain } /// - /// Move a piece from the hand to the board. + /// Move a piece from the hand to the board ignorant if check or check-mate. /// /// /// The target position expressed in board notation. @@ -184,7 +184,7 @@ namespace Shogi.Domain return isCheck; } - public bool EvaluateCheckAfterMove(Vector2 from, Vector2 to, WhichPlayer whichPlayer) + public bool IsPlayerInCheckAfterMove(Vector2 from, Vector2 to, WhichPlayer whichPlayer) { if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. @@ -265,7 +265,7 @@ namespace Shogi.Domain var simulationResult = simulationBoard.Move(fromNotation, toNotation, false); if (simulationResult.Success) { - if (!EvaluateCheckAfterMove(from, position, board.InCheck.Value)) + if (!IsPlayerInCheckAfterMove(from, position, board.InCheck.Value)) { isCheckmate = false; }