From 178cb00253cc1a6a061bdfcb9ad2196d572a44a9 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 26 Jul 2021 06:28:56 -0500 Subject: [PATCH] Before changing Piece[,] to Dictionary --- Benchmarking/Benchmarking.csproj | 4 - Benchmarking/Benchmarks.cs | 12 - .../Gameboard.ShogiUI.Rules.csproj | 14 - Gameboard.ShogiUI.Rules/Move.cs | 27 -- Gameboard.ShogiUI.Rules/Pieces/Bishop.cs | 39 --- Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs | 30 -- Gameboard.ShogiUI.Rules/Pieces/King.cs | 32 -- Gameboard.ShogiUI.Rules/Pieces/Knight.cs | 27 -- Gameboard.ShogiUI.Rules/Pieces/Lance.cs | 26 -- Gameboard.ShogiUI.Rules/Pieces/Pawn.cs | 26 -- Gameboard.ShogiUI.Rules/Pieces/Piece.cs | 47 --- Gameboard.ShogiUI.Rules/Pieces/Rook.cs | 39 --- .../Pieces/SilverGeneral.cs | 29 -- Gameboard.ShogiUI.Rules/WhichPiece.cs | 14 - Gameboard.ShogiUI.Rules/WhichPlayer.cs | 8 - .../Socket/Messages/JoinGame.cs | 7 +- .../Socket/Messages/ListGames.cs | 5 +- .../Socket/Messages/LoadGame.cs | 5 +- .../Socket/Types/BoardState.cs | 2 + .../Socket/Types/Game.cs | 27 +- Gameboard.ShogiUI.Sockets.sln | 11 +- .../Controllers/GameController.cs | 9 +- .../Controllers/SocketController.cs | 5 +- .../Extensions/ModelExtensions.cs | 13 +- .../Gameboard.ShogiUI.Sockets.csproj | 11 +- .../Managers/ActiveSessionManager.cs | 33 ++ .../Managers/BoardManager.cs | 32 -- .../ClientActionHandlers/CreateGameHandler.cs | 19 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 4 +- .../ClientActionHandlers/JoinGameHandler.cs | 52 ++- .../ClientActionHandlers/ListGamesHandler.cs | 12 +- .../ClientActionHandlers/LoadGameHandler.cs | 28 +- .../ClientActionHandlers/MoveHandler.cs | 51 +-- .../GameboardManager.cs} | 55 +++- .../Managers/SocketCommunicationManager.cs | 140 -------- .../Managers/SocketConnectionManager.cs | 221 ++++++------- .../Models/BoardState.cs | 66 ---- Gameboard.ShogiUI.Sockets/Models/Coords.cs | 41 --- Gameboard.ShogiUI.Sockets/Models/Move.cs | 73 ++++- Gameboard.ShogiUI.Sockets/Models/MoveSets.cs | 95 ++++++ Gameboard.ShogiUI.Sockets/Models/Piece.cs | 52 ++- Gameboard.ShogiUI.Sockets/Models/Player.cs | 12 - Gameboard.ShogiUI.Sockets/Models/Session.cs | 55 +--- .../Models/SessionMetadata.cs | 30 ++ .../Models/Shogi.cs | 101 +++--- .../Repositories/CouchModels/BoardState.cs | 75 ----- .../CouchModels/BoardStateDocument.cs | 57 ++++ .../Repositories/CouchModels/CouchDocument.cs | 22 +- .../Repositories/CouchModels/Move.cs | 23 +- .../Repositories/CouchModels/Piece.cs | 2 +- .../Repositories/CouchModels/Readme.md | 4 - .../Repositories/CouchModels/Session.cs | 30 -- .../CouchModels/SessionDocument.cs | 48 +++ .../CouchModels/{User.cs => UserDocument.cs} | 8 +- .../CouchModels/WhichDocumentType.cs | 9 + .../Repositories/GameboardRepository.cs | 124 ++++--- .../CreateGameRequestValidator.cs | 15 + .../JoinByCodeRequestValidator.cs | 15 + .../JoinGameRequestValidator.cs | 15 + .../ListGamesRequestValidator.cs | 14 + .../LoadGameRequestValidator.cs | 15 + .../RequestValidators/MoveRequestValidator.cs | 23 ++ .../Services/SocketService.cs | 194 +++++++++++ Gameboard.ShogiUI.Sockets/Startup.cs | 30 +- .../Gameboard.ShogiUI.UnitTests.csproj | 2 + .../PathFinding/PlanarCollectionShould.cs | 92 ++++++ .../Rules/ShogiBoardShould.cs | 309 +++++++++--------- .../Sockets/CoordsModelShould.cs | 27 -- Gameboard.ShogiUI.xUnitTests/GameShould.cs | 17 + .../Gameboard.ShogiUI.xUnitTests.csproj | 28 ++ .../MoveRequestValidatorShould.cs | 76 +++++ PathFinding/PathFinder2D.cs | 18 +- .../PlanarCollection.cs | 22 +- 73 files changed, 1537 insertions(+), 1418 deletions(-) delete mode 100644 Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj delete mode 100644 Gameboard.ShogiUI.Rules/Move.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Bishop.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/King.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Knight.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Lance.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Pawn.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Piece.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/Rook.cs delete mode 100644 Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs delete mode 100644 Gameboard.ShogiUI.Rules/WhichPiece.cs delete mode 100644 Gameboard.ShogiUI.Rules/WhichPlayer.cs rename Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs => Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs (83%) create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs rename Gameboard.ShogiUI.Sockets/{Repositories/RepositoryManagers/GameboardRepositoryManager.cs => Managers/GameboardManager.cs} (50%) delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/BoardState.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/Coords.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/MoveSets.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/Player.cs create mode 100644 Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs rename Gameboard.ShogiUI.Rules/ShogiBoard.cs => Gameboard.ShogiUI.Sockets/Models/Shogi.cs (81%) delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs rename Gameboard.ShogiUI.Sockets/Repositories/CouchModels/{User.cs => UserDocument.cs} (58%) create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs create mode 100644 Gameboard.ShogiUI.Sockets/Services/SocketService.cs create mode 100644 Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs delete mode 100644 Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/GameShould.cs create mode 100644 Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj create mode 100644 Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs rename {Gameboard.ShogiUI.Rules => PathFinding}/PlanarCollection.cs (72%) diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index d84d541..4e77937 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -10,8 +10,4 @@ - - - - diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 31f6599..5c7c052 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; -using Gameboard.ShogiUI.Rules; using System; using System.Linq; using System.Numerics; @@ -10,7 +9,6 @@ namespace Benchmarking { public class Benchmarks { - private readonly Move[] moves; private readonly Vector2[] directions; private readonly Consumer consumer = new(); @@ -48,21 +46,11 @@ namespace Benchmarking //[Benchmark] public void One() { - var board = new ShogiBoard(); - foreach (var move in moves) - { - board.Move(move); - } } //[Benchmark] public void Two() { - var board = new ShogiBoard(); - foreach (var move in moves) - { - //board.TryMove2(move); - } } diff --git a/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj deleted file mode 100644 index 5befd78..0000000 --- a/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net5.0 - true - 5 - enable - - - - - - - diff --git a/Gameboard.ShogiUI.Rules/Move.cs b/Gameboard.ShogiUI.Rules/Move.cs deleted file mode 100644 index 81286fe..0000000 --- a/Gameboard.ShogiUI.Rules/Move.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics; -using System.Numerics; - -namespace Gameboard.ShogiUI.Rules -{ - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public WhichPiece? PieceFromHand { get; } - public Vector2? From { get; } - public Vector2 To { get; } - public bool IsPromotion { get; } - - public Move(Vector2 from, Vector2 to, bool isPromotion) - { - From = from; - To = to; - IsPromotion = isPromotion; - } - - public Move(WhichPiece pieceFromHand, Vector2 to) - { - PieceFromHand = pieceFromHand; - To = to; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs b/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs deleted file mode 100644 index 3c433cc..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Bishop : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - private static readonly List PromotedMoves = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, PromotedMoves); - } - - public override Piece DeepClone() - { - var clone = new Bishop(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs deleted file mode 100644 index fa984ab..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs +++ /dev/null @@ -1,30 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class GoldenGeneral : Piece - { - public static readonly List Moves = new(6) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down) - }; - public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, Moves); - } - - public override Piece DeepClone() - { - var clone = new GoldenGeneral(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/King.cs b/Gameboard.ShogiUI.Rules/Pieces/King.cs deleted file mode 100644 index ab4d9c4..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/King.cs +++ /dev/null @@ -1,32 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class King : Piece - { - private static readonly List Moves = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public King(WhichPlayer owner) : base(WhichPiece.King, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, Moves); - } - - public override Piece DeepClone() - { - var clone = new King(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Knight.cs b/Gameboard.ShogiUI.Rules/Pieces/Knight.cs deleted file mode 100644 index 7091ceb..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Knight.cs +++ /dev/null @@ -1,27 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Knight : Piece - { - private static readonly List Moves = new(2) - { - new PathFinding.Move(Direction.KnightLeft), - new PathFinding.Move(Direction.KnightRight) - }; - - public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Knight(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Lance.cs b/Gameboard.ShogiUI.Rules/Pieces/Lance.cs deleted file mode 100644 index 48be6a2..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Lance.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Lance : Piece - { - private static readonly List Moves = new(1) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - }; - - public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Lance(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs b/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs deleted file mode 100644 index 1005c6f..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Pawn : Piece - { - private static readonly List Moves = new(1) - { - new PathFinding.Move(Direction.Up) - }; - - public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new Pawn(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Piece.cs b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs deleted file mode 100644 index b64584b..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Piece.cs +++ /dev/null @@ -1,47 +0,0 @@ -using PathFinding; -using System.Diagnostics; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public abstract class Piece : IPlanarElement - { - protected MoveSet promotedMoveSet; - protected MoveSet moveSet; - - public MoveSet MoveSet => IsPromoted ? promotedMoveSet : moveSet; - public abstract Piece DeepClone(); - public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } - public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPlayer.Player2; - - public Piece(WhichPiece piece, WhichPlayer owner) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = false; - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void ToggleOwnership() - { - Owner = Owner == WhichPlayer.Player1 - ? WhichPlayer.Player2 - : WhichPlayer.Player1; - } - - public void Promote() => IsPromoted = CanPromote; - - public void Demote() => IsPromoted = false; - - public void Capture() - { - ToggleOwnership(); - Demote(); - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/Rook.cs b/Gameboard.ShogiUI.Rules/Pieces/Rook.cs deleted file mode 100644 index 3a990a4..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/Rook.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class Rook : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep) - }; - private static readonly List PromotedMoves = new(8) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, PromotedMoves); - } - - public override Piece DeepClone() - { - var clone = new Rook(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs deleted file mode 100644 index 3287604..0000000 --- a/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs +++ /dev/null @@ -1,29 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Rules.Pieces -{ - public class SilverGeneral : Piece - { - private static readonly List Moves = new(4) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) - { - moveSet = new MoveSet(this, Moves); - promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves); - } - - public override Piece DeepClone() - { - var clone = new SilverGeneral(Owner); - if (IsPromoted) clone.Promote(); - return clone; - } - } -} diff --git a/Gameboard.ShogiUI.Rules/WhichPiece.cs b/Gameboard.ShogiUI.Rules/WhichPiece.cs deleted file mode 100644 index 5e6356c..0000000 --- a/Gameboard.ShogiUI.Rules/WhichPiece.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Gameboard.ShogiUI.Rules -{ - public enum WhichPiece - { - King, - GoldGeneral, - SilverGeneral, - Bishop, - Rook, - Knight, - Lance, - Pawn - } -} diff --git a/Gameboard.ShogiUI.Rules/WhichPlayer.cs b/Gameboard.ShogiUI.Rules/WhichPlayer.cs deleted file mode 100644 index 4b8b8f2..0000000 --- a/Gameboard.ShogiUI.Rules/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Rules -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs index b662cb1..80a15bf 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs @@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class JoinByCodeRequest : IRequest { public ClientAction Action { get; set; } - public string JoinCode { get; set; } + public string JoinCode { get; set; } = ""; } public class JoinGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = ""; } public class JoinGameResponse : IResponse @@ -25,6 +25,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public JoinGameResponse(ClientAction action) { Action = action.ToString(); + Error = ""; + GameName = ""; + PlayerName = ""; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index ab9d67f..bbd9944 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -1,6 +1,7 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { @@ -13,11 +14,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; } public string Error { get; set; } - public ICollection Games { get; set; } + public IReadOnlyList Games { get; set; } public ListGamesResponse(ClientAction action) { Action = action.ToString(); + Error = ""; + Games = new Collection(); } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index a879953..d268e07 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -1,19 +1,22 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public class LoadGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = ""; } public class LoadGameResponse : IResponse { public string Action { get; } public Game Game { get; set; } + public WhichPlayer PlayerPerspective { get; set; } public BoardState BoardState { get; set; } + public IList MoveHistory { get; set; } public string Error { get; set; } public LoadGameResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs index 6ee3051..3ba7e8b 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -8,5 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types public Piece[,] Board { get; set; } = new Piece[0, 0]; public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); + public WhichPlayer? PlayerInCheck { get; set; } + public WhichPlayer WhoseTurn { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index 8ff5fb1..f9fb1bb 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,14 +1,33 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class Game { + public string Player1 { get; set; } = string.Empty; + public string? Player2 { get; set; } = string.Empty; public string GameName { get; set; } = string.Empty; /// - /// Players[0] is the session owner, Players[1] is the other guy + /// Players[0] is the session owner, Players[1] is the other person. /// - public IReadOnlyList Players { get; set; } = Array.Empty(); + public IReadOnlyList Players + { + get + { + var list = new List(2) { Player1 }; + if (!string.IsNullOrEmpty(Player2)) list.Add(Player2); + return list; + } + } + + public Game() + { + } + public Game(string gameName, string player1, string? player2 = null) + { + GameName = gameName; + Player1 = player1; + Player2 = player2; + } } } diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index a1576c2..afad198 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -49,16 +49,17 @@ Global {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 7900ed2..15bf163 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,14 +13,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [Route("[controller]")] public class GameController : ControllerBase { - private readonly IGameboardRepositoryManager manager; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager manager; + private readonly ISocketConnectionManager communicationManager; private readonly IGameboardRepository repository; public GameController( IGameboardRepository repository, - IGameboardRepositoryManager manager, - ISocketCommunicationManager communicationManager) + IGameboardManager manager, + ISocketConnectionManager communicationManager) { this.manager = manager; this.communicationManager = communicationManager; diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index f00b5bf..739e423 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,13 +16,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers { private readonly ILogger logger; private readonly ISocketTokenManager tokenManager; - private readonly IGameboardRepositoryManager gameboardManager; + private readonly IGameboardManager gameboardManager; private readonly IGameboardRepository gameboardRepository; public SocketController( ILogger logger, ISocketTokenManager tokenManager, - IGameboardRepositoryManager gameboardManager, + IGameboardManager gameboardManager, IGameboardRepository gameboardRepository) { this.logger = logger; diff --git a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs similarity index 83% rename from Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs rename to Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index 8cf4734..36827e8 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,14 +1,13 @@ -using Gameboard.ShogiUI.Rules; -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System; using System.Text; using System.Text.RegularExpressions; -namespace Gameboard.ShogiUI.UnitTests.Rules +namespace Gameboard.ShogiUI.Sockets.Extensions { - public static class BoardStateExtensions + public static class ModelExtensions { - public static string GetShortName(this Piece self) + public static string GetShortName(this Models.Piece self) { var name = self.WhichPiece switch { @@ -27,7 +26,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules return name; } - public static void PrintStateAsAscii(this ShogiBoard self) + public static void PrintStateAsAscii(this Models.Shogi self) { var builder = new StringBuilder(); builder.Append(" Player 2(.)"); @@ -41,7 +40,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules builder.Append('|'); for (var x = 0; x < 9; x++) { - var piece = self.Board[x, y]; + var piece = self.Board[y, x]; if (piece == null) { builder.Append(" "); diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index c558037..241f8e8 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,14 +8,7 @@ - - - - - - - - + @@ -26,8 +19,8 @@ - + diff --git a/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs new file mode 100644 index 0000000..5018096 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ActiveSessionManager.cs @@ -0,0 +1,33 @@ +using Gameboard.ShogiUI.Sockets.Models; +using System.Collections.Concurrent; + +namespace Gameboard.ShogiUI.Sockets.Managers +{ + public interface IActiveSessionManager + { + void Add(Session session); + Session? Get(string sessionName); + } + + // TODO: Consider moving this class' functionality into the ConnectionManager class. + public class ActiveSessionManager : IActiveSessionManager + { + private readonly ConcurrentDictionary Sessions; + + public ActiveSessionManager() + { + Sessions = new ConcurrentDictionary(); + } + + public void Add(Session session) => Sessions.TryAdd(session.Name, session); + + public Session? Get(string sessionName) + { + if (Sessions.TryGetValue(sessionName, out var session)) + { + return session; + } + return null; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs deleted file mode 100644 index f153527..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Gameboard.ShogiUI.Rules; -using System.Collections.Concurrent; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface IBoardManager - { - void Add(string sessionName, ShogiBoard board); - ShogiBoard? Get(string sessionName); - } - - public class BoardManager : IBoardManager - { - private readonly ConcurrentDictionary Boards; - - public BoardManager() - { - Boards = new ConcurrentDictionary(); - } - - public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); - - public ShogiBoard? Get(string sessionName) - { - if (Boards.TryGetValue(sessionName, out var board)) - { - return board; - } - return null; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index 8ff9dd1..6703d3b 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using System.Threading.Tasks; @@ -14,20 +13,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers // It can be an API route and still tell socket connections about the new session. public class CreateGameHandler : ICreateGameHandler { - private readonly IGameboardRepositoryManager manager; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager manager; + private readonly ISocketConnectionManager connectionManager; public CreateGameHandler( - ISocketCommunicationManager communicationManager, - IGameboardRepositoryManager manager) + ISocketConnectionManager communicationManager, + IGameboardManager manager) { this.manager = manager; - this.communicationManager = communicationManager; + this.connectionManager = communicationManager; } public async Task Handle(CreateGameRequest request, string userName) { - var model = new Session(request.GameName, request.IsPrivate, userName); + var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null); var success = await manager.CreateSession(model); if (!success) @@ -36,7 +35,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { Error = "Unable to create game with this name." }; - await communicationManager.BroadcastToPlayers(error, userName); + await connectionManager.BroadcastToPlayers(error, userName); } var response = new CreateGameResponse(request.Action) @@ -46,8 +45,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers }; var task = request.IsPrivate - ? communicationManager.BroadcastToPlayers(response, userName) - : communicationManager.BroadcastToAll(response); + ? connectionManager.BroadcastToPlayers(response, userName) + : connectionManager.BroadcastToAll(response); await task; } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 9f3ae26..8882e8f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -11,10 +11,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public class JoinByCodeHandler : IJoinByCodeHandler { private readonly IGameboardRepository repository; - private readonly ISocketCommunicationManager communicationManager; + private readonly ISocketConnectionManager communicationManager; public JoinByCodeHandler( - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository repository) { this.repository = repository; diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index a37cdfc..223782c 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -1,5 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -10,40 +10,34 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } public class JoinGameHandler : IJoinGameHandler { - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; + private readonly IGameboardManager gameboardManager; + private readonly ISocketConnectionManager connectionManager; public JoinGameHandler( - ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + ISocketConnectionManager communicationManager, + IGameboardManager gameboardManager) { - this.gameboardRepository = gameboardRepository; - this.communicationManager = communicationManager; + this.gameboardManager = gameboardManager; + this.connectionManager = communicationManager; } public async Task Handle(JoinGameRequest request, string userName) { - //var request = JsonConvert.DeserializeObject(json); + var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName); - //var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession - //{ - // PlayerName = userName, - // SessionName = request.GameName - //}); - - //var response = new JoinGameResponse(ClientAction.JoinGame) - //{ - // PlayerName = userName, - // GameName = request.GameName - //}; - //if (joinSucceeded) - //{ - // await communicationManager.BroadcastToAll(response); - //} - //else - //{ - // response.Error = "Game is full."; - // await communicationManager.BroadcastToPlayers(response, userName); - //} + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName, + GameName = request.GameName + }; + if (joinSucceeded) + { + await connectionManager.BroadcastToAll(response); + } + else + { + response.Error = "Game is full or does not exist."; + await connectionManager.BroadcastToPlayers(response, userName); + } } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 41b5d5e..64787bd 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -11,15 +11,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers Task Handle(ListGamesRequest request, string userName); } - // TODO: This doesn't need to be a socket action. - // It can be an HTTP route. public class ListGamesHandler : IListGamesHandler { - private readonly ISocketCommunicationManager communicationManager; + private readonly ISocketConnectionManager communicationManager; private readonly IGameboardRepository repository; public ListGamesHandler( - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository repository) { this.communicationManager = communicationManager; @@ -28,12 +26,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(ListGamesRequest _, string userName) { - var sessions = await repository.ReadSessions(); - var games = sessions.Select(s => s.ToServiceModel()); // yuck + var sessions = await repository.ReadSessionMetadatas(); + var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList(); var response = new ListGamesResponse(ClientAction.ListGames) { - Games = games.ToList() + Games = games }; await communicationManager.BroadcastToPlayers(response, userName); diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 9fcb72f..93317a6 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,4 +1,4 @@ -using Gameboard.ShogiUI.Rules; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -20,14 +20,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; - private readonly IBoardManager boardManager; + private readonly ISocketConnectionManager communicationManager; + private readonly IActiveSessionManager boardManager; public LoadGameHandler( ILogger logger, - ISocketCommunicationManager communicationManager, + ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, - IBoardManager boardManager) + IActiveSessionManager boardManager) { this.logger = logger; this.gameboardRepository = gameboardRepository; @@ -37,10 +37,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers public async Task Handle(LoadGameRequest request, string userName) { - var readSession = gameboardRepository.ReadSession(request.GameName); - var readStates = gameboardRepository.ReadBoardStates(request.GameName); - - var sessionModel = await readSession; + var sessionModel = await gameboardRepository.ReadSession(request.GameName); if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); @@ -50,18 +47,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } communicationManager.SubscribeToGame(sessionModel, userName); - var boardStates = await readStates; - var moveModels = boardStates - .Where(_ => _.Move != null) - .Select(_ => _.Move!.ToRulesModel()) - .ToList(); - var shogiBoard = new ShogiBoard(moveModels); - boardManager.Add(sessionModel.Name, shogiBoard); + boardManager.Add(sessionModel); var response = new LoadGameResponse(ClientAction.LoadGame) { - Game = sessionModel.ToServiceModel(), - BoardState = new Models.BoardState(shogiBoard).ToServiceModel() + Game = new SessionMetadata(sessionModel).ToServiceModel(), + BoardState = sessionModel.Shogi.ToServiceModel(), + MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList() }; await communicationManager.BroadcastToPlayers(response, userName); } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 74141d0..4c9e9a8 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,7 +1,6 @@ using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Newtonsoft.Json; using System.Threading.Tasks; @@ -13,35 +12,45 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers } public class MoveHandler : IMoveHandler { - private readonly IBoardManager boardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketCommunicationManager communicationManager; + private readonly IActiveSessionManager boardManager; + private readonly IGameboardManager gameboardManager; + private readonly ISocketConnectionManager communicationManager; public MoveHandler( - IBoardManager boardManager, - ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + IActiveSessionManager boardManager, + ISocketConnectionManager communicationManager, + IGameboardManager gameboardManager) { this.boardManager = boardManager; - this.gameboardRepository = gameboardRepository; + this.gameboardManager = gameboardManager; this.communicationManager = communicationManager; } public async Task Handle(MoveRequest request, string userName) { - //var request = JsonConvert.DeserializeObject(json); - //var moveModel = new Move(request.Move); - //var board = boardManager.Get(request.GameName); - //if (board == null) - //{ - // // TODO: Find a flow for this - // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - // { - // Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." - // }; - // await communicationManager.BroadcastToPlayers(response, userName); + Move moveModel; + if (request.Move.PieceFromCaptured.HasValue) + { + moveModel = new Move(request.Move.PieceFromCaptured.Value, request.Move.To); + } + else + { + moveModel = new Move(request.Move.From!, request.Move.To, request.Move.IsPromotion); + } + + var board = boardManager.Get(request.GameName); + if (board == null) + { + // TODO: Find a flow for this + var response = new MoveResponse(ServiceModels.Socket.Types.ClientAction.Move) + { + Error = $"Game isn't loaded. Send a message with the {ServiceModels.Socket.Types.ClientAction.LoadGame} action first." + }; + await communicationManager.BroadcastToPlayers(response, userName); + } + + + - //} - //var boardMove = moveModel.ToBoardModel(); //var moveSuccess = board.Move(boardMove); //if (moveSuccess) //{ diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs similarity index 50% rename from Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs rename to Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs index d219516..bc2be4b 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs @@ -1,24 +1,28 @@ using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories; using System; using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers +namespace Gameboard.ShogiUI.Sockets.Managers { - public interface IGameboardRepositoryManager + public interface IGameboardManager { Task CreateGuestUser(); Task IsPlayer1(string sessionName, string playerName); bool IsGuest(string playerName); - Task CreateSession(Session session); + Task CreateSession(SessionMetadata session); + Task ReadSession(string gameName); + Task UpdateSession(Session session); + Task AssignPlayer2ToSession(string sessionName, string userName); } - public class GameboardRepositoryManager : IGameboardRepositoryManager + public class GameboardManager : IGameboardManager { private const int MaxTries = 3; private const string GuestPrefix = "Guest-"; private readonly IGameboardRepository repository; - public GameboardRepositoryManager(IGameboardRepository repository) + public GameboardManager(IGameboardRepository repository) { this.repository = repository; } @@ -53,19 +57,44 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers //{ // return await repository.PostJoinCode(sessionName, playerName); //} - return null; + return string.Empty; } - public async Task CreateSession(Session session) + public Task CreateSession(SessionMetadata session) { - var success = await repository.CreateSession(session); - if (success) - { - return await repository.CreateBoardState(session.Name, new BoardState(), null); - } - return false; + return repository.CreateSession(session); } public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); + + public Task ReadSession(string sessionName) + { + return repository.ReadSession(sessionName); + } + + /// + /// Saves the session to storage. + /// + /// The session to save. + /// True if the session was saved successfully. + public Task UpdateSession(Session session) + { + return repository.UpdateSession(session); + } + + public async Task AssignPlayer2ToSession(string sessionName, string userName) + { + var isSuccess = false; + var session = await repository.ReadSession(sessionName); + if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2)) + { + session.SetPlayer2(userName); + if (await repository.UpdateSession(session)) + { + isSuccess = true; + } + } + return isSuccess; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs deleted file mode 100644 index c3dfc6a..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface ISocketCommunicationManager - { - Task BroadcastToAll(IResponse response); - //Task BroadcastToGame(string gameName, IResponse response); - //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); - void SubscribeToGame(Session session, string playerName); - void SubscribeToBroadcast(WebSocket socket, string playerName); - void UnsubscribeFromBroadcastAndGames(string playerName); - void UnsubscribeFromGame(string gameName, string playerName); - Task BroadcastToPlayers(IResponse response, params string[] playerNames); - } - - public class SocketCommunicationManager : ISocketCommunicationManager - { - /// Dictionary key is player name. - private readonly ConcurrentDictionary connections; - /// Dictionary key is game name. - private readonly ConcurrentDictionary sessions; - private readonly ILogger logger; - - public SocketCommunicationManager(ILogger logger) - { - this.logger = logger; - connections = new ConcurrentDictionary(); - sessions = new ConcurrentDictionary(); - } - - public void SubscribeToBroadcast(WebSocket socket, string playerName) - { - connections.TryAdd(playerName, socket); - } - - public void UnsubscribeFromBroadcastAndGames(string playerName) - { - connections.TryRemove(playerName, out _); - foreach (var kvp in sessions) - { - var sessionName = kvp.Key; - UnsubscribeFromGame(sessionName, playerName); - } - } - - /// - /// Unsubscribes the player from their current game, then subscribes to the new game. - /// - public void SubscribeToGame(Session session, string playerName) - { - // Unsubscribe from any other games - foreach (var kvp in sessions) - { - var gameNameKey = kvp.Key; - UnsubscribeFromGame(gameNameKey, playerName); - } - - // Subscribe - if (connections.TryGetValue(playerName, out var socket)) - { - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); - } - } - - public void UnsubscribeFromGame(string gameName, string playerName) - { - if (sessions.TryGetValue(gameName, out var s)) - { - s.Subscriptions.TryRemove(playerName, out _); - if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); - } - } - - public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) - { - var tasks = new List(playerNames.Length); - foreach (var name in playerNames) - { - if (connections.TryGetValue(name, out var socket)) - { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", name, serialized); - tasks.Add(socket.SendTextAsync(serialized)); - - } - } - await Task.WhenAll(tasks); - } - public Task BroadcastToAll(IResponse response) - { - var message = JsonConvert.SerializeObject(response); - logger.LogInformation($"Broadcasting\n{0}", message); - var tasks = new List(connections.Count); - foreach (var kvp in connections) - { - var socket = kvp.Value; - tasks.Add(socket.SendTextAsync(message)); - } - return Task.WhenAll(tasks); - } - - //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized1 = JsonConvert.SerializeObject(forPlayer1); - // var serialized2 = JsonConvert.SerializeObject(forPlayer2); - // return Task.WhenAll( - // session.SendToPlayer1(serialized1), - // session.SendToPlayer2(serialized2)); - // } - // return Task.CompletedTask; - //} - - //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); - // return session.Broadcast(serialized); - // } - // return Task.CompletedTask; - //} - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 5ae3ff0..54798b0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,13 +1,10 @@ using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.AspNetCore.Http; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Net; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; @@ -15,127 +12,127 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface ISocketConnectionManager { - Task HandleSocketRequest(HttpContext context); + Task BroadcastToAll(IResponse response); + //Task BroadcastToGame(string gameName, IResponse response); + //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + void SubscribeToGame(Session session, string playerName); + void SubscribeToBroadcast(WebSocket socket, string playerName); + void UnsubscribeFromBroadcastAndGames(string playerName); + void UnsubscribeFromGame(string gameName, string playerName); + Task BroadcastToPlayers(IResponse response, params string[] playerNames); } + /// + /// Retains all active socket connections and provides convenient methods for sending messages to clients. + /// public class SocketConnectionManager : ISocketConnectionManager { + /// Dictionary key is player name. + private readonly ConcurrentDictionary connections; + /// Dictionary key is game name. + private readonly ConcurrentDictionary sessions; private readonly ILogger logger; - private readonly ISocketCommunicationManager communicationManager; - private readonly ISocketTokenManager tokenManager; - private readonly ICreateGameHandler createGameHandler; - private readonly IJoinByCodeHandler joinByCodeHandler; - private readonly IJoinGameHandler joinGameHandler; - private readonly IListGamesHandler listGamesHandler; - private readonly ILoadGameHandler loadGameHandler; - private readonly IMoveHandler moveHandler; - public SocketConnectionManager( - ILogger logger, - ISocketCommunicationManager communicationManager, - ISocketTokenManager tokenManager, - ICreateGameHandler createGameHandler, - IJoinByCodeHandler joinByCodeHandler, - IJoinGameHandler joinGameHandler, - IListGamesHandler listGamesHandler, - ILoadGameHandler loadGameHandler, - IMoveHandler moveHandler) : base() + public SocketConnectionManager(ILogger logger) { this.logger = logger; - this.communicationManager = communicationManager; - this.tokenManager = tokenManager; - this.createGameHandler = createGameHandler; - this.joinByCodeHandler = joinByCodeHandler; - this.joinGameHandler = joinGameHandler; - this.listGamesHandler = listGamesHandler; - this.loadGameHandler = loadGameHandler; - this.moveHandler = moveHandler; + connections = new ConcurrentDictionary(); + sessions = new ConcurrentDictionary(); } - public async Task HandleSocketRequest(HttpContext context) + public void SubscribeToBroadcast(WebSocket socket, string playerName) { - var hasToken = context.Request.Query.Keys.Contains("token"); - if (hasToken) + connections.TryAdd(playerName, socket); + } + + public void UnsubscribeFromBroadcastAndGames(string playerName) + { + connections.TryRemove(playerName, out _); + foreach (var kvp in sessions) { - var oneTimeToken = context.Request.Query["token"][0]; - var tokenAsGuid = Guid.Parse(oneTimeToken); - var userName = tokenManager.GetUsername(tokenAsGuid); - if (userName != null) + var sessionName = kvp.Key; + UnsubscribeFromGame(sessionName, playerName); + } + } + + /// + /// Unsubscribes the player from their current game, then subscribes to the new game. + /// + public void SubscribeToGame(Session session, string playerName) + { + // Unsubscribe from any other games + foreach (var kvp in sessions) + { + var gameNameKey = kvp.Key; + UnsubscribeFromGame(gameNameKey, playerName); + } + + // Subscribe + if (connections.TryGetValue(playerName, out var socket)) + { + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); + } + } + + public void UnsubscribeFromGame(string gameName, string playerName) + { + if (sessions.TryGetValue(gameName, out var s)) + { + s.Subscriptions.TryRemove(playerName, out _); + if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); + } + } + + public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (connections.TryGetValue(name, out var socket)) { - var socket = await context.WebSockets.AcceptWebSocketAsync(); + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); - communicationManager.SubscribeToBroadcast(socket, userName); - while (!socket.CloseStatus.HasValue) - { - try - { - var message = await socket.ReceiveTextAsync(); - if (string.IsNullOrWhiteSpace(message)) continue; - logger.LogInformation("Request \n{0}\n", message); - var request = JsonConvert.DeserializeObject(message); - if (!Enum.IsDefined(typeof(ClientAction), request.Action)) - { - await socket.SendTextAsync("Error: Action not recognized."); - continue; - } - switch (request.Action) - { - case ClientAction.ListGames: - { - var req = JsonConvert.DeserializeObject(message); - await listGamesHandler.Handle(req, userName); - break; - } - case ClientAction.CreateGame: - { - var req = JsonConvert.DeserializeObject(message); - await createGameHandler.Handle(req, userName); - break; - } - case ClientAction.JoinGame: - { - var req = JsonConvert.DeserializeObject(message); - await joinGameHandler.Handle(req, userName); - break; - } - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(message); - await joinByCodeHandler.Handle(req, userName); - break; - } - case ClientAction.LoadGame: - { - var req = JsonConvert.DeserializeObject(message); - await loadGameHandler.Handle(req, userName); - break; - } - case ClientAction.Move: - { - var req = JsonConvert.DeserializeObject(message); - await moveHandler.Handle(req, userName); - break; - } - } - } - catch (OperationCanceledException ex) - { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); - } - } - communicationManager.UnsubscribeFromBroadcastAndGames(userName); - - return; } } - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - return; + await Task.WhenAll(tasks); } + public Task BroadcastToAll(IResponse response) + { + var message = JsonConvert.SerializeObject(response); + logger.LogInformation($"Broadcasting\n{0}", message); + var tasks = new List(connections.Count); + foreach (var kvp in connections) + { + var socket = kvp.Value; + tasks.Add(socket.SendTextAsync(message)); + } + return Task.WhenAll(tasks); + } + + //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized1 = JsonConvert.SerializeObject(forPlayer1); + // var serialized2 = JsonConvert.SerializeObject(forPlayer2); + // return Task.WhenAll( + // session.SendToPlayer1(serialized1), + // session.SendToPlayer2(serialized2)); + // } + // return Task.CompletedTask; + //} + + //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); + // return session.Broadcast(serialized); + // } + // return Task.CompletedTask; + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs deleted file mode 100644 index df3d1f0..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Gameboard.ShogiUI.Rules; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class BoardState - { - // TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity. - public Piece?[,] Board { get; } - public IReadOnlyCollection Player1Hand { get; } - public IReadOnlyCollection Player2Hand { get; } - /// - /// Move is null in the first BoardState of a Session, before any moves have been made. - /// - public Move? Move { get; } - - public BoardState() : this(new ShogiBoard()) { } - - public BoardState(Piece?[,] board, IList player1Hand, ICollection player2Hand, Move move) - { - Board = board; - Player1Hand = new ReadOnlyCollection(player1Hand); - } - - public BoardState(ShogiBoard shogi) - { - Board = new Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = shogi.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList(); - Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); - Move = new Move(shogi.MoveHistory[^1]); - } - - public ServiceTypes.BoardState ToServiceModel() - { - var board = new ServiceTypes.Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = Board[x, y]; - if (piece != null) - { - board[x, y] = piece.ToServiceModel(); - } - } - return new ServiceTypes.BoardState - { - Board = board, - Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), - Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() - }; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Coords.cs b/Gameboard.ShogiUI.Sockets/Models/Coords.cs deleted file mode 100644 index 2104cde..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Coords.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class Coords - { - private const string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private const char A = 'A'; - public int X { get; } - public int Y { get; } - public Coords(int x, int y) - { - X = x; - Y = y; - } - - public string ToBoardNotation() - { - var file = (char)(X + A); - var rank = Y + 1; - return $"{file}{rank}"; - } - - public static Coords FromBoardNotation(string notation) - { - if (string.IsNullOrEmpty(notation)) - { - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Coords(file - A, rank); - } - throw new ArgumentException("Board notation not recognized."); // TODO: Move this error handling to the service layer. - } - return new Coords(-1, -1); // Temporarily this is how I tell Gameboard.API that a piece came from the hand. - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 3617d88..568594c 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,41 +1,86 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System; +using System.Diagnostics; using System.Numerics; +using System.Text.RegularExpressions; namespace Gameboard.ShogiUI.Sockets.Models { + [DebuggerDisplay("{From} - {To}")] public class Move { - public Coords? From { get; set; } - public bool IsPromotion { get; set; } - public WhichPiece? PieceFromHand { get; set; } - public Coords To { get; set; } + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private static readonly char A = 'A'; - public Move(Coords from, Coords to, bool isPromotion) + public Vector2? From { get; } + public bool IsPromotion { get; } + public WhichPiece? PieceFromHand { get; } + public Vector2 To { get; } + + public Move(Vector2 from, Vector2 to, bool isPromotion = false) { From = from; To = to; IsPromotion = isPromotion; } - - public Move(WhichPiece pieceFromHand, Coords to) + public Move(WhichPiece pieceFromHand, Vector2 to) { PieceFromHand = pieceFromHand; To = to; } + /// + /// Constructor to represent moving a piece on the Board to another position on the Board. + /// + /// Position the piece is being moved from. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(string fromNotation, string toNotation, bool isPromotion = false) + { + From = FromBoardNotation(fromNotation); + To = FromBoardNotation(toNotation); + IsPromotion = isPromotion; + } + + /// + /// Constructor to represent moving a piece from the Hand to the Board. + /// + /// The piece being moved from the Hand to the Board. + /// Position the piece is being moved to. + /// If the moving piece should be promoted. + public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false) + { + From = null; + PieceFromHand = pieceFromHand; + To = FromBoardNotation(toNotation); + IsPromotion = isPromotion; + } + public ServiceModels.Socket.Types.Move ToServiceModel() => new() { - From = From?.ToBoardNotation(), + From = From.HasValue ? ToBoardNotation(From.Value) : null, IsPromotion = IsPromotion, - To = To.ToBoardNotation(), - PieceFromCaptured = PieceFromHand + PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null, + To = ToBoardNotation(To) }; - public Rules.Move ToRulesModel() + private static string ToBoardNotation(Vector2 vector) { - return PieceFromHand != null - ? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y)) - : new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion); + var file = (char)(vector.X + A); + var rank = vector.Y + 1; + return $"{file}{rank}"; + } + + private static Vector2 FromBoardNotation(string notation) + { + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs new file mode 100644 index 0000000..5f4ee8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs @@ -0,0 +1,95 @@ +using PathFinding; +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.Models +{ + public static class MoveSets + { + public static readonly List King = new(8) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + + public static readonly List Bishop = new(4) + { + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) + }; + + public static readonly List PromotedBishop = new(8) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down), + new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), + new PathFinding.Move(Direction.UpRight, Distance.MultiStep), + new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), + new PathFinding.Move(Direction.DownRight, Distance.MultiStep) + }; + + public static readonly List GoldGeneral = new(6) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.Left), + new PathFinding.Move(Direction.Right), + new PathFinding.Move(Direction.Down) + }; + + public static readonly List Knight = new(2) + { + new PathFinding.Move(Direction.KnightLeft), + new PathFinding.Move(Direction.KnightRight) + }; + + public static readonly List Lance = new(1) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + }; + + public static readonly List Pawn = new(1) + { + new PathFinding.Move(Direction.Up) + }; + + public static readonly List Rook = new(4) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep) + }; + + public static readonly List PromotedRook = new(8) + { + new PathFinding.Move(Direction.Up, Distance.MultiStep), + new PathFinding.Move(Direction.Left, Distance.MultiStep), + new PathFinding.Move(Direction.Right, Distance.MultiStep), + new PathFinding.Move(Direction.Down, Distance.MultiStep), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + + public static readonly List SilverGeneral = new(4) + { + new PathFinding.Move(Direction.Up), + new PathFinding.Move(Direction.UpLeft), + new PathFinding.Move(Direction.UpRight), + new PathFinding.Move(Direction.DownLeft), + new PathFinding.Move(Direction.DownRight) + }; + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index d8bc5b9..f86d120 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -1,27 +1,59 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using PathFinding; +using System.Diagnostics; namespace Gameboard.ShogiUI.Sockets.Models { - public class Piece + [DebuggerDisplay("{WhichPiece} {Owner}")] + public class Piece : IPlanarElement { - public bool IsPromoted { get; } - public WhichPlayer Owner { get; } public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; - public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece) + public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) { - IsPromoted = isPromoted; + WhichPiece = piece; Owner = owner; - WhichPiece = whichPiece; + IsPromoted = isPromoted; } - public Piece(Rules.Pieces.Piece piece) + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void ToggleOwnership() { - IsPromoted = piece.IsPromoted; - Owner = (WhichPlayer)piece.Owner; - WhichPiece = (WhichPiece)piece.WhichPiece; + Owner = Owner == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; } + public void Promote() => IsPromoted = CanPromote; + + public void Demote() => IsPromoted = false; + + public void Capture() + { + ToggleOwnership(); + Demote(); + } + + // TODO: There is no reason to make "new" MoveSets every time this property is accessed. + public MoveSet MoveSet => WhichPiece switch + { + WhichPiece.King => new MoveSet(this, MoveSets.King), + WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral), + WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral), + WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop), + WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook), + WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight), + WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance), + WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn), + _ => throw new System.NotImplementedException() + }; + public ServiceModels.Socket.Types.Piece ToServiceModel() { return new ServiceModels.Socket.Types.Piece diff --git a/Gameboard.ShogiUI.Sockets/Models/Player.cs b/Gameboard.ShogiUI.Sockets/Models/Player.cs deleted file mode 100644 index 90fa28e..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Player.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class Player - { - public string Name { get; } - - public Player(string name) - { - Name = name; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index bf2af49..32bd545 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -1,22 +1,21 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Net.WebSockets; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Models { public class Session { + // TODO: Separate subscriptions to the Session from the Session. [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } public string Name { get; } public string Player1 { get; } - public string? Player2 { get; } + public string? Player2 { get; private set; } public bool IsPrivate { get; } - public Session(string name, bool isPrivate, string player1, string? player2 = null) + public Shogi Shogi { get; } + + public Session(string name, bool isPrivate, Shogi shogi, string player1, string? player2 = null) { Subscriptions = new ConcurrentDictionary(); @@ -24,48 +23,12 @@ namespace Gameboard.ShogiUI.Sockets.Models Player1 = player1; Player2 = player2; IsPrivate = isPrivate; + Shogi = shogi; } - public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); - - public Task Broadcast(string message) + public void SetPlayer2(string userName) { - var tasks = new List(Subscriptions.Count); - foreach (var kvp in Subscriptions) - { - var socket = kvp.Value; - tasks.Add(socket.SendTextAsync(message)); - } - return Task.WhenAll(tasks); - } - - public Task SendToPlayer1(string message) - { - if (Subscriptions.TryGetValue(Player1, out var socket)) - { - return socket.SendTextAsync(message); - } - return Task.CompletedTask; - } - - public Task SendToPlayer2(string message) - { - if (Player2 != null && Subscriptions.TryGetValue(Player2, out var socket)) - { - return socket.SendTextAsync(message); - } - return Task.CompletedTask; - } - - public Game ToServiceModel() - { - var players = new List(2) { Player1 }; - if (!string.IsNullOrWhiteSpace(Player2)) players.Add(Player2); - return new Game - { - GameName = Name, - Players = players.ToArray() - }; + Player2 = userName; } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs new file mode 100644 index 0000000..80f74fe --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -0,0 +1,30 @@ +namespace Gameboard.ShogiUI.Sockets.Models +{ + /// + /// A representation of a Session without the board and game-rules. + /// + public class SessionMetadata + { + public string Name { get; } + public string Player1 { get; } + public string? Player2 { get; } + public bool IsPrivate { get; } + + public SessionMetadata(string name, bool isPrivate, string player1, string? player2) + { + Name = name; + IsPrivate = isPrivate; + Player1 = player1; + Player2 = player2; + } + public SessionMetadata(Session sessionModel) + { + Name = sessionModel.Name; + IsPrivate = sessionModel.IsPrivate; + Player1 = sessionModel.Player1; + Player2 = sessionModel.Player2; + } + + public ServiceModels.Socket.Types.Game ToServiceModel() => new(Name, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Rules/ShogiBoard.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs similarity index 81% rename from Gameboard.ShogiUI.Rules/ShogiBoard.cs rename to Gameboard.ShogiUI.Sockets/Models/Shogi.cs index e5f104e..678aa92 100644 --- a/Gameboard.ShogiUI.Rules/ShogiBoard.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs @@ -1,21 +1,22 @@ -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using PathFinding; using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; -namespace Gameboard.ShogiUI.Rules +namespace Gameboard.ShogiUI.Sockets.Models { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. /// The board is always from Player1's perspective. /// [0,0] is the lower-left position, [8,8] is the higher-right position /// - public class ShogiBoard + public class Shogi { private delegate void MoveSetCallback(Piece piece, Vector2 position); private readonly PathFinder2D pathFinder; - private ShogiBoard? validationBoard; + private Shogi? validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } @@ -27,7 +28,7 @@ namespace Gameboard.ShogiUI.Rules public string Error { get; private set; } - public ShogiBoard() + public Shogi() { Board = new PlanarCollection(9, 9); MoveHistory = new List(20); @@ -36,13 +37,14 @@ namespace Gameboard.ShogiUI.Rules { WhichPlayer.Player2, new List()}, }; pathFinder = new PathFinder2D(Board); - InitializeBoardState(); player1King = new Vector2(4, 8); player2King = new Vector2(4, 0); Error = string.Empty; + + InitializeBoardState(); } - public ShogiBoard(IList moves) : this() + public Shogi(IList moves) : this() { for (var i = 0; i < moves.Count; i++) { @@ -54,16 +56,16 @@ namespace Gameboard.ShogiUI.Rules } } - private ShogiBoard(ShogiBoard toCopy) + private Shogi(Shogi toCopy) { Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) { - var piece = toCopy.Board[x, y]; + var piece = toCopy.Board[y, x]; if (piece != null) { - Board[x, y] = piece.DeepClone(); + Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted); } } @@ -105,7 +107,7 @@ namespace Gameboard.ShogiUI.Rules // Try making the move in a "throw away" board. if (validationBoard == null) { - validationBoard = new ShogiBoard(this); + validationBoard = new Shogi(this); } var isValid = move.PieceFromHand.HasValue @@ -138,7 +140,7 @@ namespace Gameboard.ShogiUI.Rules if (move.PieceFromHand.HasValue == false) return false; //Invalid move var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) return false; // Invalid move - if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. + if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; switch (move.PieceFromHand.Value) @@ -157,7 +159,7 @@ namespace Gameboard.ShogiUI.Rules if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; // Mutate the board. - Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; + Board[move.To.Y, move.To.X] = Hands[WhoseTurn][index]; Hands[WhoseTurn].RemoveAt(index); return true; @@ -165,7 +167,7 @@ namespace Gameboard.ShogiUI.Rules /// True if the move was successful. private bool PlaceFromBoard(Move move) { - var fromPiece = Board[move.From.Value.X, move.From.Value.Y]; + var fromPiece = Board[move.From.Value.Y, move.From.Value.X]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; @@ -182,7 +184,7 @@ namespace Gameboard.ShogiUI.Rules return false; // Invalid move; move not part of move-set. } - var captured = Board[move.To.X, move.To.Y]; + var captured = Board[move.To.Y, move.To.X]; if (captured != null) { if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. @@ -202,8 +204,8 @@ namespace Gameboard.ShogiUI.Rules fromPiece.Promote(); } } - Board[move.To.X, move.To.Y] = fromPiece; - Board[move.From.Value.X, move.From.Value.Y] = null; + Board[move.To.Y, move.To.X] = fromPiece; + Board[move.From.Value.Y, move.From.Value.X] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) @@ -223,7 +225,7 @@ namespace Gameboard.ShogiUI.Rules private bool IsPathable(Vector2 from, Vector2 to) { - var piece = Board[from.X, from.Y]; + var piece = Board[from.Y, from.X]; if (piece == null) return false; var isObstructed = false; @@ -308,8 +310,8 @@ namespace Gameboard.ShogiUI.Rules // ...evaluate if any move gets the player out of check. pathFinder.PathEvery(from, (other, position) => { - if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveToTry = new Move(from, position, false); + if (validationBoard == null) validationBoard = new Shogi(this); + var moveToTry = new Move(from, position); var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { @@ -331,44 +333,44 @@ namespace Gameboard.ShogiUI.Rules { for (int y = 3; y < 6; y++) for (int x = 0; x < 9; x++) - Board[x, y] = null; + Board[y, x] = null; } private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 6 : 2; - for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); + for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player); } private void ResetMiddleRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 7 : 1; - Board[0, y] = null; - for (int x = 2; x < 7; x++) Board[x, y] = null; - Board[8, y] = null; + Board[y, 0] = null; + for (int x = 2; x < 7; x++) Board[y, x] = null; + Board[y, 8] = null; if (player == WhichPlayer.Player1) { - Board[1, y] = new Bishop(player); - Board[7, y] = new Rook(player); + Board[y, 1] = new Piece(WhichPiece.Bishop, player); + Board[y, 7] = new Piece(WhichPiece.Rook, player); } else { - Board[1, y] = new Rook(player); - Board[7, y] = new Bishop(player); + Board[y, 1] = new Piece(WhichPiece.Rook, player); + Board[y, 7] = new Piece(WhichPiece.Bishop, player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 8 : 0; - Board[0, y] = new Lance(player); - Board[1, y] = new Knight(player); - Board[2, y] = new SilverGeneral(player); - Board[3, y] = new GoldenGeneral(player); - Board[4, y] = new King(player); - Board[5, y] = new GoldenGeneral(player); - Board[6, y] = new SilverGeneral(player); - Board[7, y] = new Knight(player); - Board[8, y] = new Lance(player); + Board[y, 0] = new Piece(WhichPiece.Lance, player); + Board[y, 1] = new Piece(WhichPiece.Knight, player); + Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player); + Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player); + Board[y, 4] = new Piece(WhichPiece.King, player); + Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player); + Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player); + Board[y, 7] = new Piece(WhichPiece.Knight, player); + Board[y, 8] = new Piece(WhichPiece.Lance, player); } private void InitializeBoardState() { @@ -381,5 +383,28 @@ namespace Gameboard.ShogiUI.Rules ResetRearRow(WhichPlayer.Player1); } #endregion + + public BoardState ToServiceModel() + { + var board = new ServiceModels.Socket.Types.Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = Board[y, x]; + if (piece != null) + { + board[y, x] = piece.ToServiceModel(); + } + } + + return new BoardState + { + Board = board, + PlayerInCheck = InCheck, + WhoseTurn = WhoseTurn, + Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(), + Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList() + }; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs deleted file mode 100644 index 2658248..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Linq; - -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels -{ - public class BoardState : CouchDocument - { - public string Name { get; set; } - public Piece?[,] Board { get; set; } - public Piece[] Player1Hand { get; set; } - public Piece[] Player2Hand { get; set; } - /// - /// Move is null for first BoardState of a session - before anybody has made moves. - /// - public Move? Move { get; set; } - - /// - /// Default constructor and setters are for deserialization. - /// - public BoardState() : base() - { - Name = string.Empty; - Board = new Piece[9, 9]; - Player1Hand = Array.Empty(); - Player2Hand = Array.Empty(); - } - - public BoardState(string sessionName, Models.BoardState boardState) : base($"{sessionName}-{DateTime.Now:O}", nameof(BoardState)) - { - Name = sessionName; - Board = new Piece[9, 9]; - - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = boardState.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = boardState.Player1Hand.Select(model => new Piece(model)).ToArray(); - Player2Hand = boardState.Player2Hand.Select(model => new Piece(model)).ToArray(); - if (boardState.Move != null) - { - Move = new Move(boardState.Move); - } - } - - public Models.BoardState ToDomainModel() - { - /* - * Board = new Piece[9, 9]; - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var piece = boardState.Board[x, y]; - if (piece != null) - { - Board[x, y] = new Piece(piece); - } - } - - Player1Hand = boardState.Player1Hand.Select(_ => new Piece(_)).ToList(); - Player2Hand = boardState.Player2Hand.Select(_ => new Piece(_)).ToList(); - if (boardState.Move != null) - { - Move = new Move(boardState.Move); - } - */ - return null; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs new file mode 100644 index 0000000..3c8d9e7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -0,0 +1,57 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System; +using System.Linq; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class BoardStateDocument : CouchDocument + { + public string Name { get; set; } + + public Piece?[,] Board { get; set; } + + public Piece[] Player1Hand { get; set; } + + public Piece[] Player2Hand { get; set; } + + /// + /// Move is null for first BoardState of a session - before anybody has made moves. + /// + public Move? Move { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public BoardStateDocument() : base(WhichDocumentType.BoardState) + { + Name = string.Empty; + Board = new Piece[9, 9]; + Player1Hand = Array.Empty(); + Player2Hand = Array.Empty(); + } + + public BoardStateDocument(string sessionName, Models.Shogi shogi) + : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) + { + Name = sessionName; + Board = new Piece[9, 9]; + + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = shogi.Board[y, x]; + if (piece != null) + { + Board[y, x] = new Piece(piece); + } + } + + Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(model => new Piece(model)).ToArray(); + Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(model => new Piece(model)).ToArray(); + if (shogi.MoveHistory.Count > 0) + { + Move = new Move(shogi.MoveHistory[^1]); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs index bd1a793..4549de0 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -5,21 +5,21 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { public abstract class CouchDocument { - [JsonProperty("_id")] - public string Id { get; set; } - public string Type { get; set; } + [JsonProperty("_id")] public string Id { get; set; } + public WhichDocumentType DocumentType { get; } public DateTimeOffset CreatedDate { get; set; } - public CouchDocument() - { - Id = string.Empty; - Type = string.Empty; - CreatedDate = DateTimeOffset.UtcNow; - } - public CouchDocument(string id, string type) + public CouchDocument(WhichDocumentType documentType) + : this(string.Empty, documentType, DateTimeOffset.UtcNow) { } + + public CouchDocument(string id, WhichDocumentType documentType) + : this(id, documentType, DateTimeOffset.UtcNow) { } + + public CouchDocument(string id, WhichDocumentType documentType, DateTimeOffset createdDate) { Id = id; - Type = type; + DocumentType = documentType; + CreatedDate = createdDate; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs index a362f7d..5dbbf03 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -1,5 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { @@ -32,14 +32,25 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public Move(Models.Move move) { - From = move.From?.ToBoardNotation(); + if (move.From.HasValue) + { + From = ToBoardNotation(move.From.Value); + } IsPromotion = move.IsPromotion; - To = move.To.ToBoardNotation(); + To = ToBoardNotation(move.To); PieceFromHand = move.PieceFromHand; } + private static readonly char A = 'A'; + private static string ToBoardNotation(Vector2 vector) + { + var file = (char)(vector.X + A); + var rank = vector.Y + 1; + return $"{file}{rank}"; + } + public Models.Move ToDomainModel() => PieceFromHand.HasValue - ? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To)) - : new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion); + ? new(PieceFromHand.Value, To) + : new(From!, To, IsPromotion); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs index 68642f9..1df6a20 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -22,6 +22,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels WhichPiece = piece.WhichPiece; } - public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece); + public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md deleted file mode 100644 index 50c4380..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md +++ /dev/null @@ -1,4 +0,0 @@ -### Couch Models - -Couch models should accept domain models during construction and offer a ToDomainModel method which constructs a domain model. -In this way, domain models have the freedom to define their valid states. diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs deleted file mode 100644 index a3dd34e..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels -{ - public class Session : CouchDocument - { - public string Name { get; set; } - public string Player1 { get; set; } - public string? Player2 { get; set; } - public bool IsPrivate { get; set; } - - /// - /// Default constructor and setters are for deserialization. - /// - public Session() : base() - { - Name = string.Empty; - Player1 = string.Empty; - Player2 = string.Empty; - } - - public Session(string id, Models.Session session) : base(id, nameof(Session)) - { - Name = session.Name; - Player1 = session.Player1; - Player2 = session.Player2; - IsPrivate = session.IsPrivate; - } - - public Models.Session ToDomainModel() => new(Name, IsPrivate, Player1, Player2); - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs new file mode 100644 index 0000000..59e8bf8 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class SessionDocument : CouchDocument + { + public string Name { get; set; } + public string Player1 { get; set; } + public string? Player2 { get; set; } + public bool IsPrivate { get; set; } + public IList History { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public SessionDocument() : base(WhichDocumentType.Session) + { + Name = string.Empty; + Player1 = string.Empty; + Player2 = string.Empty; + History = new List(0); + } + + public SessionDocument(Models.Session session) + : base(session.Name, WhichDocumentType.Session) + { + Name = session.Name; + Player1 = session.Player1; + Player2 = session.Player2; + IsPrivate = session.IsPrivate; + History = new List(0); + } + + public SessionDocument(Models.SessionMetadata sessionMetaData) + : base(sessionMetaData.Name, WhichDocumentType.Session) + { + Name = sessionMetaData.Name; + Player1 = sessionMetaData.Player1; + Player2 = sessionMetaData.Player2; + IsPrivate = sessionMetaData.IsPrivate; + History = new List(0); + } + + public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2); + + public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs similarity index 58% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs rename to Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs index 22d30c7..7fcb388 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,8 +1,6 @@ -using System; - -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { - public class User : CouchDocument + public class UserDocument : CouchDocument { public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; @@ -14,7 +12,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public string Name { get; set; } public LoginPlatform Platform { get; set; } - public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User)) + public UserDocument(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", WhichDocumentType.User) { Name = name; Platform = platform; diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs new file mode 100644 index 0000000..f5a4f72 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs @@ -0,0 +1,9 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public enum WhichDocumentType + { + User, + Session, + BoardState + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 162e2df..a7d9617 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -12,14 +12,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move); Task CreateGuestUser(string userName); - Task CreateSession(Models.Session session); - Task> ReadSessions(); + Task CreateSession(Models.SessionMetadata session); + Task> ReadSessionMetadatas(); Task IsGuestUser(string userName); Task PostJoinCode(string gameName, string userName); Task ReadSession(string name); - Task> ReadBoardStates(string name); + Task ReadShogi(string name); + Task UpdateSession(Models.Session session); } public class GameboardRepository : IGameboardRepository @@ -34,75 +34,99 @@ namespace Gameboard.ShogiUI.Sockets.Repositories this.logger = logger; } - public async Task> ReadSessions() + public async Task> ReadSessionMetadatas() { - var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}"; - var query = $@"{{ ""selector"": {selector} }}"; - var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var selector = new Dictionary(2) + { + [nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session + }; + var q = new { Selector = selector }; + var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync("_find", content); var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); + var sessions = JsonConvert.DeserializeObject>(responseContent).docs; - if (result == null) - { - logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); - return Array.Empty(); - } - return result.docs - .Select(_ => _.ToDomainModel()) + return sessions + .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2)) .ToList(); } public async Task ReadSession(string name) { + var readShogiTask = ReadShogi(name); var response = await client.GetAsync(name); var responseContent = await response.Content.ReadAsStringAsync(); - var couchModel = JsonConvert.DeserializeObject(responseContent); - return couchModel.ToDomainModel(); + var couchModel = JsonConvert.DeserializeObject(responseContent); + var shogi = await readShogiTask; + if (shogi == null) + { + return null; + } + return couchModel.ToDomainModel(shogi); } - public async Task> ReadBoardStates(string name) + public async Task ReadShogi(string name) { - var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}"; - var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}"; - var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}"; + var selector = new Dictionary(2) + { + [nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState, + [nameof(BoardStateDocument.Name)] = name + }; + var sort = new Dictionary(1) + { + [nameof(BoardStateDocument.CreatedDate)] = "desc" + }; + var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } }); + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); var response = await client.PostAsync("_find", content); var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - - if (result == null) + if (!response.IsSuccessStatusCode) { - logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); - return Array.Empty(); + logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query); + return null; } - return result.docs - .Select(_ => new Models.BoardState(_)) - .ToList(); + var boardStates = JsonConvert + .DeserializeObject>(responseContent) + .docs; + if (boardStates.Length == 0) return null; + + // Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session. + var moves = boardStates.Skip(1).Select(couchModel => + { + var move = couchModel.Move; + Models.Move model = move!.PieceFromHand.HasValue + ? new Models.Move(move.PieceFromHand.Value, move.To) + : new Models.Move(move.From!, move.To, move.IsPromotion); + return model; + }).ToList(); + return new Models.Shogi(moves); } - //public async Task DeleteGame(string gameName) - //{ - // //var uri = $"Session/{gameName}"; - // //await client.DeleteAsync(Uri.EscapeUriString(uri)); - //} - - public async Task CreateSession(Models.Session session) + public async Task CreateSession(Models.SessionMetadata session) { - var couchModel = new Session(session.Name, session); + var sessionDocument = new SessionDocument(session); + var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); + var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); + + var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi()); + var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + + if ((await postSessionDocumentTask).IsSuccessStatusCode) + { + var response = await client.PostAsync(string.Empty, boardStateContent); + return response.IsSuccessStatusCode; + } + return false; + } + + public async Task UpdateSession(Models.Session session) + { + var couchModel = new SessionDocument(session); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); + var response = await client.PutAsync(couchModel.Id, content); return response.IsSuccessStatusCode; } - - public async Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move) - { - var couchModel = new BoardState(sessionName, boardState, move); - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - return response.IsSuccessStatusCode; - } - //public async Task PutJoinPublicSession(PutJoinPublicSession request) //{ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); @@ -169,7 +193,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task CreateGuestUser(string userName) { - var couchModel = new User(userName, User.LoginPlatform.Guest); + var couchModel = new UserDocument(userName, UserDocument.LoginPlatform.Guest); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); return response.IsSuccessStatusCode; @@ -177,7 +201,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories public async Task IsGuestUser(string userName) { - var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}")); + var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{UserDocument.GetDocumentId(userName)}")); var response = await client.SendAsync(req); return response.IsSuccessStatusCode; } diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs new file mode 100644 index 0000000..0535a8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/CreateGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class CreateGameRequestValidator : AbstractValidator + { + public CreateGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.CreateGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs new file mode 100644 index 0000000..e3837a0 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class JoinByCodeRequestValidator : AbstractValidator + { + public JoinByCodeRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode); + RuleFor(_ => _.JoinCode).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs new file mode 100644 index 0000000..fa0e2d5 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class JoinGameRequestValidator : AbstractValidator + { + public JoinGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.JoinGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs new file mode 100644 index 0000000..c2ddc8e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/ListGamesRequestValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class ListGamesRequestValidator : AbstractValidator + { + public ListGamesRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.ListGames); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs new file mode 100644 index 0000000..5a4ad8a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/LoadGameRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class LoadGameRequestValidator : AbstractValidator + { + public LoadGameRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.LoadGame); + RuleFor(_ => _.GameName).NotEmpty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs new file mode 100644 index 0000000..2eb06f1 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/MoveRequestValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators +{ + public class MoveRequestValidator : AbstractValidator + { + public MoveRequestValidator() + { + RuleFor(_ => _.Action).Equal(ClientAction.Move); + RuleFor(_ => _.GameName).NotEmpty(); + RuleFor(_ => _.Move.From) + .Null() + .When(_ => _.Move.PieceFromCaptured.HasValue) + .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); + RuleFor(_ => _.Move.From) + .NotEmpty() + .When(_ => !_.Move.PieceFromCaptured.HasValue) + .WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties."); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs new file mode 100644 index 0000000..f8be6cf --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -0,0 +1,194 @@ +using FluentValidation; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; +using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets.Services +{ + public interface ISocketService + { + Task HandleSocketRequest(HttpContext context); + } + + /// + /// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages. + /// + public class SocketService : ISocketService + { + private readonly ILogger logger; + private readonly ISocketConnectionManager communicationManager; + private readonly ISocketTokenManager tokenManager; + private readonly ICreateGameHandler createGameHandler; + private readonly IJoinByCodeHandler joinByCodeHandler; + private readonly IJoinGameHandler joinGameHandler; + private readonly IListGamesHandler listGamesHandler; + private readonly ILoadGameHandler loadGameHandler; + private readonly IMoveHandler moveHandler; + private readonly IValidator createGameRequestValidator; + private readonly IValidator joinByCodeRequestValidator; + private readonly IValidator joinGameRequestValidator; + private readonly IValidator listGamesRequestValidator; + private readonly IValidator loadGameRequestValidator; + private readonly IValidator moveRequestValidator; + + public SocketService( + ILogger logger, + ISocketConnectionManager communicationManager, + ISocketTokenManager tokenManager, + ICreateGameHandler createGameHandler, + IJoinByCodeHandler joinByCodeHandler, + IJoinGameHandler joinGameHandler, + IListGamesHandler listGamesHandler, + ILoadGameHandler loadGameHandler, + IMoveHandler moveHandler, + IValidator createGameRequestValidator, + IValidator joinByCodeRequestValidator, + IValidator joinGameRequestValidator, + IValidator listGamesRequestValidator, + IValidator loadGameRequestValidator, + IValidator moveRequestValidator + ) : base() + { + this.logger = logger; + this.communicationManager = communicationManager; + this.tokenManager = tokenManager; + this.createGameHandler = createGameHandler; + this.joinByCodeHandler = joinByCodeHandler; + this.joinGameHandler = joinGameHandler; + this.listGamesHandler = listGamesHandler; + this.loadGameHandler = loadGameHandler; + this.moveHandler = moveHandler; + this.createGameRequestValidator = createGameRequestValidator; + this.joinByCodeRequestValidator = joinByCodeRequestValidator; + this.joinGameRequestValidator = joinGameRequestValidator; + this.listGamesRequestValidator = listGamesRequestValidator; + this.loadGameRequestValidator = loadGameRequestValidator; + this.moveRequestValidator = moveRequestValidator; + } + + public async Task HandleSocketRequest(HttpContext context) + { + var hasToken = context.Request.Query.Keys.Contains("token"); + if (hasToken) + { + var oneTimeToken = context.Request.Query["token"][0]; + var tokenAsGuid = Guid.Parse(oneTimeToken); + var userName = tokenManager.GetUsername(tokenAsGuid); + if (userName != null) + { + var socket = await context.WebSockets.AcceptWebSocketAsync(); + + communicationManager.SubscribeToBroadcast(socket, userName); + while (socket.State == WebSocketState.Open) + { + try + { + var message = await socket.ReceiveTextAsync(); + if (string.IsNullOrWhiteSpace(message)) continue; + logger.LogInformation("Request \n{0}\n", message); + var request = JsonConvert.DeserializeObject(message); + if (!Enum.IsDefined(typeof(ClientAction), request.Action)) + { + await socket.SendTextAsync("Error: Action not recognized."); + continue; + } + switch (request.Action) + { + case ClientAction.ListGames: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) + { + await listGamesHandler.Handle(req, userName); + } + break; + } + case ClientAction.CreateGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) + { + await createGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) + { + await joinGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) + { + await joinByCodeHandler.Handle(req, userName); + } + break; + } + case ClientAction.LoadGame: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) + { + await loadGameHandler.Handle(req, userName); + } + break; + } + case ClientAction.Move: + { + var req = JsonConvert.DeserializeObject(message); + if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) + { + await moveHandler.Handle(req, userName); + } + break; + } + } + } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } + } + communicationManager.UnsubscribeFromBroadcastAndGames(userName); + return; + } + } + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + + public async Task ValidateRequestAndReplyIfInvalid(WebSocket socket, IValidator validator, TRequest request) + { + var results = validator.Validate(request); + if (!results.IsValid) + { + await socket.SendTextAsync(string.Join('\n', results.Errors.Select(_ => _.ErrorMessage).ToString())); + } + return results.IsValid; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index cda318d..a8e52ac 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -1,8 +1,11 @@ +using FluentValidation; using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.Services; +using Gameboard.ShogiUI.Sockets.Services.RequestValidators; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -39,11 +42,19 @@ namespace Gameboard.ShogiUI.Sockets services.AddSingleton(); // Managers - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Services + services.AddSingleton, CreateGameRequestValidator>(); + services.AddSingleton, JoinByCodeRequestValidator>(); + services.AddSingleton, JoinGameRequestValidator>(); + services.AddSingleton, ListGamesRequestValidator>(); + services.AddSingleton, LoadGameRequestValidator>(); + services.AddSingleton, MoveRequestValidator>(); + services.AddSingleton(); // Repositories services.AddHttpClient("couchdb", c => @@ -77,7 +88,7 @@ namespace Gameboard.ShogiUI.Sockets } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) { var origins = new[] { "http://localhost:3000", "https://localhost:3000", @@ -135,10 +146,13 @@ namespace Gameboard.ShogiUI.Sockets Formatting = Formatting.Indented, ContractResolver = new DefaultContractResolver { - NamingStrategy = new CamelCaseNamingStrategy(), + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = true + } }, Converters = new[] { new StringEnumConverter() }, - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, }; } } diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index 02bde77..a15e786 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -5,10 +5,12 @@ + + diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs new file mode 100644 index 0000000..1ef33de --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PlanarCollectionShould.cs @@ -0,0 +1,92 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PathFinding; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.UnitTests.PathFinding +{ + [TestClass] + public class PlanarCollectionShould + { + private class SimpleElement : IPlanarElement + { + public static int Seed { get; private set; } + public MoveSet MoveSet => null; + public bool IsUpsideDown => false; + + + public SimpleElement() + { + Seed = Seed++; + } + } + + private Fixture fixture; + + [TestInitialize] + public void TestInitialize() + { + fixture = new Fixture(); + } + + [TestMethod] + public void Index() + { + // Arrange + var collection = new PlanarCollection(10, 10); + var expected1 = new SimpleElement(); + var expected2 = new SimpleElement(); + + // Act + collection[0, 0] = expected1; + collection[2, 1] = expected2; + + // Assert + collection[0, 0].Should().Be(expected1); + collection[2, 1].Should().Be(expected2); + } + + [TestMethod] + public void Iterate() + { + // Arrange + var expected = new List(); + for (var i = 0; i < 9; i++) expected.Add(new SimpleElement()); + var collection = new PlanarCollection(3, 3); + for (var x = 0; x < 3; x++) + for (var y = 0; y < 3; y++) + collection[x, y] = expected[x + y]; + + // Act + var actual = new List(); + foreach (var elem in collection) + actual.Add(elem); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void Yep() + { + var collection = new PlanarCollection(3, 3); + collection[0, 0] = new SimpleElement(); + collection[1, 0] = new SimpleElement(); + collection[0, 1] = new SimpleElement(); + + // Act + var array2d = new SimpleElement[3, 3]; + for (var x = 0; x < 3; x++) + for (var y = 0; y < 3; y++) + { + array2d[x, y] = collection[x, y]; + } + + + Console.WriteLine("hey"); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index c558571..e5151c1 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -1,11 +1,10 @@ using FluentAssertions; -using Gameboard.ShogiUI.Rules; -using Gameboard.ShogiUI.Rules.Pieces; +using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Linq; using System.Numerics; - +using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPlayer; +using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPiece; namespace Gameboard.ShogiUI.UnitTests.Rules { [TestClass] @@ -15,54 +14,54 @@ namespace Gameboard.ShogiUI.UnitTests.Rules public void InitializeBoardState() { // Assert - var board = new ShogiBoard().Board; + var board = new Shogi().Board; // Assert pieces do not start promoted. foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); // Assert Player1. for (var y = 0; y < 3; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); + board[y, x]?.Owner.Should().Be(WhichPlayer.Player2); board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); - board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[4, 0].WhichPiece.Should().Be(WhichPiece.King); - board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); - board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); - board[0, 1].Should().BeNull(); + board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); + board[0, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[0, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[0, 4].WhichPiece.Should().Be(WhichPiece.King); + board[0, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[0, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[0, 7].WhichPiece.Should().Be(WhichPiece.Knight); + board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 0].Should().BeNull(); board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook); - for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); - board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); - board[8, 1].Should().BeNull(); - for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + for (var x = 2; x < 7; x++) board[1, x].Should().BeNull(); + board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + board[1, 8].Should().BeNull(); + for (var x = 0; x < 9; x++) board[2, x].WhichPiece.Should().Be(WhichPiece.Pawn); // Assert empty locations. for (var y = 3; y < 6; y++) for (var x = 0; x < 9; x++) - board[x, y].Should().BeNull(); + board[y, x].Should().BeNull(); // Assert Player2. for (var y = 6; y < 9; y++) for (var x = 0; x < 9; x++) - board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); - board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); - board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[4, 8].WhichPiece.Should().Be(WhichPiece.King); - board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[y, x]?.Owner.Should().Be(WhichPlayer.Player1); + board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[8, 1].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[8, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[8, 4].WhichPiece.Should().Be(WhichPiece.King); + board[8, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board[8, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[8, 7].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - board[0, 7].Should().BeNull(); - board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); - for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); + board[7, 0].Should().BeNull(); + board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + for (var x = 2; x < 7; x++) board[7, x].Should().BeNull(); board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook); - board[8, 7].Should().BeNull(); - for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + board[7, 8].Should().BeNull(); + for (var x = 0; x < 9; x++) board[6, x].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -70,60 +69,52 @@ namespace Gameboard.ShogiUI.UnitTests.Rules { var moves = new[] { - new Move - { // Pawn - From = new Vector2(0, 6), - To = new Vector2(0, 5) - } + new Move(new Vector2(0, 6), new Vector2(0, 5)) }; - var shogi = new ShogiBoard(moves); - shogi.Board[0, 6].Should().BeNull(); - shogi.Board[0, 5].WhichPiece.Should().Be(WhichPiece.Pawn); + var shogi = new Shogi(moves); + shogi.Board[6, 0].Should().BeNull(); + shogi.Board[5, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] public void PreventInvalidMoves_MoveFromEmptyPosition() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); // Prerequisit shogi.Board[4, 4].Should().BeNull(); // Act - var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }); + var moveSuccess = shogi.Move(new Move(new Vector2(4, 4), new Vector2(4, 5))); // Assert moveSuccess.Should().BeFalse(); shogi.Board[4, 4].Should().BeNull(); - shogi.Board[4, 5].Should().BeNull(); + shogi.Board[5, 4].Should().BeNull(); } [TestMethod] public void PreventInvalidMoves_MoveToCurrentPosition() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 6), To = new Vector2(0, 6) }); + var moveSuccess = shogi.Move(new Move(new Vector2(0, 6), new Vector2(0, 6))); // Assert moveSuccess.Should().BeFalse(); - shogi.Board[0, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[6, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] public void PreventInvalidMoves_MoveSet() { - var invalidLanceMove = new Move - { - // Bishop moving lateral - From = new Vector2(1, 1), - To = new Vector2(2, 1) - }; + // Bishop moving lateral + var invalidLanceMove = new Move(new Vector2(1, 1), new Vector2(2, 1)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); @@ -135,30 +126,26 @@ namespace Gameboard.ShogiUI.UnitTests.Rules public void PreventInvalidMoves_Ownership() { // Arrange - var shogi = new ShogiBoard(); + var shogi = new Shogi(); shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); - shogi.Board[8, 2].Owner.Should().Be(WhichPlayer.Player2); + shogi.Board[2, 8].Owner.Should().Be(WhichPlayer.Player2); // Act - Move Player2 Pawn when it's Player1 turn. - var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }); + var moveSuccess = shogi.Move(new Move(new Vector2(8, 2), new Vector2(8, 3))); // Assert moveSuccess.Should().BeFalse(); - shogi.Board[8, 6].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board[8, 5].Should().BeNull(); + shogi.Board[6, 8].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[5, 8].Should().BeNull(); } [TestMethod] public void PreventInvalidMoves_MoveThroughAllies() { - var invalidLanceMove = new Move - { - // Lance moving through the pawn before it. - From = new Vector2(0, 8), - To = new Vector2(0, 4) - }; + // Lance moving through the pawn before it. + var invalidLanceMove = new Move(new Vector2(0, 8), new Vector2(0, 4)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidLanceMove); moveSuccess.Should().BeFalse(); @@ -169,20 +156,16 @@ namespace Gameboard.ShogiUI.UnitTests.Rules [TestMethod] public void PreventInvalidMoves_CaptureAlly() { - var invalidKnightMove = new Move - { - // Knight capturing allied Pawn - From = new Vector2(1, 8), - To = new Vector2(0, 6) - }; + // Knight capturing allied Pawn + var invalidKnightMove = new Move(new Vector2(1, 8), new Vector2(0, 6)); - var shogi = new ShogiBoard(); + var shogi = new Shogi(); var moveSuccess = shogi.Move(invalidKnightMove); moveSuccess.Should().BeFalse(); // Assert the Knight has not actually moved or captured. - shogi.Board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); - shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board[2, 0].WhichPiece.Should().Be(WhichPiece.Pawn); } [TestMethod] @@ -192,25 +175,25 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(6, 2), new Vector2(6, 3)), // P1 Bishop puts P2 in check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) } + new Move(new Vector2(1, 7), new Vector2(6, 2)) }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisit shogi.InCheck.Should().Be(WhichPlayer.Player2); // Act - P2 moves Lance while remaining in check. - var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }); + var moveSuccess = shogi.Move(new Move(new Vector2(0, 8), new Vector2(0, 7))); // Assert moveSuccess.Should().BeFalse(); shogi.InCheck.Should().Be(WhichPlayer.Player2); shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board[8, 7].Should().BeNull(); + shogi.Board[7, 8].Should().BeNull(); } [TestMethod] @@ -220,31 +203,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(0, 2), new Vector2(0, 3) ), // P1 Bishop takes P2 Pawn - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2) ), // P2 Gold, block check from P1 Bishop. - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1) ), // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - new Move { From = new Vector2(6, 2), To = new Vector2(7, 1), IsPromotion = true }, + new Move(new Vector2(6, 2), new Vector2(7, 1), true ), // P2 Pawn again - new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, + new Move(new Vector2(0, 3), new Vector2(0, 4) ), // P1 Bishop takes P2 Knight - new Move { From = new Vector2(7, 1), To = new Vector2(7, 0) }, + new Move(new Vector2(7, 1), new Vector2(7, 0) ), // P2 Pawn again - new Move { From = new Vector2(0, 4), To = new Vector2(0, 5) }, + new Move(new Vector2(0, 4), new Vector2(0, 5) ), // P1 Bishop takes P2 Lance - new Move { From = new Vector2(7, 0), To = new Vector2(8, 0) }, + new Move(new Vector2(7, 0), new Vector2(8, 0) ), // P2 Lance (move to make room for attempted P1 Pawn placement) - new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, + new Move(new Vector2(0, 0), new Vector2(0, 1) ), // P1 arbitrary move - new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, + new Move(new Vector2(4, 8), new Vector2(4, 7) ), // P2 Pawn again, takes P1 Pawn - new Move { From = new Vector2(0, 5), To = new Vector2(0, 6) }, + new Move(new Vector2(0, 5) , new Vector2(0, 6) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4); @@ -255,27 +238,27 @@ namespace Gameboard.ShogiUI.UnitTests.Rules // Act | Assert - It is P1 turn /// try illegally placing Knight from the hand. - shogi.Board[7, 0].Should().BeNull(); - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 0) }); + shogi.Board[0, 7].Should().BeNull(); + var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 0].Should().BeNull(); - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 1) }); + shogi.Board[0, 7].Should().BeNull(); + dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 1))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 1].Should().BeNull(); + shogi.Board[1, 7].Should().BeNull(); /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Board[7, 0].Should().BeNull(); + shogi.Board[0, 7].Should().BeNull(); /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(7, 0))); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Board[7, 0].Should().BeNull(); + shogi.Board[0, 7].Should().BeNull(); } [TestMethod] @@ -285,34 +268,34 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), + // P2 Pawn + new Move(new Vector2(8, 2), new Vector2(8, 3)), // P1 Bishop, check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2)), // P2 Gold, block check - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1)), // P1 arbitrary move - new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, + new Move(new Vector2(0, 6), new Vector2(0, 5)), // P2 Bishop - new Move { From = new Vector2(7, 1), To = new Vector2(8, 2) }, + new Move(new Vector2(7, 1), new Vector2(8, 2)), // P1 Bishop takes P2 Lance - new Move { From = new Vector2(6, 2), To = new Vector2(8, 0) }, + new Move(new Vector2(6, 2), new Vector2(8, 0)), // P2 Bishop - new Move { From = new Vector2(8, 2), To = new Vector2(7, 1) }, + new Move(new Vector2(8, 2), new Vector2(7, 1)), // P1 arbitrary move - new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, + new Move(new Vector2(0, 5), new Vector2(0, 4)), // P2 Bishop, check - new Move { From = new Vector2(7, 1), To = new Vector2(2, 6) }, + new Move(new Vector2(7, 1), new Vector2(2, 6)), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.InCheck.Should().Be(WhichPlayer.Player1); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act - P1 tries to place a Lance while in check. - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) }); + var dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(4, 4))); // Assert dropSuccess.Should().BeFalse(); @@ -328,31 +311,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3)), // P1 Bishop, capture P2 Pawn, check - new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, + new Move(new Vector2(1, 7), new Vector2(6, 2)), // P2 Gold, block check - new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, + new Move(new Vector2(5, 0), new Vector2(5, 1)), // P1 Bishop capture P2 Bishop - new Move { From = new Vector2(6, 2), To = new Vector2(7, 1) }, + new Move(new Vector2(6, 2), new Vector2(7, 1)), // P2 arbitrary move - new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, + new Move(new Vector2(0, 0), new Vector2(0, 1)), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Prerequisites shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[4, 0].Should().NotBeNull(); + shogi.Board[0, 4].Should().NotBeNull(); // Act - P1 tries to place Bishop from hand to an already-occupied position - var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) }); + var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, new Vector2(4, 0))); // Assert dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board[4, 0].WhichPiece.Should().Be(WhichPiece.King); + shogi.Board[0, 4].WhichPiece.Should().Be(WhichPiece.King); } [TestMethod] @@ -362,14 +345,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Bishop, check - shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }); + shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2))); // Assert shogi.InCheck.Should().Be(WhichPlayer.Player2); @@ -382,14 +365,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, + new Move(new Vector2(2, 6), new Vector2(2, 5)), // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } + new Move(new Vector2(6, 2), new Vector2(6, 3)) }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(7, 1) }); + var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(7, 1))); // Assert moveSuccess.Should().BeTrue(); @@ -398,20 +381,20 @@ namespace Gameboard.ShogiUI.UnitTests.Rules .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) .Should() .Be(1); - shogi.Board[1, 7].Should().BeNull(); - shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Board[7, 1].Should().BeNull(); + shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); shogi.Hands[WhichPlayer.Player1] .Should() .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); // Act - P2 Silver captures P1 Bishop - moveSuccess = shogi.Move(new Move { From = new Vector2(6, 0), To = new Vector2(7, 1) }); + moveSuccess = shogi.Move(new Move(new Vector2(6, 0), new Vector2(7, 1))); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[6, 0].Should().BeNull(); - shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + shogi.Board[0, 6].Should().BeNull(); + shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); shogi.Board .Cast() .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) @@ -428,19 +411,19 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Pawn - new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, - // P2 Pawn - new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } - }; - var shogi = new ShogiBoard(moves); + new Move(new Vector2(2, 6), new Vector2(2, 5) ), + // P2 Pawn + new Move(new Vector2(6, 2), new Vector2(6, 3) ) + }; + var shogi = new Shogi(moves); // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2), IsPromotion = true }); + var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2), true)); // Assert moveSuccess.Should().BeTrue(); - shogi.Board[1, 7].Should().BeNull(); - shogi.Board[6, 2].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); + shogi.Board[7, 1].Should().BeNull(); + shogi.Board[2, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); } [TestMethod] @@ -450,30 +433,30 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var moves = new[] { // P1 Rook - new Move { From = new Vector2(7, 7), To = new Vector2(4, 7) }, + new Move(new Vector2(7, 7), new Vector2(4, 7) ), // P2 Gold - new Move { From = new Vector2(3, 0), To = new Vector2(2, 1) }, + new Move(new Vector2(3, 0), new Vector2(2, 1) ), // P1 Pawn - new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, + new Move(new Vector2(4, 6), new Vector2(4, 5) ), // P2 other Gold - new Move { From = new Vector2(5, 0), To = new Vector2(6, 1) }, + new Move(new Vector2(5, 0), new Vector2(6, 1) ), // P1 same Pawn - new Move { From = new Vector2(4, 5), To = new Vector2(4, 4) }, + new Move(new Vector2(4, 5), new Vector2(4, 4) ), // P2 Pawn - new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, + new Move(new Vector2(4, 2), new Vector2(4, 3) ), // P1 Pawn takes P2 Pawn - new Move { From = new Vector2(4, 4), To = new Vector2(4, 3) }, + new Move(new Vector2(4, 4), new Vector2(4, 3) ), // P2 King - new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, + new Move(new Vector2(4, 0), new Vector2(4, 1) ), // P1 Pawn promotes, threatens P2 King - new Move { From = new Vector2(4, 3), To = new Vector2(4, 2), IsPromotion = true }, + new Move(new Vector2(4, 3), new Vector2(4, 2), true ), // P2 King retreat - new Move { From = new Vector2(4, 1), To = new Vector2(4, 0) }, + new Move(new Vector2(4, 1), new Vector2(4, 0) ), }; - var shogi = new ShogiBoard(moves); + var shogi = new Shogi(moves); // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 2), To = new Vector2(4, 1) }); + var moveSuccess = shogi.Move(new Move(new Vector2(4, 2), new Vector2(4, 1))); // Assert - checkmate moveSuccess.Should().BeTrue(); diff --git a/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs deleted file mode 100644 index f878da3..0000000 --- a/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentAssertions; -using Gameboard.ShogiUI.Sockets.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Gameboard.ShogiUI.UnitTests.Sockets -{ - [TestClass] - public class CoordsModelShould - { - [TestMethod] - public void ConvertToNotation() - { - var letters = "ABCDEFGHI"; - - for (var x = 0; x < 8; x++) // file - { - for (var y = 0; y < 8; y++) // rank - { - var move = new Coords(x, y); - var actual = move.ToBoardNotation(); - var expected = $"{letters[x]}{y + 1}"; - actual.Should().Be(expected); - } - } - } - } -} diff --git a/Gameboard.ShogiUI.xUnitTests/GameShould.cs b/Gameboard.ShogiUI.xUnitTests/GameShould.cs new file mode 100644 index 0000000..80f299d --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/GameShould.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests +{ + public class GameShould + { + [Fact] + public void DiscardNullPLayers() + { + var game = new Game("Test", "P1", null); + + game.Players.Count.Should().Be(1); + } + } +} diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj new file mode 100644 index 0000000..5cc61ef --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs new file mode 100644 index 0000000..2ae7da4 --- /dev/null +++ b/Gameboard.ShogiUI.xUnitTests/RequestValidators/MoveRequestValidatorShould.cs @@ -0,0 +1,76 @@ +using AutoFixture; +using FluentAssertions; +using FluentAssertions.Execution; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Gameboard.ShogiUI.Sockets.Services.RequestValidators; +using Xunit; + +namespace Gameboard.ShogiUI.xUnitTests.RequestValidators +{ + public class MoveRequestValidatorShould + { + private readonly Fixture fixture; + private readonly MoveRequestValidator validator; + public MoveRequestValidatorShould() + { + fixture = new Fixture(); + validator = new MoveRequestValidator(); + } + + [Fact] + public void PreventInvalidPropertyCombinations() + { + // Arrange + var request = fixture.Create(); + + // Act + var results = validator.Validate(request); + + // Assert + using (new AssertionScope()) + { + results.IsValid.Should().BeFalse(); + } + } + + [Fact] + public void AllowValidPropertyCombinations() + { + // Arrange + var requestWithoutFrom = new MoveRequest() + { + Action = ClientAction.Move, + GameName = "Some game name", + Move = new Move() + { + IsPromotion = false, + PieceFromCaptured = WhichPiece.Bishop, + To = "A4" + } + }; + var requestWithoutPieceFromCaptured = new MoveRequest() + { + Action = ClientAction.Move, + GameName = "Some game name", + Move = new Move() + { + From = "A1", + IsPromotion = false, + To = "A4" + } + }; + + // Act + var results = validator.Validate(requestWithoutFrom); + var results2 = validator.Validate(requestWithoutPieceFromCaptured); + + // Assert + using (new AssertionScope()) + { + results.IsValid.Should().BeTrue(); + results2.IsValid.Should().BeTrue(); + } + } + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index f2c9323..5f2f769 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -34,7 +35,7 @@ namespace PathFinding { return false; } - var element = collection[origin.X, origin.Y]; + var element = collection[origin.Y, origin.X]; if (element == null) return false; var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); @@ -49,7 +50,7 @@ namespace PathFinding while (shouldPath && next != destination) { next = Vector2.Add(next, path.Direction); - var collider = collection[(int)next.X, (int)next.Y]; + var collider = collection[(int)next.Y, (int)next.X]; if (collider != null) { callback?.Invoke(collider, next); @@ -65,14 +66,19 @@ namespace PathFinding public void PathEvery(Vector2 from, Callback callback) { - var element = collection[from.X, from.Y]; + var element = collection[from.Y, from.X]; + if (element == null) + { + Console.WriteLine("Null element in PathEvery"); + return; + } foreach (var path in element.MoveSet.GetMoves()) { var shouldPath = true; var next = Vector2.Add(from, path.Direction); ; while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) { - var collider = collection[(int)next.X, (int)next.Y]; + var collider = collection[(int)next.Y, (int)next.X]; if (collider != null) { callback(collider, next); @@ -97,7 +103,7 @@ namespace PathFinding var next = Vector2.Add(origin, direction); while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) { - var element = collection[next.X, next.Y]; + var element = collection[next.Y, next.X]; if (element != null) callback(element, next); next = Vector2.Add(next, direction); } diff --git a/Gameboard.ShogiUI.Rules/PlanarCollection.cs b/PathFinding/PlanarCollection.cs similarity index 72% rename from Gameboard.ShogiUI.Rules/PlanarCollection.cs rename to PathFinding/PlanarCollection.cs index d606134..9fa7266 100644 --- a/Gameboard.ShogiUI.Rules/PlanarCollection.cs +++ b/PathFinding/PlanarCollection.cs @@ -1,10 +1,10 @@ -using PathFinding; -using System; +using System; using System.Collections; using System.Collections.Generic; -namespace Gameboard.ShogiUI.Rules +namespace PathFinding { + // TODO: Get rid of this thing in favor of T[,] multi-dimensional array with extension methods. public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { public delegate void ForEachDelegate(T element, int x, int y); @@ -19,12 +19,12 @@ namespace Gameboard.ShogiUI.Rules array = new T[width * height]; } - public T? this[int x, int y] + public T? this[int y, int x] { get => array[y * width + x]; set => array[y * width + x] = value; } - public T? this[float x, float y] + public T? this[float y, float x] { get => array[(int)y * width + (int)x]; set => array[(int)y * width + (int)x] = value; @@ -32,8 +32,8 @@ namespace Gameboard.ShogiUI.Rules public int GetLength(int dimension) => dimension switch { - 0 => width, - 1 => height, + 0 => height, + 1 => width, _ => throw new IndexOutOfRangeException() }; @@ -43,15 +43,17 @@ namespace Gameboard.ShogiUI.Rules { for (var y = 0; y < height; y++) { - if (this[x, y] != null) - callback(this[x, y], x, y); + var elem = this[y, x]; + if (elem != null) + callback(elem, x, y); } } } public IEnumerator GetEnumerator() { - foreach (var item in array) yield return item; + foreach (var item in array) + if (item != null) yield return item; } IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator();