From b10f61a48965d1262eaa81f2d7162997ebc6ecbe Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 1 Aug 2021 17:32:43 -0500 Subject: [PATCH] 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(); - } -}