From f8f779e84cc9f37c1b4d87f573b336312f3b3d21 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 8 May 2021 10:26:04 -0500 Subject: [PATCH] before deleting Rules --- Benchmarking/Benchmarking.csproj | 2 +- Benchmarking/Benchmarks.cs | 52 ++-- .../CouchDB.csproj | 4 + CouchDB/CouchDocument.cs | 16 ++ CouchDB/Selectors/CouchQuery.cs | 27 ++ CouchDB/Selectors/Equals.cs | 18 ++ Gameboard.ShogiUI.BoardState/Move.cs | 14 -- Gameboard.ShogiUI.Domain/Entities/Board.cs | 6 - Gameboard.ShogiUI.Domain/Entities/Match.cs | 30 --- .../ValueObjects/Class1.cs | 15 -- .../Gameboard.ShogiUI.Rules.csproj | 1 + Gameboard.ShogiUI.Rules/Move.cs | 27 ++ .../Pieces/Bishop.cs | 0 .../Pieces/GoldGeneral.cs | 2 +- .../Pieces/King.cs | 0 .../Pieces/Knight.cs | 0 .../Pieces/Lance.cs | 0 .../Pieces/Pawn.cs | 0 .../Pieces/Piece.cs | 2 +- .../Pieces/Rook.cs | 0 .../Pieces/SilverGeneral.cs | 0 .../PlanarCollection.cs | 6 +- .../ShogiBoard.cs | 39 +-- .../WhichPiece.cs | 2 +- .../WhichPlayer.cs | 0 .../Api/Messages/GetGuestToken.cs | 2 +- .../Api/Messages/PostSession.cs | 10 + ...board.ShogiUI.Sockets.ServiceModels.csproj | 1 + .../Socket/Interfaces/IRequest.cs | 2 +- .../Socket/Messages/CreateGame.cs | 7 +- .../Socket/Messages/ErrorResponse.cs | 16 -- .../Socket/Messages/JoinByCode.cs | 11 - .../Socket/Messages/JoinGame.cs | 8 +- .../Socket/Messages/ListGames.cs | 2 +- .../Socket/Messages/LoadGame.cs | 3 +- .../Socket/Messages/Move.cs | 8 +- .../Socket/Types/BoardState.cs | 9 +- .../Socket/Types/ClientActionEnum.cs | 3 +- .../Socket/Types/Game.cs | 7 +- .../Socket/Types/Move.cs | 6 +- .../Socket/Types/Piece.cs | 4 +- .../Socket/Types/WhichPlayer.cs | 8 + Gameboard.ShogiUI.Sockets.sln | 22 +- .../Controllers/GameController.cs | 33 ++- .../Controllers/SocketController.cs | 18 +- .../Gameboard.ShogiUI.Sockets.csproj | 13 +- .../Managers/BoardManager.cs | 6 +- .../ClientActionHandlers/CreateGameHandler.cs | 60 ++--- .../ClientActionHandlers/IActionHandler.cs | 15 -- .../ClientActionHandlers/JoinByCodeHandler.cs | 81 +++--- .../ClientActionHandlers/JoinGameHandler.cs | 53 ++-- .../ClientActionHandlers/ListGamesHandler.cs | 23 +- .../ClientActionHandlers/LoadGameHandler.cs | 52 ++-- .../ClientActionHandlers/MoveHandler.cs | 81 +++--- .../Managers/SocketCommunicationManager.cs | 90 ++----- .../Managers/SocketConnectionManager.cs | 105 +++++++- .../Managers/SocketTokenManager.cs | 25 +- .../Managers/Utility/Request.cs | 1 - .../Models/BoardState.cs | 38 ++- Gameboard.ShogiUI.Sockets/Models/Move.cs | 89 ++----- Gameboard.ShogiUI.Sockets/Models/Piece.cs | 20 +- Gameboard.ShogiUI.Sockets/Models/Session.cs | 19 +- .../Repositories/CouchModels/BoardState.cs | 75 ++++++ .../CouchModels/CouchCreatedResult.cs | 15 ++ .../Repositories/CouchModels/CouchDocument.cs | 25 ++ .../CouchModels/CouchFindResult.cs | 14 ++ .../Repositories/CouchModels/Move.cs | 45 ++++ .../Repositories/CouchModels/Piece.cs | 27 ++ .../Repositories/CouchModels/Readme.md | 4 + .../Repositories/CouchModels/Session.cs | 30 +++ .../Repositories/CouchModels/User.cs | 23 ++ .../Repositories/GameboardRepository.cs | 233 ++++++++++-------- .../GameboardRepositoryManager.cs | 37 +-- .../Utility/AuthenticatedHttpClient.cs | 122 --------- Gameboard.ShogiUI.Sockets/Startup.cs | 43 ++-- Gameboard.ShogiUI.Sockets/appsettings.json | 9 +- .../Rules/BoardStateExtensions.cs | 2 +- .../Rules/ShogiBoardShould.cs | 20 +- PathFinding/IPlanarCollection.cs | 2 +- PathFinding/PathFinding.csproj | 1 + 80 files changed, 1109 insertions(+), 832 deletions(-) rename Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj => CouchDB/CouchDB.csproj (67%) create mode 100644 CouchDB/CouchDocument.cs create mode 100644 CouchDB/Selectors/CouchQuery.cs create mode 100644 CouchDB/Selectors/Equals.cs delete mode 100644 Gameboard.ShogiUI.BoardState/Move.cs delete mode 100644 Gameboard.ShogiUI.Domain/Entities/Board.cs delete mode 100644 Gameboard.ShogiUI.Domain/Entities/Match.cs delete mode 100644 Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Gameboard.ShogiUI.Rules.csproj (90%) create mode 100644 Gameboard.ShogiUI.Rules/Move.cs rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Bishop.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/GoldGeneral.cs (97%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/King.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Knight.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Lance.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Pawn.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Piece.cs (95%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/Rook.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/Pieces/SilverGeneral.cs (100%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/PlanarCollection.cs (92%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/ShogiBoard.cs (93%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/WhichPiece.cs (89%) rename {Gameboard.ShogiUI.BoardState => Gameboard.ShogiUI.Rules}/WhichPlayer.cs (100%) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj index ec8dd8e..d84d541 100644 --- a/Benchmarking/Benchmarking.csproj +++ b/Benchmarking/Benchmarking.csproj @@ -11,7 +11,7 @@ - + diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index b5785ed..31f6599 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -16,33 +16,33 @@ namespace Benchmarking public Benchmarks() { - moves = new[] - { - // P1 Rook - new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, - // P2 Gold - new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, - // P1 Pawn - new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, - // P2 other Gold - new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, - // P1 same Pawn - new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, - // P2 Pawn - new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, - // P1 Pawn takes P2 Pawn - new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, - // P2 King - new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, - // P1 Pawn promotes - new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, - // P2 King retreat - new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, - }; - var rand = new Random(); + //moves = new[] + //{ + // // P1 Rook + // new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, + // // P2 Gold + // new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, + // // P1 Pawn + // new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, + // // P2 other Gold + // new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, + // // P1 same Pawn + // new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, + // // P2 Pawn + // new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, + // // P1 Pawn takes P2 Pawn + // new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, + // // P2 King + // new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, + // // P1 Pawn promotes + // new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, + // // P2 King retreat + // new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, + //}; + //var rand = new Random(); - directions = new Vector2[10]; - for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); + //directions = new Vector2[10]; + //for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); } //[Benchmark] diff --git a/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj b/CouchDB/CouchDB.csproj similarity index 67% rename from Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj rename to CouchDB/CouchDB.csproj index f208d30..83f1b25 100644 --- a/Gameboard.ShogiUI.Domain/Gameboard.ShogiUI.Domain.csproj +++ b/CouchDB/CouchDB.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/CouchDB/CouchDocument.cs b/CouchDB/CouchDocument.cs new file mode 100644 index 0000000..8f29f6c --- /dev/null +++ b/CouchDB/CouchDocument.cs @@ -0,0 +1,16 @@ +namespace CouchDB +{ + public class CouchDocument + { + public readonly string _id; + public readonly string type; + public readonly T model; + + public CouchDocument(string id, T model) + { + _id = id; + this.model = model; + type = nameof(T); + } + } +} diff --git a/CouchDB/Selectors/CouchQuery.cs b/CouchDB/Selectors/CouchQuery.cs new file mode 100644 index 0000000..7bd7b29 --- /dev/null +++ b/CouchDB/Selectors/CouchQuery.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace CouchDB.Selectors +{ + public class CouchQuery + { + public static CouchQuery Select => new(); + + private readonly List equals; + protected CouchQuery() + { + equals = new List(); + } + + public CouchQuery WithEqual(string key, string value) + { + equals.Add(new Equals(key, value)); + return this; + } + + public override string ToString() + { + var selector = string.Join(",", equals); + return $"{{ \"selector\": {selector}"; + } + } +} diff --git a/CouchDB/Selectors/Equals.cs b/CouchDB/Selectors/Equals.cs new file mode 100644 index 0000000..418f547 --- /dev/null +++ b/CouchDB/Selectors/Equals.cs @@ -0,0 +1,18 @@ +namespace CouchDB.Selectors +{ + public class Equals + { + private readonly string key; + private readonly string value; + internal Equals(string key, string value) + { + this.key = key; + this.value = value; + } + + public override string ToString() + { + return $"{{ \"{key}\": {{ \"$eq\": {value}}} }}"; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs deleted file mode 100644 index 693c954..0000000 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics; -using System.Numerics; - -namespace Gameboard.ShogiUI.Rules -{ - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public WhichPiece? PieceFromCaptured { get; set; } - public Vector2 From { get; set; } - public Vector2 To { get; set; } - public bool IsPromotion { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Domain/Entities/Board.cs b/Gameboard.ShogiUI.Domain/Entities/Board.cs deleted file mode 100644 index 9533d8d..0000000 --- a/Gameboard.ShogiUI.Domain/Entities/Board.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Gameboard.ShogiUI.Domain -{ - public class Board - { - } -} diff --git a/Gameboard.ShogiUI.Domain/Entities/Match.cs b/Gameboard.ShogiUI.Domain/Entities/Match.cs deleted file mode 100644 index 2d54945..0000000 --- a/Gameboard.ShogiUI.Domain/Entities/Match.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Gameboard.ShogiUI.Domain -{ - public class Match - { - public string Name { get; } - public string Player1 { get; } - public string Player2 { get; } - - /// - /// Initialize pre-existing Match. - /// - public Match(MatchMeta meta, Board board) - { - Name = meta.Name; - Player1 = meta.Player1; - Player2 = meta.Player2; - } - - /// - /// Create a new Match. - /// - public Match(string name, string player1) - { - Name = name; - Player1 = player1; - } - - - } -} diff --git a/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs b/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs deleted file mode 100644 index 6939def..0000000 --- a/Gameboard.ShogiUI.Domain/ValueObjects/Class1.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Domain -{ - public class MatchMeta - { - public string Name { get; } - public string Player1 { get; } - public string Player2 { get; } - } -} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj similarity index 90% rename from Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj rename to Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj index 1a2ca2c..5befd78 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.Rules.csproj +++ b/Gameboard.ShogiUI.Rules/Gameboard.ShogiUI.Rules.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable diff --git a/Gameboard.ShogiUI.Rules/Move.cs b/Gameboard.ShogiUI.Rules/Move.cs new file mode 100644 index 0000000..81286fe --- /dev/null +++ b/Gameboard.ShogiUI.Rules/Move.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Gameboard.ShogiUI.Rules +{ + [DebuggerDisplay("{From} - {To}")] + public class Move + { + public WhichPiece? PieceFromHand { get; } + public Vector2? From { get; } + public Vector2 To { get; } + public bool IsPromotion { get; } + + public Move(Vector2 from, Vector2 to, bool isPromotion) + { + From = from; + To = to; + IsPromotion = isPromotion; + } + + public Move(WhichPiece pieceFromHand, Vector2 to) + { + PieceFromHand = pieceFromHand; + To = to; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.Rules/Pieces/Bishop.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs rename to Gameboard.ShogiUI.Rules/Pieces/Bishop.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs similarity index 97% rename from Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs rename to Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs index 055b779..fa984ab 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs +++ b/Gameboard.ShogiUI.Rules/Pieces/GoldGeneral.cs @@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces new PathFinding.Move(Direction.Right), new PathFinding.Move(Direction.Down) }; - public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) + public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner) { moveSet = new MoveSet(this, Moves); promotedMoveSet = new MoveSet(this, Moves); diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.Rules/Pieces/King.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/King.cs rename to Gameboard.ShogiUI.Rules/Pieces/King.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.Rules/Pieces/Knight.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Knight.cs rename to Gameboard.ShogiUI.Rules/Pieces/Knight.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.Rules/Pieces/Lance.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Lance.cs rename to Gameboard.ShogiUI.Rules/Pieces/Lance.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.Rules/Pieces/Pawn.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs rename to Gameboard.ShogiUI.Rules/Pieces/Pawn.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs similarity index 95% rename from Gameboard.ShogiUI.BoardState/Pieces/Piece.cs rename to Gameboard.ShogiUI.Rules/Pieces/Piece.cs index 1f019b2..b64584b 100644 --- a/Gameboard.ShogiUI.BoardState/Pieces/Piece.cs +++ b/Gameboard.ShogiUI.Rules/Pieces/Piece.cs @@ -25,7 +25,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces public bool CanPromote => !IsPromoted && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldenGeneral; + && WhichPiece != WhichPiece.GoldGeneral; public void ToggleOwnership() { diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.Rules/Pieces/Rook.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/Rook.cs rename to Gameboard.ShogiUI.Rules/Pieces/Rook.cs diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs rename to Gameboard.ShogiUI.Rules/Pieces/SilverGeneral.cs diff --git a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs b/Gameboard.ShogiUI.Rules/PlanarCollection.cs similarity index 92% rename from Gameboard.ShogiUI.BoardState/PlanarCollection.cs rename to Gameboard.ShogiUI.Rules/PlanarCollection.cs index 4baf867..d606134 100644 --- a/Gameboard.ShogiUI.BoardState/PlanarCollection.cs +++ b/Gameboard.ShogiUI.Rules/PlanarCollection.cs @@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Rules public class PlanarCollection : IPlanarCollection, IEnumerable where T : IPlanarElement { public delegate void ForEachDelegate(T element, int x, int y); - private readonly T[] array; + private readonly T?[] array; private readonly int width; private readonly int height; @@ -19,12 +19,12 @@ namespace Gameboard.ShogiUI.Rules array = new T[width * height]; } - public T this[int x, int y] + public T? this[int x, int y] { get => array[y * width + x]; set => array[y * width + x] = value; } - public T this[float x, float y] + public T? this[float x, float y] { get => array[(int)y * width + (int)x]; set => array[(int)y * width + (int)x] = value; diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.Rules/ShogiBoard.cs similarity index 93% rename from Gameboard.ShogiUI.BoardState/ShogiBoard.cs rename to Gameboard.ShogiUI.Rules/ShogiBoard.cs index 0449668..e5f104e 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.Rules/ShogiBoard.cs @@ -14,9 +14,8 @@ namespace Gameboard.ShogiUI.Rules public class ShogiBoard { private delegate void MoveSetCallback(Piece piece, Vector2 position); - private readonly bool isValidationBoard; private readonly PathFinder2D pathFinder; - private ShogiBoard validationBoard; + private ShogiBoard? validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } @@ -25,7 +24,6 @@ namespace Gameboard.ShogiUI.Rules public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } public bool IsCheckmate { get; private set; } - public string Error { get; private set; } @@ -41,6 +39,7 @@ namespace Gameboard.ShogiUI.Rules InitializeBoardState(); player1King = new Vector2(4, 8); player2King = new Vector2(4, 0); + Error = string.Empty; } public ShogiBoard(IList moves) : this() @@ -57,11 +56,16 @@ namespace Gameboard.ShogiUI.Rules private ShogiBoard(ShogiBoard toCopy) { - isValidationBoard = true; Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - Board[x, y] = toCopy.Board[x, y]?.DeepClone(); + { + var piece = toCopy.Board[x, y]; + if (piece != null) + { + Board[x, y] = piece.DeepClone(); + } + } pathFinder = new PathFinder2D(Board); MoveHistory = new List(toCopy.MoveHistory); @@ -72,6 +76,7 @@ namespace Gameboard.ShogiUI.Rules }; player1King = toCopy.player1King; player2King = toCopy.player2King; + Error = toCopy.Error; } public bool Move(Move move) @@ -103,7 +108,7 @@ namespace Gameboard.ShogiUI.Rules validationBoard = new ShogiBoard(this); } - var isValid = move.PieceFromCaptured.HasValue + var isValid = move.PieceFromHand.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); if (!isValid) @@ -123,20 +128,20 @@ namespace Gameboard.ShogiUI.Rules } // The move is valid and legal; update board state. - if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); + if (move.PieceFromHand.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } /// True if the move was successful. private bool PlaceFromHand(Move move) { - if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move - var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); + if (move.PieceFromHand.HasValue == false) return false; //Invalid move + var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) return false; // Invalid move if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; - switch (move.PieceFromCaptured.Value) + switch (move.PieceFromHand.Value) { case WhichPiece.Knight: // Knight cannot be placed onto the farthest two ranks from the hand. @@ -160,7 +165,7 @@ namespace Gameboard.ShogiUI.Rules /// True if the move was successful. private bool PlaceFromBoard(Move move) { - var fromPiece = Board[move.From.X, move.From.Y]; + var fromPiece = Board[move.From.Value.X, move.From.Value.Y]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; @@ -171,7 +176,7 @@ namespace Gameboard.ShogiUI.Rules Error = "Not allowed to move the opponents piece"; return false; // Invalid move; cannot move other players pieces. } - if (IsPathable(move.From, move.To) == false) + if (IsPathable(move.From.Value, move.To) == false) { Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; return false; // Invalid move; move not part of move-set. @@ -188,17 +193,17 @@ namespace Gameboard.ShogiUI.Rules //Mutate the board. if (move.IsPromotion) { - if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Y < 3)) + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Value.Y < 3)) { fromPiece.Promote(); } - else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Y > 5)) + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5)) { fromPiece.Promote(); } } Board[move.To.X, move.To.Y] = fromPiece; - Board[move.From.X, move.From.Y] = null; + Board[move.From.Value.X, move.From.Value.Y] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) @@ -239,7 +244,7 @@ namespace Gameboard.ShogiUI.Rules if (pathFinder.PathTo(move.To, kingPosition)) return true; // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From); + var direction = Vector2.Subtract(kingPosition, move.From.Value); var slope = Math.Abs(direction.Y / direction.X); // If absolute slope is 45°, look for a bishop along the line. // If absolute slope is 0° or 90°, look for a rook along the line. @@ -304,7 +309,7 @@ namespace Gameboard.ShogiUI.Rules pathFinder.PathEvery(from, (other, position) => { if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveToTry = new Move { From = from, To = position }; + var moveToTry = new Move(from, position, false); var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { diff --git a/Gameboard.ShogiUI.BoardState/WhichPiece.cs b/Gameboard.ShogiUI.Rules/WhichPiece.cs similarity index 89% rename from Gameboard.ShogiUI.BoardState/WhichPiece.cs rename to Gameboard.ShogiUI.Rules/WhichPiece.cs index ec4fd4d..5e6356c 100644 --- a/Gameboard.ShogiUI.BoardState/WhichPiece.cs +++ b/Gameboard.ShogiUI.Rules/WhichPiece.cs @@ -3,7 +3,7 @@ public enum WhichPiece { King, - GoldenGeneral, + GoldGeneral, SilverGeneral, Bishop, Rook, diff --git a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs b/Gameboard.ShogiUI.Rules/WhichPlayer.cs similarity index 100% rename from Gameboard.ShogiUI.BoardState/WhichPlayer.cs rename to Gameboard.ShogiUI.Rules/WhichPlayer.cs diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs index 6f6a751..6049f8e 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs @@ -4,7 +4,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages { public class GetGuestToken { - public string ClientId { get; set; } + public string? ClientId { get; set; } } public class GetGuestTokenResponse diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs new file mode 100644 index 0000000..8b7061a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostSession.cs @@ -0,0 +1,10 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages +{ + public class PostSession + { + public string Name { get; set; } + public string Player1 { get; set; } + public string Player2 { get; set; } + public bool IsPrivate { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj index ee29921..4466c3d 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs index 79ec262..79f20e5 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs @@ -4,6 +4,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces { public interface IRequest { - ClientAction Action { get; set; } + ClientAction Action { get; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs index 09d7455..9fa701a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs @@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class CreateGameRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } + public string GameName { get; set; } = string.Empty; public bool IsPrivate { get; set; } } public class CreateGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public Game Game { get; set; } public string PlayerName { get; set; } @@ -20,6 +20,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public CreateGameResponse(ClientAction action) { Action = action.ToString(); + Error = string.Empty; + Game = new Game(); + PlayerName = string.Empty; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs deleted file mode 100644 index 376ffc8..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages -{ - public class ErrorResponse : IResponse - { - public string Action { get; private set; } - public string Error { get; set; } - - public ErrorResponse(ClientAction action) - { - Action = action.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs deleted file mode 100644 index 51f2f8d..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages -{ - public class JoinByCode : IRequest - { - public ClientAction Action { get; set; } - public string JoinCode { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs index ef8842d..b662cb1 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs @@ -3,6 +3,12 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { + public class JoinByCodeRequest : IRequest + { + public ClientAction Action { get; set; } + public string JoinCode { get; set; } + } + public class JoinGameRequest : IRequest { public ClientAction Action { get; set; } @@ -11,7 +17,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class JoinGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public string GameName { get; set; } public string PlayerName { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index c45d51c..ab9d67f 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -11,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class ListGamesResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public string Error { get; set; } public ICollection Games { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index 981ebf6..a879953 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { @@ -12,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class LoadGameResponse : IResponse { - public string Action { get; private set; } + public string Action { get; } public Game Game { get; set; } public BoardState BoardState { get; set; } public string Error { get; set; } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs index e46f9a0..3ce4c6e 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs @@ -6,8 +6,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public class MoveRequest : IRequest { public ClientAction Action { get; set; } - public string GameName { get; set; } - public Move Move { get; set; } + public string GameName { get; set; } = string.Empty; + public Move Move { get; set; } = new Move(); } public class MoveResponse : IResponse @@ -21,6 +21,10 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages public MoveResponse(ClientAction action) { Action = action.ToString(); + Error = string.Empty; + GameName = string.Empty; + BoardState = new BoardState(); + PlayerName = string.Empty; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs index b42c398..6ee3051 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class BoardState { - public Piece[,] Board { get; set; } - public IReadOnlyCollection Player1Hand { get; set; } - public IReadOnlyCollection Player2Hand { get; set; } + public Piece[,] Board { get; set; } = new Piece[0, 0]; + public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); + public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs index 7b91157..5215171 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs @@ -7,7 +7,6 @@ JoinGame, JoinByCode, LoadGame, - Move, - KeepAlive + Move } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index 3f5ddc6..8ff5fb1 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { public class Game { - public string GameName { get; set; } + public string GameName { get; set; } = string.Empty; /// /// Players[0] is the session owner, Players[1] is the other guy /// - public IReadOnlyList Players { get; set; } + public IReadOnlyList Players { get; set; } = Array.Empty(); } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs index 3ad3719..ef9723a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs @@ -2,11 +2,11 @@ { public class Move { - public string PieceFromCaptured { get; set; } + public WhichPiece? PieceFromCaptured { get; set; } /// Board position notation, like A3 or G1 - public string From { get; set; } + public string? From { get; set; } /// Board position notation, like A3 or G1 - public string To { get; set; } + public string To { get; set; } = string.Empty; public bool IsPromotion { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs index 8f9fd23..71d9dbc 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs @@ -2,8 +2,8 @@ { public class Piece { - public WhichPiece WhichPiece { get; set; } - public bool IsPromoted { get; set; } + public WhichPiece WhichPiece { get; set; } + public WhichPlayer Owner { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs new file mode 100644 index 0000000..835b8e6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 76d0058..a1576c2 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -9,15 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Domain", "Gameboard.ShogiUI.Domain\Gameboard.ShogiUI.Domain.csproj", "{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,10 +33,6 @@ Global {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.Build.0 = Release|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -49,10 +45,14 @@ Global {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.Build.0 = Release|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 72a1fa8..7900ed2 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,4 +1,5 @@ -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; @@ -14,17 +15,20 @@ namespace Gameboard.ShogiUI.Sockets.Controllers public class GameController : ControllerBase { private readonly IGameboardRepositoryManager manager; + private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; public GameController( IGameboardRepository repository, - IGameboardRepositoryManager manager) + IGameboardRepositoryManager manager, + ISocketCommunicationManager communicationManager) { this.manager = manager; + this.communicationManager = communicationManager; this.repository = repository; } - [Route("JoinCode")] + [HttpPost("JoinCode")] public async Task PostGameInvitation([FromBody] PostGameInvitation request) { var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; @@ -41,7 +45,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } [AllowAnonymous] - [Route("GuestJoinCode")] + [HttpPost("GuestJoinCode")] public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) { @@ -57,5 +61,26 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return new UnauthorizedResult(); } } + + // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. + //[Route("")] + //public async Task PostSession([FromBody] PostSession request) + //{ + // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); + // var success = await repository.CreateSession(model); + // if (success) + // { + // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Socket.Types.ClientAction.CreateGame) + // { + // Game = model.ToServiceModel(), + // PlayerName = + // } + // var task = request.IsPrivate + // ? communicationManager.BroadcastToPlayers(response, userName) + // : communicationManager.BroadcastToAll(response); + // return new CreatedResult("", null); + // } + // return new ConflictResult(); + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 4891963..f00b5bf 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,8 +1,10 @@ using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using System.Linq; using System.Threading.Tasks; @@ -13,18 +15,24 @@ namespace Gameboard.ShogiUI.Sockets.Controllers [ApiController] public class SocketController : ControllerBase { + private readonly ILogger logger; private readonly ISocketTokenManager tokenManager; private readonly IGameboardRepositoryManager gameboardManager; + private readonly IGameboardRepository gameboardRepository; public SocketController( - ISocketTokenManager tokenManager, - IGameboardRepositoryManager gameboardManager) + ILogger logger, + ISocketTokenManager tokenManager, + IGameboardRepositoryManager gameboardManager, + IGameboardRepository gameboardRepository) { + this.logger = logger; this.tokenManager = tokenManager; this.gameboardManager = gameboardManager; + this.gameboardRepository = gameboardRepository; } - [Route("Token")] + [HttpGet("Token")] public IActionResult GetToken() { var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; @@ -33,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } [AllowAnonymous] - [Route("GuestToken")] + [HttpGet("GuestToken")] public async Task GetGuestToken([FromQuery] GetGuestToken request) { if (request.ClientId == null) @@ -44,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers } else { - if (await gameboardManager.PlayerExists(request.ClientId)) + if (await gameboardRepository.IsGuestUser(request.ClientId)) { var token = tokenManager.GenerateToken(request.ClientId); return new JsonResult(new GetGuestTokenResponse(request.ClientId, token)); diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index dd477c5..c558037 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -4,10 +4,18 @@ net5.0 true 5 + enable - + + + + + + + + @@ -17,7 +25,8 @@ - + + diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs index 813887e..f153527 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -6,7 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers public interface IBoardManager { void Add(string sessionName, ShogiBoard board); - ShogiBoard Get(string sessionName); + ShogiBoard? Get(string sessionName); } public class BoardManager : IBoardManager @@ -20,10 +20,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); - public ShogiBoard Get(string sessionName) + public ShogiBoard? Get(string sessionName) { if (Boards.TryGetValue(sessionName, out var board)) + { return board; + } return null; } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index bd93ab6..8ff9dd1 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,61 +1,55 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface ICreateGameHandler + { + Task Handle(CreateGameRequest request, string userName); + } + // TODO: This doesn't need to be a socket action. // It can be an API route and still tell socket connections about the new session. - public class CreateGameHandler : IActionHandler + public class CreateGameHandler : ICreateGameHandler { - private readonly IGameboardRepository repository; + private readonly IGameboardRepositoryManager manager; private readonly ISocketCommunicationManager communicationManager; public CreateGameHandler( ISocketCommunicationManager communicationManager, - IGameboardRepository repository) + IGameboardRepositoryManager manager) { - this.repository = repository; + this.manager = manager; this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(CreateGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var sessionName = await repository.PostSession(new PostSession + var model = new Session(request.GameName, request.IsPrivate, userName); + var success = await manager.CreateSession(model); + + if (!success) { - SessionName = request.GameName, - PlayerName = userName, - IsPrivate = request.IsPrivate - }); + var error = new CreateGameResponse(request.Action) + { + Error = "Unable to create game with this name." + }; + await communicationManager.BroadcastToPlayers(error, userName); + } var response = new CreateGameResponse(request.Action) { PlayerName = userName, - Game = new Game - { - GameName = sessionName, - Players = new[] { userName } - } + Game = model.ToServiceModel() }; - if (string.IsNullOrWhiteSpace(sessionName)) - { - response.Error = "Game already exists."; - } + var task = request.IsPrivate + ? communicationManager.BroadcastToPlayers(response, userName) + : communicationManager.BroadcastToAll(response); - if (request.IsPrivate) - { - await communicationManager.BroadcastToPlayers(response, userName); - } - else - { - await communicationManager.BroadcastToAll(response); - } + await task; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs deleted file mode 100644 index 24cf9d2..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IActionHandler - { - /// - /// Responsible for parsing json and handling the request. - /// - Task Handle(string json, string userName); - } - - public delegate IActionHandler ActionHandlerResolver(ClientAction action); -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index 6f22279..9f3ae26 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -1,13 +1,14 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class JoinByCodeHandler : IActionHandler + public interface IJoinByCodeHandler + { + Task Handle(JoinByCodeRequest request, string userName); + } + public class JoinByCodeHandler : IJoinByCodeHandler { private readonly IGameboardRepository repository; private readonly ISocketCommunicationManager communicationManager; @@ -20,44 +21,44 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(JoinByCodeRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession - { - PlayerName = userName, - JoinCode = request.JoinCode - }); + //var request = JsonConvert.DeserializeObject(json); + //var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession + //{ + // PlayerName = userName, + // JoinCode = request.JoinCode + //}); - if (sessionName == null) - { - var response = new JoinGameResponse(ClientAction.JoinByCode) - { - PlayerName = userName, - GameName = sessionName, - Error = "Error joining game." - }; - await communicationManager.BroadcastToPlayers(response, userName); - } - else - { - // Other members of the game see a regular JoinGame occur. - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName, - GameName = sessionName - }; - // At this time, userName hasn't subscribed and won't receive this message. - await communicationManager.BroadcastToGame(sessionName, response); + //if (sessionName == null) + //{ + // var response = new JoinGameResponse(ClientAction.JoinByCode) + // { + // PlayerName = userName, + // GameName = sessionName, + // Error = "Error joining game." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} + //else + //{ + // // Other members of the game see a regular JoinGame occur. + // var response = new JoinGameResponse(ClientAction.JoinGame) + // { + // PlayerName = userName, + // GameName = sessionName + // }; + // // At this time, userName hasn't subscribed and won't receive this message. + // await communicationManager.BroadcastToGame(sessionName, response); - // The player joining sees the JoinByCode occur. - response = new JoinGameResponse(ClientAction.JoinByCode) - { - PlayerName = userName, - GameName = sessionName - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + // // The player joining sees the JoinByCode occur. + // response = new JoinGameResponse(ClientAction.JoinByCode) + // { + // PlayerName = userName, + // GameName = sessionName + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 150a137..a37cdfc 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -1,13 +1,14 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class JoinGameHandler : IActionHandler + public interface IJoinGameHandler + { + Task Handle(JoinGameRequest request, string userName); + } + public class JoinGameHandler : IJoinGameHandler { private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; @@ -19,30 +20,30 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(JoinGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); + //var request = JsonConvert.DeserializeObject(json); - var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession - { - PlayerName = userName, - SessionName = request.GameName - }); + //var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession + //{ + // PlayerName = userName, + // SessionName = request.GameName + //}); - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName, - GameName = request.GameName - }; - if (joinSucceeded) - { - await communicationManager.BroadcastToAll(response); - } - else - { - response.Error = "Game is full."; - await communicationManager.BroadcastToPlayers(response, userName); - } + //var response = new JoinGameResponse(ClientAction.JoinGame) + //{ + // PlayerName = userName, + // GameName = request.GameName + //}; + //if (joinSucceeded) + //{ + // await communicationManager.BroadcastToAll(response); + //} + //else + //{ + // response.Error = "Game is full."; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index d4379e3..41b5d5e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,16 +1,19 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using Newtonsoft.Json; using System.Linq; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface IListGamesHandler + { + Task Handle(ListGamesRequest request, string userName); + } + // TODO: This doesn't need to be a socket action. // It can be an HTTP route. - public class ListGamesHandler : IActionHandler + public class ListGamesHandler : IListGamesHandler { private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; @@ -23,16 +26,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.repository = repository; } - public async Task Handle(string json, string userName) + public async Task Handle(ListGamesRequest _, string userName) { - var request = JsonConvert.DeserializeObject(json); - var getGamesResponse = string.IsNullOrWhiteSpace(userName) - ? await repository.GetGames() - : await repository.GetGames(userName); - - var games = getGamesResponse.Sessions - .OrderBy(s => s.Player1 == userName || s.Player2 == userName) - .Select(s => new Session(s).ToServiceModel()); // yuck + var sessions = await repository.ReadSessions(); + var games = sessions.Select(s => s.ToServiceModel()); // yuck var response = new ListGamesResponse(ClientAction.ListGames) { diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 9efc43b..9fcb72f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -3,16 +3,20 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using System.Linq; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + public interface ILoadGameHandler + { + Task Handle(LoadGameRequest request, string userName); + } + /// /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. /// - public class LoadGameHandler : IActionHandler + public class LoadGameHandler : ILoadGameHandler { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; @@ -31,35 +35,35 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.boardManager = boardManager; } - public async Task Handle(string json, string userName) + public async Task Handle(LoadGameRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var gameTask = gameboardRepository.GetGame(request.GameName); - var moveTask = gameboardRepository.GetMoves(request.GameName); + var readSession = gameboardRepository.ReadSession(request.GameName); + var readStates = gameboardRepository.ReadBoardStates(request.GameName); - var sessionModel = await gameTask; + var sessionModel = await readSession; if (sessionModel == null) { logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); - var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; - await communicationManager.BroadcastToPlayers(response, userName); + var error = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; + await communicationManager.BroadcastToPlayers(error, userName); + return; } - else + + communicationManager.SubscribeToGame(sessionModel, userName); + var boardStates = await readStates; + var moveModels = boardStates + .Where(_ => _.Move != null) + .Select(_ => _.Move!.ToRulesModel()) + .ToList(); + var shogiBoard = new ShogiBoard(moveModels); + boardManager.Add(sessionModel.Name, shogiBoard); + + var response = new LoadGameResponse(ClientAction.LoadGame) { - var moveModels = await moveTask; - - communicationManager.SubscribeToGame(sessionModel, userName); - var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - var shogiBoard = new ShogiBoard(boardMoves); - boardManager.Add(sessionModel.Name, shogiBoard); - - var response = new LoadGameResponse(ClientAction.LoadGame) - { - Game = sessionModel.ToServiceModel(), - BoardState = new Models.BoardState(shogiBoard).ToServiceModel() - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + Game = sessionModel.ToServiceModel(), + BoardState = new Models.BoardState(shogiBoard).ToServiceModel() + }; + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index 3fa5bb5..74141d0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,14 +1,17 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Newtonsoft.Json; using System.Threading.Tasks; -using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { - public class MoveHandler : IActionHandler + public interface IMoveHandler + { + Task Handle(MoveRequest request, string userName); + } + public class MoveHandler : IMoveHandler { private readonly IBoardManager boardManager; private readonly IGameboardRepository gameboardRepository; @@ -23,43 +26,43 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(string json, string userName) + public async Task Handle(MoveRequest request, string userName) { - var request = JsonConvert.DeserializeObject(json); - var moveModel = new Move(request.Move); - var board = boardManager.Get(request.GameName); - if (board == null) - { - // TODO: Find a flow for this - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." - }; - await communicationManager.BroadcastToPlayers(response, userName); + //var request = JsonConvert.DeserializeObject(json); + //var moveModel = new Move(request.Move); + //var board = boardManager.Get(request.GameName); + //if (board == null) + //{ + // // TODO: Find a flow for this + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); - } - var boardMove = moveModel.ToBoardModel(); - var moveSuccess = board.Move(boardMove); - if (moveSuccess) - { - await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); - var boardState = new BoardState(board); - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - GameName = request.GameName, - PlayerName = userName, - BoardState = boardState.ToServiceModel() - }; - await communicationManager.BroadcastToGame(request.GameName, response); - } - else - { - var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) - { - Error = "Invalid move." - }; - await communicationManager.BroadcastToPlayers(response, userName); - } + //} + //var boardMove = moveModel.ToBoardModel(); + //var moveSuccess = board.Move(boardMove); + //if (moveSuccess) + //{ + // await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); + // var boardState = new BoardState(board); + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // GameName = request.GameName, + // PlayerName = userName, + // BoardState = boardState.ToServiceModel() + // }; + // await communicationManager.BroadcastToGame(request.GameName, response); + //} + //else + //{ + // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) + // { + // Error = "Invalid move." + // }; + // await communicationManager.BroadcastToPlayers(response, userName); + //} } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 33ec4d2..c3dfc6a 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -3,6 +3,7 @@ using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Managers.Utility; using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -16,10 +17,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers { public interface ISocketCommunicationManager { - Task CommunicateWith(WebSocket w, string s); Task BroadcastToAll(IResponse response); - Task BroadcastToGame(string gameName, IResponse response); - Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); + //Task BroadcastToGame(string gameName, IResponse response); + //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); void SubscribeToGame(Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); @@ -34,54 +34,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// Dictionary key is game name. private readonly ConcurrentDictionary sessions; private readonly ILogger logger; - private readonly ActionHandlerResolver handlerResolver; - public SocketCommunicationManager( - ILogger logger, - ActionHandlerResolver handlerResolver) + public SocketCommunicationManager(ILogger logger) { this.logger = logger; - this.handlerResolver = handlerResolver; connections = new ConcurrentDictionary(); sessions = new ConcurrentDictionary(); } - public async Task CommunicateWith(WebSocket socket, string userName) - { - SubscribeToBroadcast(socket, userName); - - while (!socket.CloseStatus.HasValue) - { - try - { - var message = await socket.ReceiveTextAsync(); - if (string.IsNullOrWhiteSpace(message)) continue; - logger.LogInformation("Request \n{0}\n", message); - var request = JsonConvert.DeserializeObject(message); - if (!Enum.IsDefined(typeof(ClientAction), request.Action)) - { - await socket.SendTextAsync("Error: Action not recognized."); - } - else - { - var handler = handlerResolver(request.Action); - await handler.Handle(message, userName); - } - } - catch (OperationCanceledException ex) - { - logger.LogError(ex.Message); - } - catch (WebSocketException ex) - { - logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); - logger.LogInformation("Probably tried writing to a closed socket."); - logger.LogError(ex.Message); - } - } - UnsubscribeFromBroadcastAndGames(userName); - } - public void SubscribeToBroadcast(WebSocket socket, string playerName) { connections.TryAdd(playerName, socket); @@ -154,27 +114,27 @@ namespace Gameboard.ShogiUI.Sockets.Managers return Task.WhenAll(tasks); } - public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - { - if (sessions.TryGetValue(gameName, out var session)) - { - var serialized1 = JsonConvert.SerializeObject(forPlayer1); - var serialized2 = JsonConvert.SerializeObject(forPlayer2); - return Task.WhenAll( - session.SendToPlayer1(serialized1), - session.SendToPlayer2(serialized2)); - } - return Task.CompletedTask; - } + //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized1 = JsonConvert.SerializeObject(forPlayer1); + // var serialized2 = JsonConvert.SerializeObject(forPlayer2); + // return Task.WhenAll( + // session.SendToPlayer1(serialized1), + // session.SendToPlayer2(serialized2)); + // } + // return Task.CompletedTask; + //} - public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - { - if (sessions.TryGetValue(gameName, out var session)) - { - var serialized = JsonConvert.SerializeObject(messageForAllPlayers); - return session.Broadcast(serialized); - } - return Task.CompletedTask; - } + //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) + //{ + // if (sessions.TryGetValue(gameName, out var session)) + // { + // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); + // return session.Broadcast(serialized); + // } + // return Task.CompletedTask; + //} } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 05374e3..5ae3ff0 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,6 +1,14 @@ -using Microsoft.AspNetCore.Http; +using Gameboard.ShogiUI.Sockets.Extensions; +using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; +using Gameboard.ShogiUI.Sockets.Managers.Utility; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using System; using System.Net; +using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers @@ -12,14 +20,36 @@ namespace Gameboard.ShogiUI.Sockets.Managers public class SocketConnectionManager : ISocketConnectionManager { + private readonly ILogger logger; private readonly ISocketCommunicationManager communicationManager; private readonly ISocketTokenManager tokenManager; + private readonly ICreateGameHandler createGameHandler; + private readonly IJoinByCodeHandler joinByCodeHandler; + private readonly IJoinGameHandler joinGameHandler; + private readonly IListGamesHandler listGamesHandler; + private readonly ILoadGameHandler loadGameHandler; + private readonly IMoveHandler moveHandler; - public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base() + public SocketConnectionManager( + ILogger logger, + ISocketCommunicationManager communicationManager, + ISocketTokenManager tokenManager, + ICreateGameHandler createGameHandler, + IJoinByCodeHandler joinByCodeHandler, + IJoinGameHandler joinGameHandler, + IListGamesHandler listGamesHandler, + ILoadGameHandler loadGameHandler, + IMoveHandler moveHandler) : base() { + this.logger = logger; this.communicationManager = communicationManager; this.tokenManager = tokenManager; - + this.createGameHandler = createGameHandler; + this.joinByCodeHandler = joinByCodeHandler; + this.joinGameHandler = joinGameHandler; + this.listGamesHandler = listGamesHandler; + this.loadGameHandler = loadGameHandler; + this.moveHandler = moveHandler; } public async Task HandleSocketRequest(HttpContext context) @@ -33,7 +63,74 @@ namespace Gameboard.ShogiUI.Sockets.Managers if (userName != null) { var socket = await context.WebSockets.AcceptWebSocketAsync(); - await communicationManager.CommunicateWith(socket, userName); + + communicationManager.SubscribeToBroadcast(socket, userName); + while (!socket.CloseStatus.HasValue) + { + try + { + var message = await socket.ReceiveTextAsync(); + if (string.IsNullOrWhiteSpace(message)) continue; + logger.LogInformation("Request \n{0}\n", message); + var request = JsonConvert.DeserializeObject(message); + if (!Enum.IsDefined(typeof(ClientAction), request.Action)) + { + await socket.SendTextAsync("Error: Action not recognized."); + continue; + } + switch (request.Action) + { + case ClientAction.ListGames: + { + var req = JsonConvert.DeserializeObject(message); + await listGamesHandler.Handle(req, userName); + break; + } + case ClientAction.CreateGame: + { + var req = JsonConvert.DeserializeObject(message); + await createGameHandler.Handle(req, userName); + break; + } + case ClientAction.JoinGame: + { + var req = JsonConvert.DeserializeObject(message); + await joinGameHandler.Handle(req, userName); + break; + } + case ClientAction.JoinByCode: + { + var req = JsonConvert.DeserializeObject(message); + await joinByCodeHandler.Handle(req, userName); + break; + } + case ClientAction.LoadGame: + { + var req = JsonConvert.DeserializeObject(message); + await loadGameHandler.Handle(req, userName); + break; + } + case ClientAction.Move: + { + var req = JsonConvert.DeserializeObject(message); + await moveHandler.Handle(req, userName); + break; + } + } + } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + catch (WebSocketException ex) + { + logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); + logger.LogInformation("Probably tried writing to a closed socket."); + logger.LogError(ex.Message); + } + } + communicationManager.UnsubscribeFromBroadcastAndGames(userName); + return; } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs index 971e438..b2e2f73 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -16,27 +17,24 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// /// Key is userName /// - private readonly Dictionary Tokens; + private readonly ConcurrentDictionary Tokens; public SocketTokenManager() { - Tokens = new Dictionary(); + Tokens = new ConcurrentDictionary(); } public Guid GenerateToken(string userName) { - var guid = Guid.NewGuid(); + Tokens.Remove(userName, out _); - if (Tokens.ContainsKey(userName)) - { - Tokens.Remove(userName); - } - Tokens.Add(userName, guid); + var guid = Guid.NewGuid(); + Tokens.TryAdd(userName, guid); _ = Task.Run(async () => { await Task.Delay(TimeSpan.FromMinutes(1)); - Tokens.Remove(userName); + Tokens.Remove(userName, out _); }); return guid; @@ -45,13 +43,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// User name associated to the guid or null. public string GetUsername(Guid guid) { - if (Tokens.ContainsValue(guid)) + var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key; + if (userName != null) { - var username = Tokens.First(kvp => kvp.Value == guid).Key; - Tokens.Remove(username); - return username; + Tokens.Remove(userName, out _); } - return null; + return userName; } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs index df3f245..9d8903f 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs @@ -6,6 +6,5 @@ namespace Gameboard.ShogiUI.Sockets.Managers.Utility public class Request : IRequest { public ClientAction Action { get; set; } - public string PlayerName { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs index 1d50469..df3d1f0 100644 --- a/Gameboard.ShogiUI.Sockets/Models/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets/Models/BoardState.cs @@ -1,5 +1,6 @@ using Gameboard.ShogiUI.Rules; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; @@ -7,28 +8,53 @@ namespace Gameboard.ShogiUI.Sockets.Models { public class BoardState { - public Piece[,] Board { get; set; } - public IReadOnlyCollection Player1Hand { get; set; } - public IReadOnlyCollection Player2Hand { get; set; } + // TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity. + public Piece?[,] Board { get; } + public IReadOnlyCollection Player1Hand { get; } + public IReadOnlyCollection Player2Hand { get; } + /// + /// Move is null in the first BoardState of a Session, before any moves have been made. + /// + public Move? Move { get; } + + public BoardState() : this(new ShogiBoard()) { } + + public BoardState(Piece?[,] board, IList player1Hand, ICollection player2Hand, Move move) + { + Board = board; + Player1Hand = new ReadOnlyCollection(player1Hand); + } public BoardState(ShogiBoard shogi) { Board = new Piece[9, 9]; for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - Board[x, y] = new Piece(shogi.Board[x, y]); + { + var piece = shogi.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList(); Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); + Move = new Move(shogi.MoveHistory[^1]); } public ServiceTypes.BoardState ToServiceModel() { var board = new ServiceTypes.Piece[9, 9]; - Board = new Piece[9, 9]; for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) - board[x, y] = Board[x, y].ToServiceModel(); + { + var piece = Board[x, y]; + if (piece != null) + { + board[x, y] = piece.ToServiceModel(); + } + } return new ServiceTypes.BoardState { Board = board, diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index a597493..3617d88 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,88 +1,41 @@ -using Gameboard.ShogiUI.Rules; -using Microsoft.FSharp.Core; -using System; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Numerics; -using BoardStateMove = Gameboard.ShogiUI.Rules.Move; -using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; namespace Gameboard.ShogiUI.Sockets.Models { public class Move { - public string PieceFromCaptured { get; set; } - public Coords From { get; set; } - public Coords To { get; set; } + public Coords? From { get; set; } public bool IsPromotion { get; set; } + public WhichPiece? PieceFromHand { get; set; } + public Coords To { get; set; } - public Move(ServiceModels.Socket.Types.Move move) + public Move(Coords from, Coords to, bool isPromotion) { - From = Coords.FromBoardNotation(move.From); - To = Coords.FromBoardNotation(move.To); - PieceFromCaptured = move.PieceFromCaptured; - IsPromotion = move.IsPromotion; + From = from; + To = to; + IsPromotion = isPromotion; } - public Move(ShogiApi.Move move) + + public Move(WhichPiece pieceFromHand, Coords to) { - string pieceFromCaptured = null; - if (move.PieceFromCaptured != null) - { - pieceFromCaptured = move.PieceFromCaptured.Value switch - { - ShogiApi.WhichPieceName.Bishop => "", - ShogiApi.WhichPieceName.GoldenGeneral => "G", - ShogiApi.WhichPieceName.King => "K", - ShogiApi.WhichPieceName.Knight => "k", - ShogiApi.WhichPieceName.Lance => "L", - ShogiApi.WhichPieceName.Pawn => "P", - ShogiApi.WhichPieceName.Rook => "R", - ShogiApi.WhichPieceName.SilverGeneral => "S", - _ => "" - }; - } - From = new Coords(move.Origin.X, move.Origin.Y); - To = new Coords(move.Destination.X, move.Destination.Y); - IsPromotion = move.IsPromotion; - PieceFromCaptured = pieceFromCaptured; + PieceFromHand = pieceFromHand; + To = to; } + public ServiceModels.Socket.Types.Move ToServiceModel() => new() { - From = From.ToBoardNotation(), + From = From?.ToBoardNotation(), IsPromotion = IsPromotion, - PieceFromCaptured = PieceFromCaptured, - To = To.ToBoardNotation() + To = To.ToBoardNotation(), + PieceFromCaptured = PieceFromHand }; - public ShogiApi.Move ToApiModel() + + public Rules.Move ToRulesModel() { - var pieceFromCaptured = PieceFromCaptured switch - { - "B" => new FSharpOption(ShogiApi.WhichPieceName.Bishop), - "G" => new FSharpOption(ShogiApi.WhichPieceName.GoldenGeneral), - "K" => new FSharpOption(ShogiApi.WhichPieceName.King), - "k" => new FSharpOption(ShogiApi.WhichPieceName.Knight), - "L" => new FSharpOption(ShogiApi.WhichPieceName.Lance), - "P" => new FSharpOption(ShogiApi.WhichPieceName.Pawn), - "R" => new FSharpOption(ShogiApi.WhichPieceName.Rook), - "S" => new FSharpOption(ShogiApi.WhichPieceName.SilverGeneral), - _ => null - }; - var target = new ShogiApi.Move - { - Origin = new ShogiApi.BoardLocation { X = From.X, Y = From.Y }, - Destination = new ShogiApi.BoardLocation { X = To.X, Y = To.Y }, - IsPromotion = IsPromotion, - PieceFromCaptured = pieceFromCaptured - }; - return target; - } - public BoardStateMove ToBoardModel() - { - return new BoardStateMove - { - From = new Vector2(From.X, From.Y), - IsPromotion = IsPromotion, - PieceFromCaptured = Enum.TryParse(PieceFromCaptured, out var whichPiece) ? whichPiece : null, - To = new Vector2(To.X, To.Y) - }; + return PieceFromHand != null + ? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y)) + : new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion); } } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs index 8ce5124..d8bc5b9 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs @@ -1,18 +1,25 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; -using BoardStatePiece = Gameboard.ShogiUI.Rules.Pieces.Piece; namespace Gameboard.ShogiUI.Sockets.Models { public class Piece { - public WhichPiece WhichPiece { get; set; } + public bool IsPromoted { get; } + public WhichPlayer Owner { get; } + public WhichPiece WhichPiece { get; } - public bool IsPromoted { get; set; } - - public Piece(BoardStatePiece piece) + public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece) + { + IsPromoted = isPromoted; + Owner = owner; + WhichPiece = whichPiece; + } + + public Piece(Rules.Pieces.Piece piece) { - WhichPiece = (WhichPiece)piece.WhichPiece; IsPromoted = piece.IsPromoted; + Owner = (WhichPlayer)piece.Owner; + WhichPiece = (WhichPiece)piece.WhichPiece; } public ServiceModels.Socket.Types.Piece ToServiceModel() @@ -20,6 +27,7 @@ namespace Gameboard.ShogiUI.Sockets.Models return new ServiceModels.Socket.Types.Piece { IsPromoted = IsPromoted, + Owner = Owner, WhichPiece = WhichPiece }; } diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs index 1e824f7..bf2af49 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs @@ -1,5 +1,6 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; +using Newtonsoft.Json; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.WebSockets; @@ -9,18 +10,20 @@ namespace Gameboard.ShogiUI.Sockets.Models { public class Session { + [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } public string Name { get; } public string Player1 { get; } - public string Player2 { get; } + public string? Player2 { get; } + public bool IsPrivate { get; } - public ConcurrentDictionary Subscriptions { get; } - - public Session(Shogi.Api.ServiceModels.Types.Session session) + public Session(string name, bool isPrivate, string player1, string? player2 = null) { - Name = session.Name; - Player1 = session.Player1; - Player2 = session.Player2; Subscriptions = new ConcurrentDictionary(); + + Name = name; + Player1 = player1; + Player2 = player2; + IsPrivate = isPrivate; } public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); @@ -47,7 +50,7 @@ namespace Gameboard.ShogiUI.Sockets.Models public Task SendToPlayer2(string message) { - if (Subscriptions.TryGetValue(Player2, out var socket)) + if (Player2 != null && Subscriptions.TryGetValue(Player2, out var socket)) { return socket.SendTextAsync(message); } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs new file mode 100644 index 0000000..2658248 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardState.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class BoardState : CouchDocument + { + public string Name { get; set; } + public Piece?[,] Board { get; set; } + public Piece[] Player1Hand { get; set; } + public Piece[] Player2Hand { get; set; } + /// + /// Move is null for first BoardState of a session - before anybody has made moves. + /// + public Move? Move { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public BoardState() : base() + { + Name = string.Empty; + Board = new Piece[9, 9]; + Player1Hand = Array.Empty(); + Player2Hand = Array.Empty(); + } + + public BoardState(string sessionName, Models.BoardState boardState) : base($"{sessionName}-{DateTime.Now:O}", nameof(BoardState)) + { + Name = sessionName; + Board = new Piece[9, 9]; + + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = boardState.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } + + Player1Hand = boardState.Player1Hand.Select(model => new Piece(model)).ToArray(); + Player2Hand = boardState.Player2Hand.Select(model => new Piece(model)).ToArray(); + if (boardState.Move != null) + { + Move = new Move(boardState.Move); + } + } + + public Models.BoardState ToDomainModel() + { + /* + * Board = new Piece[9, 9]; + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + { + var piece = boardState.Board[x, y]; + if (piece != null) + { + Board[x, y] = new Piece(piece); + } + } + + Player1Hand = boardState.Player1Hand.Select(_ => new Piece(_)).ToList(); + Player2Hand = boardState.Player2Hand.Select(_ => new Piece(_)).ToList(); + if (boardState.Move != null) + { + Move = new Move(boardState.Move); + } + */ + return null; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs new file mode 100644 index 0000000..3435e7b --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs @@ -0,0 +1,15 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class CouchCreateResult + { + public string Id { get; set; } + public bool Ok { get; set; } + public string Rev { get; set; } + + public CouchCreateResult() + { + Id = string.Empty; + Rev = string.Empty; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs new file mode 100644 index 0000000..bd1a793 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public abstract class CouchDocument + { + [JsonProperty("_id")] + public string Id { get; set; } + public string Type { get; set; } + public DateTimeOffset CreatedDate { get; set; } + + public CouchDocument() + { + Id = string.Empty; + Type = string.Empty; + CreatedDate = DateTimeOffset.UtcNow; + } + public CouchDocument(string id, string type) + { + Id = id; + Type = type; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs new file mode 100644 index 0000000..fd02af6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs @@ -0,0 +1,14 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + internal class CouchFindResult + { + public T[] docs; + + public CouchFindResult() + { + docs = Array.Empty(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs new file mode 100644 index 0000000..a362f7d --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -0,0 +1,45 @@ +using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Move + { + /// + /// A board coordinate, like A3 or G6. When null, look for PieceFromHand to exist. + /// + public string? From { get; set; } + + public bool IsPromotion { get; set; } + + /// + /// The piece placed from the player's hand. + /// + public WhichPiece? PieceFromHand { get; set; } + + /// + /// A board coordinate, like A3 or G6. + /// + public string To { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Move() + { + To = string.Empty; + } + + public Move(Models.Move move) + { + From = move.From?.ToBoardNotation(); + IsPromotion = move.IsPromotion; + To = move.To.ToBoardNotation(); + PieceFromHand = move.PieceFromHand; + } + + public Models.Move ToDomainModel() => PieceFromHand.HasValue + ? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To)) + : new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs new file mode 100644 index 0000000..68642f9 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -0,0 +1,27 @@ +using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Piece + { + public bool IsPromoted { get; set; } + public WhichPlayer Owner { get; set; } + public WhichPiece WhichPiece { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Piece() + { + } + + public Piece(Models.Piece piece) + { + IsPromoted = piece.IsPromoted; + Owner = piece.Owner; + WhichPiece = piece.WhichPiece; + } + + public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md new file mode 100644 index 0000000..50c4380 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Readme.md @@ -0,0 +1,4 @@ +### Couch Models + +Couch models should accept domain models during construction and offer a ToDomainModel method which constructs a domain model. +In this way, domain models have the freedom to define their valid states. diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs new file mode 100644 index 0000000..a3dd34e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Session.cs @@ -0,0 +1,30 @@ +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class Session : CouchDocument + { + public string Name { get; set; } + public string Player1 { get; set; } + public string? Player2 { get; set; } + public bool IsPrivate { get; set; } + + /// + /// Default constructor and setters are for deserialization. + /// + public Session() : base() + { + Name = string.Empty; + Player1 = string.Empty; + Player2 = string.Empty; + } + + public Session(string id, Models.Session session) : base(id, nameof(Session)) + { + Name = session.Name; + Player1 = session.Player1; + Player2 = session.Player2; + IsPrivate = session.IsPrivate; + } + + public Models.Session ToDomainModel() => new(Name, IsPrivate, Player1, Player2); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs new file mode 100644 index 0000000..22d30c7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/User.cs @@ -0,0 +1,23 @@ +using System; + +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +{ + public class User : CouchDocument + { + public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; + + public enum LoginPlatform + { + Microsoft, + Guest + } + + public string Name { get; set; } + public LoginPlatform Platform { get; set; } + public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User)) + { + Name = name; + Platform = platform; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index bd483eb..162e2df 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -1,6 +1,5 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; +using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -13,141 +12,173 @@ namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { - Task DeleteGame(string gameName); - Task GetGame(string gameName); - Task GetGames(); - Task GetGames(string playerName); - Task> GetMoves(string gameName); - Task PostSession(PostSession request); - Task PostJoinPrivateSession(PostJoinPrivateSession request); - Task PutJoinPublicSession(PutJoinPublicSession request); - Task PostMove(string gameName, PostMove request); + Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move); + Task CreateGuestUser(string userName); + Task CreateSession(Models.Session session); + Task> ReadSessions(); + Task IsGuestUser(string userName); Task PostJoinCode(string gameName, string userName); - Task GetPlayer(string userName); - Task PostPlayer(PostPlayer request); + Task ReadSession(string name); + Task> ReadBoardStates(string name); } public class GameboardRepository : IGameboardRepository { - private const string GetSessionsRoute = "Sessions"; - private const string PostSessionRoute = "Session"; - private const string JoinSessionRoute = "Session/Join"; - private const string PlayerRoute = "Player"; - private const string MediaType = "application/json"; - private readonly IAuthenticatedHttpClient client; - public GameboardRepository(IAuthenticatedHttpClient client) + private const string ApplicationJson = "application/json"; + private readonly HttpClient client; + private readonly ILogger logger; + + public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) { - this.client = client; + client = clientFactory.CreateClient("couchdb"); + this.logger = logger; } - public async Task GetGames() + public async Task> ReadSessions() { - var response = await client.GetAsync(GetSessionsRoute); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); - } + var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}"; + var query = $@"{{ ""selector"": {selector} }}"; + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync("_find", content); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); - public async Task GetGames(string playerName) - { - var uri = $"Sessions/{playerName}"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); - } - - public async Task GetGame(string gameName) - { - var uri = $"Session/{gameName}"; - var response = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await response.Content.ReadAsStringAsync(); - if (string.IsNullOrWhiteSpace(json)) + if (result == null) { - return null; + logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); + return Array.Empty(); } - return new Session(JsonConvert.DeserializeObject(json).Session); + return result.docs + .Select(_ => _.ToDomainModel()) + .ToList(); } - public async Task DeleteGame(string gameName) + public async Task ReadSession(string name) { - var uri = $"Session/{gameName}"; - await client.DeleteAsync(Uri.EscapeUriString(uri)); + var response = await client.GetAsync(name); + var responseContent = await response.Content.ReadAsStringAsync(); + var couchModel = JsonConvert.DeserializeObject(responseContent); + return couchModel.ToDomainModel(); } - public async Task PostSession(PostSession request) + public async Task> ReadBoardStates(string name) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PostAsync(PostSessionRoute, content); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json).SessionName; - } + var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}"; + var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}"; + var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}"; + var content = new StringContent(query, Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync("_find", content); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); - public async Task PutJoinPublicSession(PutJoinPublicSession request) - { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PutAsync(JoinSessionRoute, content); - var json = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json).JoinSucceeded; - } - - public async Task PostJoinPrivateSession(PostJoinPrivateSession request) - { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PostAsync(JoinSessionRoute, content); - var json = await response.Content.ReadAsStringAsync(); - var deserialized = JsonConvert.DeserializeObject(json); - if (deserialized.JoinSucceeded) + if (result == null) { - return deserialized.SessionName; + logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); + return Array.Empty(); } - return null; + return result.docs + .Select(_ => new Models.BoardState(_)) + .ToList(); } - public async Task> GetMoves(string gameName) + //public async Task DeleteGame(string gameName) + //{ + // //var uri = $"Session/{gameName}"; + // //await client.DeleteAsync(Uri.EscapeUriString(uri)); + //} + + public async Task CreateSession(Models.Session session) { - var uri = $"Session/{gameName}/Moves"; - var get = await client.GetAsync(Uri.EscapeUriString(uri)); - var json = await get.Content.ReadAsStringAsync(); - if (string.IsNullOrWhiteSpace(json)) - { - return new List(); - } - var response = JsonConvert.DeserializeObject(json); - return response.Moves.Select(m => new Move(m)).ToList(); + var couchModel = new Session(session.Name, session); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } - public async Task PostMove(string gameName, PostMove request) + public async Task CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move) { - var uri = $"Session/{gameName}/Move"; - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - await client.PostAsync(Uri.EscapeUriString(uri), content); + var couchModel = new BoardState(sessionName, boardState, move); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } + //public async Task PutJoinPublicSession(PutJoinPublicSession request) + //{ + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // var response = await client.PutAsync(JoinSessionRoute, content); + // var json = await response.Content.ReadAsStringAsync(); + // return JsonConvert.DeserializeObject(json).JoinSucceeded; + //} + + //public async Task PostJoinPrivateSession(PostJoinPrivateSession request) + //{ + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // var response = await client.PostAsync(JoinSessionRoute, content); + // var json = await response.Content.ReadAsStringAsync(); + // var deserialized = JsonConvert.DeserializeObject(json); + // if (deserialized.JoinSucceeded) + // { + // return deserialized.SessionName; + // } + // return null; + //} + + //public async Task> GetMoves(string gameName) + //{ + // var uri = $"Session/{gameName}/Moves"; + // var get = await client.GetAsync(Uri.EscapeUriString(uri)); + // var json = await get.Content.ReadAsStringAsync(); + // if (string.IsNullOrWhiteSpace(json)) + // { + // return new List(); + // } + // var response = JsonConvert.DeserializeObject(json); + // return response.Moves.Select(m => new Move(m)).ToList(); + //} + + //public async Task PostMove(string gameName, PostMove request) + //{ + // var uri = $"Session/{gameName}/Move"; + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // await client.PostAsync(Uri.EscapeUriString(uri), content); + //} + public async Task PostJoinCode(string gameName, string userName) { - var uri = $"JoinCode/{gameName}"; - var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); - var content = new StringContent(serialized, Encoding.UTF8, MediaType); - var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json).JoinCode; + // var uri = $"JoinCode/{gameName}"; + // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); + // var content = new StringContent(serialized, Encoding.UTF8, MediaType); + // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); + // return JsonConvert.DeserializeObject(json).JoinCode; + return string.Empty; } - public async Task GetPlayer(string playerName) + //public async Task GetPlayer(string playerName) + //{ + // var uri = $"Player/{playerName}"; + // var get = await client.GetAsync(Uri.EscapeUriString(uri)); + // var content = await get.Content.ReadAsStringAsync(); + // if (!string.IsNullOrWhiteSpace(content)) + // { + // var response = JsonConvert.DeserializeObject(content); + // return new Player(response.Player.Name); + // } + // return null; + //} + + public async Task CreateGuestUser(string userName) { - var uri = $"Player/{playerName}"; - var get = await client.GetAsync(Uri.EscapeUriString(uri)); - var content = await get.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) - { - var response = JsonConvert.DeserializeObject(content); - return new Player(response.Player.Name); - } - return null; + var couchModel = new User(userName, User.LoginPlatform.Guest); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; } - public async Task PostPlayer(PostPlayer request) + public async Task IsGuestUser(string userName) { - var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - var response = await client.PostAsync(PlayerRoute, content); + var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}")); + var response = await client.SendAsync(req); return response.IsSuccessStatusCode; } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs index 30fed61..d219516 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs @@ -1,4 +1,4 @@ -using Gameboard.Shogi.Api.ServiceModels.Messages; +using Gameboard.ShogiUI.Sockets.Models; using System; using System.Threading.Tasks; @@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers Task CreateGuestUser(); Task IsPlayer1(string sessionName, string playerName); bool IsGuest(string playerName); - Task PlayerExists(string playerName); + Task CreateSession(Session session); } public class GameboardRepositoryManager : IGameboardRepositoryManager @@ -30,11 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers { count++; var clientId = $"Guest-{Guid.NewGuid()}"; - var request = new PostPlayer - { - PlayerName = clientId - }; - var isCreated = await repository.PostPlayer(request); + var isCreated = await repository.CreateGuestUser(clientId); if (isCreated) { return clientId; @@ -45,22 +41,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers public async Task IsPlayer1(string sessionName, string playerName) { - var session = await repository.GetGame(sessionName); - return session?.Player1 == playerName; + //var session = await repository.GetGame(sessionName); + //return session?.Player1 == playerName; + return true; } public async Task CreateJoinCode(string sessionName, string playerName) { - var session = await repository.GetGame(sessionName); - if (playerName == session?.Player1) - { - return await repository.PostJoinCode(sessionName, playerName); - } + //var session = await repository.GetGame(sessionName); + //if (playerName == session?.Player1) + //{ + // return await repository.PostJoinCode(sessionName, playerName); + //} return null; } - public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); + public async Task CreateSession(Session session) + { + var success = await repository.CreateSession(session); + if (success) + { + return await repository.CreateBoardState(session.Name, new BoardState(), null); + } + return false; + } - public async Task PlayerExists(string playerName) => await repository.GetPlayer(playerName) != null; + public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs deleted file mode 100644 index 3b4067d..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs +++ /dev/null @@ -1,122 +0,0 @@ -using IdentityModel.Client; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Repositories.Utility -{ - public interface IAuthenticatedHttpClient - { - Task DeleteAsync(string requestUri); - Task GetAsync(string requestUri); - Task PostAsync(string requestUri, HttpContent content); - Task PutAsync(string requestUri, HttpContent content); - } - - public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient - { - private readonly ILogger logger; - private readonly string identityServerUrl; - private TokenResponse tokenResponse; - private readonly string clientId; - private readonly string clientSecret; - - public AuthenticatedHttpClient(ILogger logger, IConfiguration configuration) : base() - { - this.logger = logger; - identityServerUrl = configuration["AppSettings:IdentityServer"]; - clientId = configuration["AppSettings:ClientId"]; - clientSecret = configuration["AppSettings:ClientSecret"]; - BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]); - } - - private async Task RefreshBearerToken() - { - var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl); - if (disco.IsError) - { - logger.LogError("{DiscoveryErrorType}", disco.ErrorType); - throw new Exception(disco.Error); - } - - var request = new ClientCredentialsTokenRequest - { - Address = disco.TokenEndpoint, - ClientId = clientId, - ClientSecret = clientSecret - }; - var response = await this.RequestClientCredentialsTokenAsync(request); - if (response.IsError) - { - throw new Exception(response.Error); - } - tokenResponse = response; - logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress); - this.SetBearerToken(tokenResponse.AccessToken); - } - - public new async Task GetAsync(string requestUri) - { - var response = await base.GetAsync(requestUri); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.GetAsync(requestUri); - } - logger.LogInformation( - "Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n", - BaseAddress, - requestUri, - await response.Content.ReadAsStringAsync()); - return response; - } - public new async Task PostAsync(string requestUri, HttpContent content) - { - var response = await base.PostAsync(requestUri, content); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.PostAsync(requestUri, content); - } - logger.LogInformation( - "Repository POST to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n", - BaseAddress, - requestUri, - response.StatusCode, - await content.ReadAsStringAsync(), - await response.Content.ReadAsStringAsync()); - return response; - } - public new async Task PutAsync(string requestUri, HttpContent content) - { - var response = await base.PutAsync(requestUri, content); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.PutAsync(requestUri, content); - } - logger.LogInformation( - "Repository PUT to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n", - BaseAddress, - requestUri, - response.StatusCode, - await content.ReadAsStringAsync(), - await response.Content.ReadAsStringAsync()); - return response; - } - public new async Task DeleteAsync(string requestUri) - { - var response = await base.DeleteAsync(requestUri); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - await RefreshBearerToken(); - response = await base.DeleteAsync(requestUri); - } - logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri); - return response; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 8431c68..cda318d 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -3,8 +3,6 @@ using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; -using Gameboard.ShogiUI.Sockets.Repositories.Utility; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -15,8 +13,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System; -using System.Collections.Generic; using System.Linq; +using System.Text; namespace Gameboard.ShogiUI.Sockets { @@ -33,36 +31,33 @@ namespace Gameboard.ShogiUI.Sockets public void ConfigureServices(IServiceCollection services) { // Socket ActionHandlers - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Managers services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddScoped(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => action => - { - return action switch - { - ClientAction.ListGames => sp.GetService(), - ClientAction.CreateGame => sp.GetService(), - ClientAction.JoinGame => sp.GetService(), - ClientAction.JoinByCode => sp.GetService(), - ClientAction.LoadGame => sp.GetService(), - ClientAction.Move => sp.GetService(), - _ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"), - }; - }); // Repositories + services.AddHttpClient("couchdb", c => + { + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); + + var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; + c.BaseAddress = new Uri(baseUrl); + }); services.AddTransient(); - services.AddSingleton(); + //services.AddSingleton(); + //services.AddSingleton(provider => new CouchClient(databaseName, couchUrl)); services.AddControllers(); diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 1b50ee6..9d9f40f 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -1,10 +1,9 @@ { "AppSettings": { - "IdentityServer": "https://identity.lucaserver.space/", - "GameboardShogiApi": "https://dev.lucaserver.space/Gameboard.Shogi.Api/", - "ClientId": "DevClientId", - "ClientSecret": "DevSecret", - "Scope": "DevEnvironment" + "CouchDB": { + "Database": "shogi-dev", + "Url": "http://192.168.1.15:5984" + } }, "Logging": { "LogLevel": { diff --git a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs index 7145e5e..8cf4734 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/BoardStateExtensions.cs @@ -13,7 +13,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules var name = self.WhichPiece switch { WhichPiece.King => " K ", - WhichPiece.GoldenGeneral => " G ", + WhichPiece.GoldGeneral => " G ", WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ", WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ", WhichPiece.Rook => self.IsPromoted ? "^R " : " R ", diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index fbe26b3..c558571 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -26,9 +26,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[4, 0].WhichPiece.Should().Be(WhichPiece.King); - board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); @@ -51,9 +51,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[4, 8].WhichPiece.Should().Be(WhichPiece.King); - board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); @@ -256,23 +256,23 @@ namespace Gameboard.ShogiUI.UnitTests.Rules // Act | Assert - It is P1 turn /// try illegally placing Knight from the hand. shogi.Board[7, 0].Should().BeNull(); - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 0) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 0].Should().BeNull(); - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 1) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 1) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 1].Should().BeNull(); /// try illegally placing Pawn from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); shogi.Board[7, 0].Should().BeNull(); /// try illegally placing Lance from the hand - dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 0) }); + dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(7, 0) }); dropSuccess.Should().BeFalse(); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Board[7, 0].Should().BeNull(); @@ -312,7 +312,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act - P1 tries to place a Lance while in check. - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(4, 4) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) }); // Assert dropSuccess.Should().BeFalse(); @@ -347,7 +347,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules shogi.Board[4, 0].Should().NotBeNull(); // Act - P1 tries to place Bishop from hand to an already-occupied position - var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) }); + var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) }); // Assert dropSuccess.Should().BeFalse(); diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index d276b12..48b20e7 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -4,7 +4,7 @@ namespace PathFinding { public interface IPlanarCollection : IEnumerable where T : IPlanarElement { - T this[float x, float y] { get; set; } + T? this[float x, float y] { get; set; } int GetLength(int dimension); } } diff --git a/PathFinding/PathFinding.csproj b/PathFinding/PathFinding.csproj index ee29921..4466c3d 100644 --- a/PathFinding/PathFinding.csproj +++ b/PathFinding/PathFinding.csproj @@ -4,6 +4,7 @@ net5.0 true 5 + enable