diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs deleted file mode 100644 index 8fdbfe4..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class GetSessionResponse - { - public Game Game { get; set; } - /// - /// The perspective on the game of the requesting user. - /// - public WhichPerspective PlayerPerspective { get; set; } - public BoardState BoardState { get; set; } - public IList MoveHistory { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs index cc972bc..5fa50d1 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs @@ -5,7 +5,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api { public class GetSessionsResponse { - public Collection PlayerHasJoinedSessions { get; set; } - public Collection AllOtherSessions { get; set; } + public Collection PlayerHasJoinedSessions { get; set; } + public Collection AllOtherSessions { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs index e1b59bb..71fdeca 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs @@ -6,7 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket public class CreateGameResponse : IResponse { public string Action { get; } - public Game Game { get; set; } + public Session Game { get; set; } /// /// The player who created the game. diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs index b6d0a98..398ba4a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs @@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types public Dictionary Board { get; set; } = new Dictionary(); public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); - public WhichPerspective? PlayerInCheck { get; set; } - public WhichPerspective WhoseTurn { get; set; } + public WhichPlayer? PlayerInCheck { get; set; } + public WhichPlayer WhoseTurn { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs deleted file mode 100644 index 5b70ad8..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types -{ - public class Game - { - public string Player1 { get; set; } - public string? Player2 { get; set; } - public string GameName { get; set; } = string.Empty; - - /// - /// Players[0] is the session owner, Players[1] is the other person. - /// - public IReadOnlyList Players - { - get - { - var list = new List(2) { Player1 }; - if (!string.IsNullOrEmpty(Player2)) list.Add(Player2); - return list; - } - } - - /// - /// Constructor for serialization. - /// - public Game() - { - } - - public Game(string gameName, string player1, string? player2 = null) - { - GameName = gameName; - Player1 = player1; - Player2 = player2; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs index 8e28d04..1c0ee78 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs @@ -4,6 +4,6 @@ { public bool IsPromoted { get; set; } public WhichPiece WhichPiece { get; set; } - public WhichPerspective Owner { get; set; } + public WhichPlayer Owner { get; set; } } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs new file mode 100644 index 0000000..a2426c8 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs @@ -0,0 +1,10 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +{ + public class Session + { + public string Player1 { get; set; } + public string? Player2 { get; set; } + public string GameName { get; set; } + public BoardState BoardState { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs index cf8a4c4..3fb839b 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs @@ -1,9 +1,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types { - public enum WhichPerspective + public enum WhichPlayer { Player1, - Player2, - Spectator + Player2 } } diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 3b54d4e..82bd209 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -13,13 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,10 +41,6 @@ Global {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU - {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index 0f76e3b..15fe9cf 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -1,5 +1,4 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers; +using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Api; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; @@ -7,10 +6,8 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Controllers @@ -169,25 +166,48 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return NotFound(); } - var playerPerspective = WhichPerspective.Spectator; - if (session.Player1.Id == user.Id) - { - playerPerspective = WhichPerspective.Player1; - } - else if (session.Player2?.Id == user.Id) - { - playerPerspective = WhichPerspective.Player2; - } + var playerPerspective = session.Player2Name == user.Id + ? WhichPlayer.Player2 + : WhichPlayer.Player1; - communicationManager.SubscribeToGame(session, user!.Id); - var response = new GetSessionResponse() + var response = new Session { - Game = new Models.SessionMetadata(session).ToServiceModel(), - BoardState = session.Shogi.ToServiceModel(), - MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(), - PlayerPerspective = playerPerspective + BoardState = new BoardState + { + Board = null, + Player1Hand = session.Player1Hand.Select(MapPiece).ToList(), + Player2Hand = session.Player2Hand.Select(MapPiece).ToList(), + PlayerInCheck = session.InCheck.HasValue ? Map(session.InCheck.Value) : null + }, + GameName = session.Name, + Player1 = session.Player1Name, + Player2 = session.Player2Name }; - return new JsonResult(response); + return this.Ok(response); + + static WhichPlayer Map(Shogi.Domain.WhichPlayer whichPlayer) + { + return whichPlayer == Shogi.Domain.WhichPlayer.Player1 + ? WhichPlayer.Player1 + : WhichPlayer.Player2; + } + static Piece MapPiece(Shogi.Domain.Pieces.Piece piece) + { + var owner = Map(piece.Owner); + var whichPiece = piece.WhichPiece switch + { + Shogi.Domain.WhichPiece.King => WhichPiece.King, + Shogi.Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + Shogi.Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + Shogi.Domain.WhichPiece.Bishop => WhichPiece.Bishop, + Shogi.Domain.WhichPiece.Rook => WhichPiece.Rook, + Shogi.Domain.WhichPiece.Knight => WhichPiece.Knight, + Shogi.Domain.WhichPiece.Lance => WhichPiece.Lance, + Shogi.Domain.WhichPiece.Pawn => WhichPiece.Pawn, + _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}") + }; + return new Piece { IsPromoted = piece.IsPromoted, Owner = owner, WhichPiece = whichPiece }; + } } [HttpGet] @@ -207,8 +227,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return new GetSessionsResponse { - PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser), - AllOtherSessions = new Collection(sessionsNotJoinedByUser) + PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser), + AllOtherSessions = new Collection(sessionsNotJoinedByUser) }; } diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs index 6f442bd..4824284 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -1,6 +1,5 @@ using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; -using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Api; using Microsoft.AspNetCore.Authentication; @@ -56,7 +55,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers var userId = User?.UserId(); if (!string.IsNullOrEmpty(userId)) { - connectionManager.UnsubscribeFromBroadcastAndGames(userId); + connectionManager.Unsubscribe(userId); } await signoutTask; diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs deleted file mode 100644 index f9ce7f9..0000000 --- a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Text; -using System.Text.RegularExpressions; - -namespace Gameboard.ShogiUI.Sockets.Extensions -{ - public static class ModelExtensions - { - public static string GetShortName(this Models.Piece self) - { - var name = self.WhichPiece switch - { - WhichPiece.King => " K ", - WhichPiece.GoldGeneral => " G ", - WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ", - WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ", - WhichPiece.Rook => self.IsPromoted ? "^R " : " R ", - WhichPiece.Knight => self.IsPromoted ? "^k " : " k ", - WhichPiece.Lance => self.IsPromoted ? "^L " : " L ", - WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ", - _ => " ? ", - }; - if (self.Owner == WhichPerspective.Player2) - name = Regex.Replace(name, @"([^\s]+)\s", "$1."); - return name; - } - - public static string PrintStateAsAscii(this Models.Shogi self) - { - var builder = new StringBuilder(); - builder.Append(" Player 2(.)"); - builder.AppendLine(); - for (var y = 8; y >= 0; y--) - { - builder.Append("- "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append('|'); - for (var x = 0; x < 9; x++) - { - var piece = self.Board[x, y]; - if (piece == null) - { - builder.Append(" "); - } - else - { - builder.AppendFormat("{0}", piece.GetShortName()); - } - builder.Append('|'); - } - builder.AppendLine(); - } - builder.Append("- "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append(" Player 1"); - return builder.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 4ba5c59..e9540eb 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -21,9 +21,12 @@ - + + + + diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs index 0b2a5b8..ecace67 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -1,5 +1,4 @@ using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -14,12 +13,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers public interface ISocketConnectionManager { Task BroadcastToAll(IResponse response); - //Task BroadcastToGame(string gameName, IResponse response); - //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); - void SubscribeToGame(Session session, string playerName); - void SubscribeToBroadcast(WebSocket socket, string playerName); - void UnsubscribeFromBroadcastAndGames(string playerName); - void UnsubscribeFromGame(string gameName, string playerName); + void Subscribe(WebSocket socket, string playerName); + void Unsubscribe(string playerName); Task BroadcastToPlayers(IResponse response, params string?[] playerNames); } @@ -31,59 +26,23 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// Dictionary key is player name. private readonly ConcurrentDictionary connections; /// Dictionary key is game name. - private readonly ConcurrentDictionary sessions; private readonly ILogger logger; public SocketConnectionManager(ILogger logger) { this.logger = logger; connections = new ConcurrentDictionary(); - sessions = new ConcurrentDictionary(); } - public void SubscribeToBroadcast(WebSocket socket, string playerName) + public void Subscribe(WebSocket socket, string playerName) { connections.TryRemove(playerName, out var _); connections.TryAdd(playerName, socket); } - public void UnsubscribeFromBroadcastAndGames(string playerName) + public void Unsubscribe(string playerName) { connections.TryRemove(playerName, out _); - foreach (var kvp in sessions) - { - var sessionName = kvp.Key; - UnsubscribeFromGame(sessionName, playerName); - } - } - - /// - /// Unsubscribes the player from their current game, then subscribes to the new game. - /// - public void SubscribeToGame(Session session, string playerName) - { - // Unsubscribe from any other games - foreach (var kvp in sessions) - { - var gameNameKey = kvp.Key; - UnsubscribeFromGame(gameNameKey, playerName); - } - - // Subscribe - if (connections.TryGetValue(playerName, out var socket)) - { - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); - } - } - - public void UnsubscribeFromGame(string gameName, string playerName) - { - if (sessions.TryGetValue(gameName, out var s)) - { - s.Subscriptions.TryRemove(playerName, out _); - if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); - } } public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames) @@ -113,15 +72,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers tasks.Add(socket.SendTextAsync(message)); } - catch (WebSocketException webSocketException) + catch (WebSocketException) { logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - UnsubscribeFromBroadcastAndGames(kvp.Key); + Unsubscribe(kvp.Key); } - catch (Exception exception) + catch { logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - UnsubscribeFromBroadcastAndGames(kvp.Key); + Unsubscribe(kvp.Key); } } try @@ -129,34 +88,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers var task = Task.WhenAll(tasks); return task; } - catch (Exception e) + catch { Console.WriteLine("Yo"); } return Task.FromResult(0); } - - //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized1 = JsonConvert.SerializeObject(forPlayer1); - // var serialized2 = JsonConvert.SerializeObject(forPlayer2); - // return Task.WhenAll( - // session.SendToPlayer1(serialized1), - // session.SendToPlayer2(serialized2)); - // } - // return Task.CompletedTask; - //} - - //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) - //{ - // if (sessions.TryGetValue(gameName, out var session)) - // { - // var serialized = JsonConvert.SerializeObject(messageForAllPlayers); - // return session.Broadcast(serialized); - // } - // return Task.CompletedTask; - //} } } diff --git a/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs deleted file mode 100644 index 5f4ee8e..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs +++ /dev/null @@ -1,95 +0,0 @@ -using PathFinding; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public static class MoveSets - { - public static readonly List King = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - - public static readonly List Bishop = new(4) - { - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - - public static readonly List PromotedBishop = new(8) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down), - new PathFinding.Move(Direction.UpLeft, Distance.MultiStep), - new PathFinding.Move(Direction.UpRight, Distance.MultiStep), - new PathFinding.Move(Direction.DownLeft, Distance.MultiStep), - new PathFinding.Move(Direction.DownRight, Distance.MultiStep) - }; - - public static readonly List GoldGeneral = new(6) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.Left), - new PathFinding.Move(Direction.Right), - new PathFinding.Move(Direction.Down) - }; - - public static readonly List Knight = new(2) - { - new PathFinding.Move(Direction.KnightLeft), - new PathFinding.Move(Direction.KnightRight) - }; - - public static readonly List Lance = new(1) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - }; - - public static readonly List Pawn = new(1) - { - new PathFinding.Move(Direction.Up) - }; - - public static readonly List Rook = new(4) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep) - }; - - public static readonly List PromotedRook = new(8) - { - new PathFinding.Move(Direction.Up, Distance.MultiStep), - new PathFinding.Move(Direction.Left, Distance.MultiStep), - new PathFinding.Move(Direction.Right, Distance.MultiStep), - new PathFinding.Move(Direction.Down, Distance.MultiStep), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - - public static readonly List SilverGeneral = new(4) - { - new PathFinding.Move(Direction.Up), - new PathFinding.Move(Direction.UpLeft), - new PathFinding.Move(Direction.UpRight), - new PathFinding.Move(Direction.DownLeft), - new PathFinding.Move(Direction.DownRight) - }; - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs deleted file mode 100644 index 7b86808..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Piece.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using PathFinding; -using System.Diagnostics; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public class Piece : IPlanarElement - { - public WhichPiece WhichPiece { get; } - public WhichPerspective Owner { get; private set; } - public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPerspective.Player2; - - public Piece(WhichPiece piece, WhichPerspective owner, bool isPromoted = false) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = isPromoted; - } - public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted) - { - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void ToggleOwnership() - { - Owner = Owner == WhichPerspective.Player1 - ? WhichPerspective.Player2 - : WhichPerspective.Player1; - } - - public void Promote() => IsPromoted = CanPromote; - - public void Demote() => IsPromoted = false; - - public void Capture() - { - ToggleOwnership(); - Demote(); - } - - // TODO: There is no reason to make "new" MoveSets every time this property is accessed. - public MoveSet MoveSet => WhichPiece switch - { - WhichPiece.King => new MoveSet(this, MoveSets.King), - WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral), - WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral), - WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop), - WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook), - WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight), - WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance), - WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn), - _ => throw new System.NotImplementedException() - }; - - public ServiceModels.Types.Piece ToServiceModel() - { - return new ServiceModels.Types.Piece - { - IsPromoted = IsPromoted, - Owner = Owner, - WhichPiece = WhichPiece - }; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs deleted file mode 100644 index fd73b40..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Session.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Newtonsoft.Json; -using System.Collections.Concurrent; -using System.Net.WebSockets; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class Session - { - // TODO: Separate subscriptions to the Session from the Session. - [JsonIgnore] public ConcurrentDictionary Subscriptions { get; } - public string Name { get; } - public User Player1 { get; } - public User? Player2 { get; private set; } - public bool IsPrivate { get; } - - // TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied. - public Shogi Shogi { get; } - - public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null) - { - Subscriptions = new ConcurrentDictionary(); - - Name = name; - Player1 = player1; - Player2 = player2; - IsPrivate = isPrivate; - Shogi = shogi; - } - - public void SetPlayer2(User user) - { - Player2 = user; - } - - public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName); - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs index 350b273..9747608 100644 --- a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs +++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs @@ -32,6 +32,6 @@ public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id; - public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName); + public ServiceModels.Types.Session ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName); } } diff --git a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs deleted file mode 100644 index ec944a2..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs +++ /dev/null @@ -1,463 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Gameboard.ShogiUI.Sockets.Utilities; -using PathFinding; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - /// - /// Facilitates Shogi board state transitions, cognisant of Shogi rules. - /// The board is always from Player1's perspective. - /// [0,0] is the lower-left position, [8,8] is the higher-right position - /// - public class Shogi - { - private delegate void MoveSetCallback(Piece piece, Vector2 position); - private readonly PathFinder2D pathFinder; - private Shogi? validationBoard; - private Vector2 player1King; - private Vector2 player2King; - private List Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand; - public List Player1Hand { get; } - public List Player2Hand { get; } - public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method - public List MoveHistory { get; } - public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2; - public WhichPerspective? InCheck { get; private set; } - public bool IsCheckmate { get; private set; } - - public string Error { get; private set; } - - public Shogi() - { - Board = new CoordsToNotationCollection(); - MoveHistory = new List(20); - Player1Hand = new List(); - Player2Hand = new List(); - pathFinder = new PathFinder2D(Board, 9, 9); - player1King = new Vector2(4, 0); - player2King = new Vector2(4, 8); - Error = string.Empty; - - InitializeBoardState(); - } - - public Shogi(IList moves) : this() - { - for (var i = 0; i < moves.Count; i++) - { - if (!Move(moves[i])) - { - // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); - } - } - } - - private Shogi(Shogi toCopy) - { - Board = new CoordsToNotationCollection(); - foreach (var kvp in toCopy.Board) - { - Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); - } - - pathFinder = new PathFinder2D(Board, 9, 9); - MoveHistory = new List(toCopy.MoveHistory); - Player1Hand = new List(toCopy.Player1Hand); - Player2Hand = new List(toCopy.Player2Hand); - player1King = toCopy.player1King; - player2King = toCopy.player2King; - Error = toCopy.Error; - } - - public bool Move(Move move) - { - var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1; - var moveSuccess = TryMove(move); - - if (!moveSuccess) - { - return false; - } - - // Evaluate check - if (EvaluateCheckAfterMove(move, otherPlayer)) - { - InCheck = otherPlayer; - IsCheckmate = EvaluateCheckmate(); - } - else - { - InCheck = null; - } - return true; - } - /// - /// Attempts a given move. Returns false if the move is illegal. - /// - private bool TryMove(Move move) - { - // Try making the move in a "throw away" board. - if (validationBoard == null) - { - validationBoard = new Shogi(this); - } - - var isValid = move.PieceFromHand.HasValue - ? validationBoard.PlaceFromHand(move) - : validationBoard.PlaceFromBoard(move); - if (!isValid) - { - // Surface the error description. - Error = validationBoard.Error; - // Invalidate the "throw away" board. - validationBoard = null; - return false; - } - // If already in check, assert the move that resulted in check no longer results in check. - if (InCheck == WhoseTurn) - { - if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) - { - // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - return false; - } - } - - // The move is valid and legal; update board state. - if (move.PieceFromHand.HasValue) PlaceFromHand(move); - else PlaceFromBoard(move); - return true; - } - /// True if the move was successful. - private bool PlaceFromHand(Move move) - { - var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); - if (index < 0) - { - Error = $"{move.PieceFromHand} does not exist in the hand."; - return false; - } - if (Board[move.To] != null) - { - Error = $"Illegal move - attempting to capture while playing a piece from the hand."; - return false; - } - - switch (move.PieceFromHand!.Value) - { - case WhichPiece.Knight: - { - // Knight cannot be placed onto the farthest two ranks from the hand. - if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6) - || (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2)) - { - Error = $"Knight has no valid moves after placed."; - return false; - } - break; - } - case WhichPiece.Lance: - case WhichPiece.Pawn: - { - // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8) - || (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0)) - { - Error = $"{move.PieceFromHand} has no valid moves after placed."; - return false; - } - break; - } - } - - // Mutate the board. - Board[move.To] = Hand[index]; - Hand.RemoveAt(index); - MoveHistory.Add(move); - - return true; - } - /// True if the move was successful. - private bool PlaceFromBoard(Move move) - { - var fromPiece = Board[move.From!.Value]; - if (fromPiece == null) - { - Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; - return false; // Invalid move - } - if (fromPiece.Owner != WhoseTurn) - { - Error = "Not allowed to move the opponents piece"; - return false; // Invalid move; cannot move other players pieces. - } - if (IsPathable(move.From.Value, move.To) == false) - { - Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; - return false; // Invalid move; move not part of move-set. - } - - var captured = Board[move.To]; - if (captured != null) - { - if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. - captured.Capture(); - Hand.Add(captured); - } - - //Mutate the board. - if (move.IsPromotion) - { - if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5)) - { - fromPiece.Promote(); - } - else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3)) - { - fromPiece.Promote(); - } - } - Board[move.To] = fromPiece; - Board[move.From!.Value] = null; - if (fromPiece.WhichPiece == WhichPiece.King) - { - if (fromPiece.Owner == WhichPerspective.Player1) - { - player1King.X = move.To.X; - player1King.Y = move.To.Y; - } - else if (fromPiece.Owner == WhichPerspective.Player2) - { - player2King.X = move.To.X; - player2King.Y = move.To.Y; - } - } - MoveHistory.Add(move); - return true; - } - - private bool IsPathable(Vector2 from, Vector2 to) - { - var piece = Board[from]; - if (piece == null) return false; - - var isObstructed = false; - var isPathable = pathFinder.PathTo(from, to, (other, position) => - { - if (other.Owner == piece.Owner) isObstructed = true; - }); - return !isObstructed && isPathable; - } - - #region Rules Validation - private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective) - { - if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. - - var isCheck = false; - var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King; - - // Check if the move put the king in check. - if (pathFinder.PathTo(move.To, kingPosition)) return true; - - if (move.From.HasValue) - { - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From!.Value); - var slope = Math.Abs(direction.Y / direction.X); - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) - { - // if slope of the move is also infinity...can skip this? - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective) - { - switch (piece.WhichPiece) - { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; - } - } - }); - } - else if (slope == 1) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) - { - isCheck = true; - } - }); - } - else if (slope == 0) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) - { - isCheck = true; - } - }); - } - } - else - { - // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. - // Go read the shogi rules to be sure this is true. - } - - return isCheck; - } - private bool EvaluateCheckmate() - { - if (!InCheck.HasValue) return false; - - // Assume true and try to disprove. - var isCheckmate = true; - Board.ForEachNotNull((piece, from) => // For each piece... - { - // Short circuit - if (!isCheckmate) return; - - if (piece.Owner == InCheck) // ...owned by the player in check... - { - // ...evaluate if any move gets the player out of check. - pathFinder.PathEvery(from, (other, position) => - { - if (validationBoard == null) validationBoard = new Shogi(this); - var moveToTry = new Move(from, position); - var moveSuccess = validationBoard.TryMove(moveToTry); - if (moveSuccess) - { - validationBoard = null; - if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) - { - isCheckmate = false; - } - } - }); - } - }); - return isCheckmate; - } - #endregion - - private void InitializeBoardState() - { - Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); - Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); - Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); - Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); - Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1); - Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); - Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); - Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); - Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); - - Board["A2"] = null; - Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1); - Board["C2"] = null; - Board["D2"] = null; - Board["E2"] = null; - Board["F2"] = null; - Board["G2"] = null; - Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1); - Board["I2"] = null; - - Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); - - Board["A4"] = null; - Board["B4"] = null; - Board["C4"] = null; - Board["D4"] = null; - Board["E4"] = null; - Board["F4"] = null; - Board["G4"] = null; - Board["H4"] = null; - Board["I4"] = null; - - Board["A5"] = null; - Board["B5"] = null; - Board["C5"] = null; - Board["D5"] = null; - Board["E5"] = null; - Board["F5"] = null; - Board["G5"] = null; - Board["H5"] = null; - Board["I5"] = null; - - Board["A6"] = null; - Board["B6"] = null; - Board["C6"] = null; - Board["D6"] = null; - Board["E6"] = null; - Board["F6"] = null; - Board["G6"] = null; - Board["H6"] = null; - Board["I6"] = null; - - Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); - - Board["A8"] = null; - Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2); - Board["C8"] = null; - Board["D8"] = null; - Board["E8"] = null; - Board["F8"] = null; - Board["G8"] = null; - Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2); - Board["I8"] = null; - - Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); - Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); - Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); - Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); - Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2); - Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); - Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); - Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); - Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); - } - - public BoardState ToServiceModel() - { - return new BoardState - { - Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()), - PlayerInCheck = InCheck, - WhoseTurn = WhoseTurn, - Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), - Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() - }; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs index ecd0450..ba76c72 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -1,65 +1,52 @@ -using Gameboard.ShogiUI.Sockets.Utilities; +using Shogi.Domain; using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { - public class BoardStateDocument : CouchDocument - { - public string Name { get; set; } + public class BoardStateDocument : CouchDocument + { + public string Name { get; set; } - /// - /// A dictionary where the key is a board-notation position, like D3. - /// - public Dictionary Board { get; set; } + /// + /// A dictionary where the key is a board-notation position, like D3. + /// + public Dictionary Board { get; set; } - public Piece[] Player1Hand { get; set; } + public Piece[] Player1Hand { get; set; } - public Piece[] Player2Hand { 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; } + /// + /// Move is null for first BoardState of a session - before anybody has made moves. + /// + public Move? Move { get; set; } - /// - /// Default constructor and setters are for deserialization. - /// - public BoardStateDocument() : base(WhichDocumentType.BoardState) - { - Name = string.Empty; - Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); - Player1Hand = Array.Empty(); - Player2Hand = Array.Empty(); - } + /// + /// Default constructor and setters are for deserialization. + /// + public BoardStateDocument() : base(WhichDocumentType.BoardState) + { + Name = string.Empty; + Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); + Player1Hand = Array.Empty(); + Player2Hand = Array.Empty(); + } - public BoardStateDocument(string sessionName, Models.Shogi shogi) - : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) - { - Name = sessionName; - Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); + public BoardStateDocument(string sessionName, Session shogi) + : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) + { + static Piece MapPiece(Shogi.Domain.Pieces.Piece piece) + { + return new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece }; + } - for (var x = 0; x < 9; x++) - for (var y = 0; y < 9; y++) - { - var position = new Vector2(x, y); - var piece = shogi.Board[y, x]; + Name = sessionName; + Board = shogi.BoardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null ? null : MapPiece(kvp.Value)); - if (piece != null) - { - var positionNotation = NotationHelper.ToBoardNotation(position); - Board[positionNotation] = new Piece(piece); - } - } - - Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray(); - Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray(); - if (shogi.MoveHistory.Count > 0) - { - Move = new Move(shogi.MoveHistory[^1]); - } - } - } + Player1Hand = shogi.Player1Hand.Select(piece => MapPiece(piece)).ToArray(); + Player2Hand = shogi.Player2Hand.Select(piece => MapPiece(piece)).ToArray(); + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs index 8b29998..917fdf7 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs @@ -1,5 +1,4 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Numerics; +using Shogi.Domain; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { @@ -29,28 +28,5 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { To = string.Empty; } - - public Move(Models.Move move) - { - if (move.From.HasValue) - { - From = ToBoardNotation(move.From.Value); - } - IsPromotion = move.IsPromotion; - To = ToBoardNotation(move.To); - PieceFromHand = move.PieceFromHand; - } - - private static readonly char A = 'A'; - private static string ToBoardNotation(Vector2 vector) - { - var file = (char)(vector.X + A); - var rank = vector.Y + 1; - return $"{file}{rank}"; - } - - public Models.Move ToDomainModel() => PieceFromHand.HasValue - ? new(PieceFromHand.Value, To) - : new(From!, To, IsPromotion); } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs index 7f0be6f..e5df4fc 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs @@ -1,27 +1,11 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Shogi.Domain; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { - public class Piece - { - public bool IsPromoted { get; set; } - public WhichPerspective 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(WhichPiece, Owner, IsPromoted); - } + public class Piece + { + public bool IsPromoted { get; set; } + public WhichPlayer Owner { get; set; } + public WhichPiece WhichPiece { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs index 6db129e..75249e1 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels { - public class SessionDocument : CouchDocument + public class SessionDocument : CouchDocument { public string Name { get; set; } public string Player1Id { get; set; } public string? Player2Id { get; set; } public bool IsPrivate { get; set; } - public IList History { get; set; } /// /// Default constructor and setters are for deserialization. @@ -18,17 +15,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels Name = string.Empty; Player1Id = string.Empty; Player2Id = string.Empty; - History = new List(0); - } - - public SessionDocument(Models.Session session) - : base(session.Name, WhichDocumentType.Session) - { - Name = session.Name; - Player1Id = session.Player1.Id; - Player2Id = session.Player2?.Id; - IsPrivate = session.IsPrivate; - History = new List(0); } public SessionDocument(Models.SessionMetadata sessionMetaData) @@ -38,7 +24,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels Player1Id = sessionMetaData.Player1.Id; Player2Id = sessionMetaData.Player2?.Id; IsPrivate = sessionMetaData.IsPrivate; - History = new List(0); } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs index 3489693..075e388 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Shogi.Domain; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -11,282 +12,302 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using System.Web; namespace Gameboard.ShogiUI.Sockets.Repositories { - public interface IGameboardRepository - { - Task CreateBoardState(Models.Session session); - Task CreateSession(Models.SessionMetadata session); - Task CreateUser(Models.User user); - Task> ReadSessionMetadatas(); - Task ReadSession(string name); - Task UpdateSession(Models.SessionMetadata session); - Task ReadSessionMetaData(string name); - Task ReadUser(string userName); - } + public interface IGameboardRepository + { + Task CreateBoardState(Session session); + Task CreateSession(Models.SessionMetadata session); + Task CreateUser(Models.User user); + Task> ReadSessionMetadatas(); + Task ReadSession(string name); + Task UpdateSession(Models.SessionMetadata session); + Task ReadSessionMetaData(string name); + Task ReadUser(string userName); + } - public class GameboardRepository : IGameboardRepository - { - /// - /// Returns session, board state, and user documents, grouped by session. - /// - private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; - /// - /// Returns session and user documents, grouped by session. - /// - private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; - private static readonly string View_User = "_design/user/_view/user"; - private const string ApplicationJson = "application/json"; - private readonly HttpClient client; - private readonly ILogger logger; + public class GameboardRepository : IGameboardRepository + { + /// + /// Returns session, board state, and user documents, grouped by session. + /// + private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; + /// + /// Returns session and user documents, grouped by session. + /// + private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; + private static readonly string View_User = "_design/user/_view/user"; + private const string ApplicationJson = "application/json"; + private readonly HttpClient client; + private readonly ILogger logger; - public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) - { - client = clientFactory.CreateClient("couchdb"); - this.logger = logger; - } + public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) + { + client = clientFactory.CreateClient("couchdb"); + this.logger = logger; + } - public async Task> ReadSessionMetadatas() - { - var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); - var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null) - { - var groupedBySession = result.rows.GroupBy(row => row.id); - var sessions = new List(result.total_rows / 3); - foreach (var group in groupedBySession) - { - /** + public async Task> ReadSessionMetadatas() + { + var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); + var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null) + { + var groupedBySession = result.rows.GroupBy(row => row.id); + var sessions = new List(result.total_rows / 3); + foreach (var group in groupedBySession) + { + /** * A group contains 3 elements. * 1) The session metadata. * 2) User document of Player1. * 3) User document of Player2. */ - var session = group.FirstOrDefault()?.doc.ToObject(); - var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); - var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); - if (session != null && player1Doc != null) - { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2)); - } - } - return new Collection(sessions); - } - return new Collection(Array.Empty()); - } + var session = group.FirstOrDefault()?.doc.ToObject(); + var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); + var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); + if (session != null && player1Doc != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2)); + } + } + return new Collection(sessions); + } + return new Collection(Array.Empty()); + } - public async Task ReadSession(string name) - { - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "startkey", JsonConvert.SerializeObject(new [] {name}) }, - { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } - }.ToQueryString(); - var query = $"{View_SessionWithBoardState}{queryParams}"; - logger.LogInformation("ReadSession() query: {query}", query); - var response = await client.GetAsync(query); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 2) - { - var group = result.rows; - /** - * A group contains 3 type of elements. - * 1) The session metadata. - * 2) User documents of Player1 and Player2. + public async Task ReadSession(string name) + { + static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece) + { + return piece == null + ? null + : Shogi.Domain.Pieces.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + } + + var queryParams = new QueryBuilder + { + { "include_docs", "true" }, + { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + }.ToQueryString(); + var query = $"{View_SessionWithBoardState}{queryParams}"; + logger.LogInformation("ReadSession() query: {query}", query); + var response = await client.GetAsync(query); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 2) + { + var group = result.rows; + /** + * A group contains multiple elements. + * 0) The session metadata. + * 1) User documents of Player1. + * 2) User documents of Player1. * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :( - * 3) BoardState + * Everything Else) Snapshots of the boardstate after every player move. */ - var session = group[0].doc.ToObject(); - var player1Doc = group[1].doc.ToObject(); - var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); - var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() - ? group[2].doc.ToObject() - : null; - var moves = group - .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session. - // TODO: Deserialize just the Move property. - .Select(row => row.doc.ToObject()) - .Select(boardState => - { - var move = boardState!.Move!; - return move.PieceFromHand.HasValue - ? new Models.Move(move.PieceFromHand.Value, move.To) - : new Models.Move(move.From!, move.To, move.IsPromotion); - }) - .ToList(); + var session = group[0].doc.ToObject(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : null; + var boardState = group.Last().doc.ToObject(); - var shogi = new Models.Shogi(moves); - if (session != null && player1Doc != null) - { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2); - } - } - return null; - } + if (session != null && player1Doc != null && boardState != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName); + var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value))); + return new Session(shogiBoardState, metaData); + } + } + return null; + } - public async Task ReadSessionMetaData(string name) - { - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "startkey", JsonConvert.SerializeObject(new [] {name}) }, - { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } - }.ToQueryString(); - var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 2) - { - var group = result.rows; - /** + public async Task ReadSessionMetaData(string name) + { + var queryParams = new QueryBuilder + { + { "include_docs", "true" }, + { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + }.ToQueryString(); + var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 2) + { + var group = result.rows; + /** * A group contains 3 elements. * 1) The session metadata. * 2) User document of Player1. * 3) User document of Player2. */ - var session = group[0].doc.ToObject(); - var player1Doc = group[1].doc.ToObject(); - var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); - var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() - ? group[2].doc.ToObject() - : null; - if (session != null && player1Doc != null) - { - var player2 = player2Doc == null ? null : new Models.User(player2Doc); - return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2); - } - } - return null; - } + var session = group[0].doc.ToObject(); + var player1Doc = group[1].doc.ToObject(); + var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); + var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() + ? group[2].doc.ToObject() + : null; + if (session != null && player1Doc != null) + { + var player2 = player2Doc == null ? null : new Models.User(player2Doc); + return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2); + } + } + return null; + } - /// - /// Saves a snapshot of board state and the most recent move. - /// - public async Task CreateBoardState(Models.Session session) - { - var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi); - var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - return response.IsSuccessStatusCode; - } + /// + /// Saves a snapshot of board state and the most recent move. + /// + public async Task CreateBoardState(Session session) + { + Piece? MapPiece(Shogi.Domain.Pieces.Piece? piece) + { + return piece == null + ? null + : new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece }; + } - public async Task CreateSession(Models.SessionMetadata session) - { - var sessionDocument = new SessionDocument(session); - var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); - var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); + var boardStateDocument = new BoardStateDocument(session.Name, session); + var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; + } - var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi()); - var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + public async Task CreateSession(Models.SessionMetadata session) + { + var sessionDocument = new SessionDocument(session); + var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); + var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); - if ((await postSessionDocumentTask).IsSuccessStatusCode) - { - var response = await client.PostAsync(string.Empty, boardStateContent); - return response.IsSuccessStatusCode; - } - return false; - } + var boardStateDocument = new BoardStateDocument(session.Name, new Session()); + var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); - public async Task UpdateSession(Models.SessionMetadata session) - { - // GET existing session to get revisionId. - var readResponse = await client.GetAsync(session.Name); - if (!readResponse.IsSuccessStatusCode) return false; - var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); + if ((await postSessionDocumentTask).IsSuccessStatusCode) + { + var response = await client.PostAsync(string.Empty, boardStateContent); + return response.IsSuccessStatusCode; + } + return false; + } - // PUT the document with the revisionId. - var couchModel = new SessionDocument(session) - { - RevisionId = sessionDocument?.RevisionId - }; - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PutAsync(couchModel.Id, content); - return response.IsSuccessStatusCode; - } - //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 UpdateSession(Models.SessionMetadata session) + { + // GET existing session to get revisionId. + var readResponse = await client.GetAsync(session.Name); + if (!readResponse.IsSuccessStatusCode) return false; + var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); - //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; - //} + // PUT the document with the revisionId. + var couchModel = new SessionDocument(session) + { + RevisionId = sessionDocument?.RevisionId + }; + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PutAsync(couchModel.Id, content); + return response.IsSuccessStatusCode; + } + //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> 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 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 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> 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 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; - return string.Empty; - } + //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 ReadUser(string id) - { - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "key", JsonConvert.SerializeObject(id) }, - }.ToQueryString(); - var response = await client.GetAsync($"{View_User}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 0) - { - return new Models.User(result.rows[0].doc); - } + 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; + return string.Empty; + } - return null; - } + public async Task ReadUser(string id) + { + var queryParams = new QueryBuilder + { + { "include_docs", "true" }, + { "key", JsonConvert.SerializeObject(id) }, + }.ToQueryString(); + var response = await client.GetAsync($"{View_User}{queryParams}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseContent); + if (result != null && result.rows.Length > 0) + { + return new Models.User(result.rows[0].doc); + } - public async Task CreateUser(Models.User user) - { - var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - return response.IsSuccessStatusCode; - } + return null; + } - } + public async Task CreateUser(Models.User user) + { + var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); + var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + return response.IsSuccessStatusCode; + } + + public void ReadMoveHistory() + { + //TODO: Separate move history into a separate request. + //var moves = group + // .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session. + // // TODO: Deserialize just the Move property. + // .Select(row => row.doc.ToObject()) + // .Select(boardState => + // { + // var move = boardState!.Move!; + // return move.PieceFromHand.HasValue + // ? new Models.Move(move.PieceFromHand.Value, move.To) + // : new Models.Move(move.From!, move.To, move.IsPromotion); + // }) + // .ToList(); + } + } } diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs index 0418950..fefbd52 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs @@ -5,7 +5,6 @@ using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Gameboard.ShogiUI.Sockets.Services.Utility; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -17,7 +16,7 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Services { - public interface ISocketService + public interface ISocketService { Task HandleSocketRequest(HttpContext context); } @@ -74,7 +73,7 @@ namespace Gameboard.ShogiUI.Sockets.Services } var socket = await context.WebSockets.AcceptWebSocketAsync(); - communicationManager.SubscribeToBroadcast(socket, userName); + communicationManager.Subscribe(socket, userName); while (socket.State == WebSocketState.Open) { try @@ -82,7 +81,7 @@ namespace Gameboard.ShogiUI.Sockets.Services var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); - var request = JsonConvert.DeserializeObject(message); + var request = JsonConvert.DeserializeObject(message); if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); @@ -114,7 +113,7 @@ namespace Gameboard.ShogiUI.Sockets.Services logger.LogInformation("Probably tried writing to a closed socket."); logger.LogError(ex.Message); } - communicationManager.UnsubscribeFromBroadcastAndGames(userName); + communicationManager.Unsubscribe(userName); } } diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs deleted file mode 100644 index c4fde65..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; - -namespace Gameboard.ShogiUI.Sockets.Services.Utility -{ - public class JsonRequest - { - public IRequest Request { get; private set; } - public string Json { get; private set; } - public JsonRequest(IRequest request, string json) - { - Request = request; - Json = json; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs deleted file mode 100644 index 1928408..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.Utility -{ - public class Request : IRequest - { - public ClientAction Action { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs deleted file mode 100644 index 067d374..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; - -namespace Gameboard.ShogiUI.Sockets.Services.Utility -{ - public class Response : IResponse - { - public string Action { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs b/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs deleted file mode 100644 index c302164..0000000 --- a/Gameboard.ShogiUI.Sockets/Utilities/CoordsToNotationCollection.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using PathFinding; -using System; -using System.Collections.Generic; -using System.Numerics; - -namespace Gameboard.ShogiUI.Sockets.Utilities -{ - public class CoordsToNotationCollection : Dictionary, IPlanarCollection - { - public delegate void ForEachDelegate(Piece element, Vector2 position); - - public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase) - { - } - - public CoordsToNotationCollection(Dictionary board) : base(board, StringComparer.OrdinalIgnoreCase) - { - } - - public Piece? this[Vector2 vector] - { - get => this[NotationHelper.ToBoardNotation(vector)]; - set => this[NotationHelper.ToBoardNotation(vector)] = value; - } - - public Piece? this[int x, int y] - { - get => this[NotationHelper.ToBoardNotation(x, y)]; - set => this[NotationHelper.ToBoardNotation(x, y)] = value; - } - - - public void ForEachNotNull(ForEachDelegate callback) - { - for (var x = 0; x < 9; x++) - { - for (var y = 0; y < 9; y++) - { - var position = new Vector2(x, y); - var elem = this[position]; - if (elem != null) - callback(elem, position); - } - } - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs b/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs deleted file mode 100644 index c6831db..0000000 --- a/Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Numerics; -using System.Text.RegularExpressions; - -namespace Gameboard.ShogiUI.Sockets.Utilities -{ - public static class NotationHelper - { - private static readonly string BoardNotationRegex = @"(?[a-iA-I])(?[1-9])"; - private static readonly char A = 'A'; - - public static string ToBoardNotation(Vector2 vector) - { - return ToBoardNotation((int)vector.X, (int)vector.Y); - } - public static string ToBoardNotation(int x, int y) - { - var file = (char)(x + A); - var rank = y + 1; - return $"{file}{rank}"; - } - - public static Vector2 FromBoardNotation(string notation) - { - notation = notation.ToUpper(); - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Vector2(file - A, rank - 1); - } - throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); - } - } -} diff --git a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs index 1476dbb..e13dea3 100644 --- a/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs @@ -3,7 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; using System.Numerics; -using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective; +using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer; using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; namespace Gameboard.ShogiUI.UnitTests.Rules { diff --git a/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs b/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs deleted file mode 100644 index d7c0123..0000000 --- a/Gameboard.ShogiUI.xUnitTests/CoordsToNotationCollectionShould.cs +++ /dev/null @@ -1,41 +0,0 @@ -using AutoFixture; -using FluentAssertions; -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Utilities; -using Xunit; - -namespace Gameboard.ShogiUI.xUnitTests -{ - public class CoordsToNotationCollectionShould - { - private readonly Fixture fixture; - private readonly CoordsToNotationCollection collection; - public CoordsToNotationCollectionShould() - { - fixture = new Fixture(); - collection = new CoordsToNotationCollection(); - } - - [Fact] - public void TranslateCoordinatesToNotation() - { - // Arrange - collection[0, 0] = fixture.Create(); - collection[4, 4] = fixture.Create(); - collection[8, 8] = fixture.Create(); - collection[2, 2] = fixture.Create(); - - // Assert - collection["A1"].Should().BeSameAs(collection[0, 0]); - collection["E5"].Should().BeSameAs(collection[4, 4]); - collection["I9"].Should().BeSameAs(collection[8, 8]); - collection["C3"].Should().BeSameAs(collection[2, 2]); - } - - [Fact] - public void Yep() - { - - } - } -} diff --git a/Gameboard.ShogiUI.xUnitTests/GameShould.cs b/Gameboard.ShogiUI.xUnitTests/GameShould.cs index 7d9354e..451d4f3 100644 --- a/Gameboard.ShogiUI.xUnitTests/GameShould.cs +++ b/Gameboard.ShogiUI.xUnitTests/GameShould.cs @@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.xUnitTests [Fact] public void DiscardNullPLayers() { - var game = new Game("Test", "P1", null); + var game = new Session("Test", "P1", null); game.Players.Count.Should().Be(1); } diff --git a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs deleted file mode 100644 index 8b130c7..0000000 --- a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs +++ /dev/null @@ -1,621 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Models; -using System.Linq; -using Xunit; -using Xunit.Abstractions; -using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; -using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective; -using ShogiModel = Gameboard.ShogiUI.Sockets.Models.Shogi; - -namespace Gameboard.ShogiUI.xUnitTests -{ - public class ShogiShould - { - private readonly ITestOutputHelper output; - public ShogiShould(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void InitializeBoardState() - { - // Act - var board = new ShogiModel().Board; - - // Assert - board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - board["A1"].Owner.Should().Be(WhichPerspective.Player1); - board["A1"].IsPromoted.Should().Be(false); - board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); - board["B1"].Owner.Should().Be(WhichPerspective.Player1); - board["B1"].IsPromoted.Should().Be(false); - board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C1"].Owner.Should().Be(WhichPerspective.Player1); - board["C1"].IsPromoted.Should().Be(false); - board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D1"].Owner.Should().Be(WhichPerspective.Player1); - board["D1"].IsPromoted.Should().Be(false); - board["E1"].WhichPiece.Should().Be(WhichPiece.King); - board["E1"].Owner.Should().Be(WhichPerspective.Player1); - board["E1"].IsPromoted.Should().Be(false); - board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F1"].Owner.Should().Be(WhichPerspective.Player1); - board["F1"].IsPromoted.Should().Be(false); - board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G1"].Owner.Should().Be(WhichPerspective.Player1); - board["G1"].IsPromoted.Should().Be(false); - board["H1"].WhichPiece.Should().Be(WhichPiece.Knight); - board["H1"].Owner.Should().Be(WhichPerspective.Player1); - board["H1"].IsPromoted.Should().Be(false); - board["I1"].WhichPiece.Should().Be(WhichPiece.Lance); - board["I1"].Owner.Should().Be(WhichPerspective.Player1); - board["I1"].IsPromoted.Should().Be(false); - - board["A2"].Should().BeNull(); - board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop); - board["B2"].Owner.Should().Be(WhichPerspective.Player1); - board["B2"].IsPromoted.Should().Be(false); - board["C2"].Should().BeNull(); - board["D2"].Should().BeNull(); - board["E2"].Should().BeNull(); - board["F2"].Should().BeNull(); - board["G2"].Should().BeNull(); - board["H2"].WhichPiece.Should().Be(WhichPiece.Rook); - board["H2"].Owner.Should().Be(WhichPerspective.Player1); - board["H2"].IsPromoted.Should().Be(false); - board["I2"].Should().BeNull(); - - board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["A3"].Owner.Should().Be(WhichPerspective.Player1); - board["A3"].IsPromoted.Should().Be(false); - board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["B3"].Owner.Should().Be(WhichPerspective.Player1); - board["B3"].IsPromoted.Should().Be(false); - board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["C3"].Owner.Should().Be(WhichPerspective.Player1); - board["C3"].IsPromoted.Should().Be(false); - board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["D3"].Owner.Should().Be(WhichPerspective.Player1); - board["D3"].IsPromoted.Should().Be(false); - board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["E3"].Owner.Should().Be(WhichPerspective.Player1); - board["E3"].IsPromoted.Should().Be(false); - board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["F3"].Owner.Should().Be(WhichPerspective.Player1); - board["F3"].IsPromoted.Should().Be(false); - board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["G3"].Owner.Should().Be(WhichPerspective.Player1); - board["G3"].IsPromoted.Should().Be(false); - board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["H3"].Owner.Should().Be(WhichPerspective.Player1); - board["H3"].IsPromoted.Should().Be(false); - board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["I3"].Owner.Should().Be(WhichPerspective.Player1); - board["I3"].IsPromoted.Should().Be(false); - - board["A4"].Should().BeNull(); - board["B4"].Should().BeNull(); - board["C4"].Should().BeNull(); - board["D4"].Should().BeNull(); - board["E4"].Should().BeNull(); - board["F4"].Should().BeNull(); - board["G4"].Should().BeNull(); - board["H4"].Should().BeNull(); - board["I4"].Should().BeNull(); - - board["A5"].Should().BeNull(); - board["B5"].Should().BeNull(); - board["C5"].Should().BeNull(); - board["D5"].Should().BeNull(); - board["E5"].Should().BeNull(); - board["F5"].Should().BeNull(); - board["G5"].Should().BeNull(); - board["H5"].Should().BeNull(); - board["I5"].Should().BeNull(); - - board["A6"].Should().BeNull(); - board["B6"].Should().BeNull(); - board["C6"].Should().BeNull(); - board["D6"].Should().BeNull(); - board["E6"].Should().BeNull(); - board["F6"].Should().BeNull(); - board["G6"].Should().BeNull(); - board["H6"].Should().BeNull(); - board["I6"].Should().BeNull(); - - board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["A7"].Owner.Should().Be(WhichPerspective.Player2); - board["A7"].IsPromoted.Should().Be(false); - board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["B7"].Owner.Should().Be(WhichPerspective.Player2); - board["B7"].IsPromoted.Should().Be(false); - board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["C7"].Owner.Should().Be(WhichPerspective.Player2); - board["C7"].IsPromoted.Should().Be(false); - board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["D7"].Owner.Should().Be(WhichPerspective.Player2); - board["D7"].IsPromoted.Should().Be(false); - board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["E7"].Owner.Should().Be(WhichPerspective.Player2); - board["E7"].IsPromoted.Should().Be(false); - board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["F7"].Owner.Should().Be(WhichPerspective.Player2); - board["F7"].IsPromoted.Should().Be(false); - board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["G7"].Owner.Should().Be(WhichPerspective.Player2); - board["G7"].IsPromoted.Should().Be(false); - board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["H7"].Owner.Should().Be(WhichPerspective.Player2); - board["H7"].IsPromoted.Should().Be(false); - board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn); - board["I7"].Owner.Should().Be(WhichPerspective.Player2); - board["I7"].IsPromoted.Should().Be(false); - - board["A8"].Should().BeNull(); - board["B8"].WhichPiece.Should().Be(WhichPiece.Rook); - board["B8"].Owner.Should().Be(WhichPerspective.Player2); - board["B8"].IsPromoted.Should().Be(false); - board["C8"].Should().BeNull(); - board["D8"].Should().BeNull(); - board["E8"].Should().BeNull(); - board["F8"].Should().BeNull(); - board["G8"].Should().BeNull(); - board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); - board["H8"].Owner.Should().Be(WhichPerspective.Player2); - board["H8"].IsPromoted.Should().Be(false); - board["I8"].Should().BeNull(); - - board["A9"].WhichPiece.Should().Be(WhichPiece.Lance); - board["A9"].Owner.Should().Be(WhichPerspective.Player2); - board["A9"].IsPromoted.Should().Be(false); - board["B9"].WhichPiece.Should().Be(WhichPiece.Knight); - board["B9"].Owner.Should().Be(WhichPerspective.Player2); - board["B9"].IsPromoted.Should().Be(false); - board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C9"].Owner.Should().Be(WhichPerspective.Player2); - board["C9"].IsPromoted.Should().Be(false); - board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D9"].Owner.Should().Be(WhichPerspective.Player2); - board["D9"].IsPromoted.Should().Be(false); - board["E9"].WhichPiece.Should().Be(WhichPiece.King); - board["E9"].Owner.Should().Be(WhichPerspective.Player2); - board["E9"].IsPromoted.Should().Be(false); - board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F9"].Owner.Should().Be(WhichPerspective.Player2); - board["F9"].IsPromoted.Should().Be(false); - board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G9"].Owner.Should().Be(WhichPerspective.Player2); - board["G9"].IsPromoted.Should().Be(false); - board["H9"].WhichPiece.Should().Be(WhichPiece.Knight); - board["H9"].Owner.Should().Be(WhichPerspective.Player2); - board["H9"].IsPromoted.Should().Be(false); - board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - board["I9"].Owner.Should().Be(WhichPerspective.Player2); - board["I9"].IsPromoted.Should().Be(false); - } - - [Fact] - public void InitializeBoardStateWithMoves() - { - var moves = new[] - { - // P1 Pawn - new Move("A3", "A4") - }; - var shogi = new ShogiModel(moves); - shogi.Board["A3"].Should().BeNull(); - shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); - } - - [Fact] - public void AllowValidMoves_AfterCheck() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("G7", "G6"), - // P1 Bishop puts P2 in check - new Move("B2", "G7"), - }; - var shogi = new ShogiModel(moves); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - - // Act - P2 is able to un-check theirself. - /// P2 King moves out of check - var moveSuccess = shogi.Move(new Move("E9", "E8")); - - // Assert - using var _ = new AssertionScope(); - moveSuccess.Should().BeTrue(); - shogi.InCheck.Should().BeNull(); - } - - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = new ShogiModel(); - shogi.Board["D5"].Should().BeNull(); - - // Act - var moveSuccess = shogi.Move(new Move("D5", "D6")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["D5"].Should().BeNull(); - shogi.Board["D6"].Should().BeNull(); - } - - [Fact] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var shogi = new ShogiModel(); - - // Act - P1 "moves" pawn to the position it already exists at. - var moveSuccess = shogi.Move(new Move("A3", "A3")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } - - [Fact] - public void PreventInvalidMoves_MoveSet() - { - // Arrange - var shogi = new ShogiModel(); - - // Act - Move Lance illegally - var moveSuccess = shogi.Move(new Move("A1", "D5")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["A5"].Should().BeNull(); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } - - [Fact] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var shogi = new ShogiModel(); - shogi.WhoseTurn.Should().Be(WhichPerspective.Player1); - shogi.Board["A7"].Owner.Should().Be(WhichPerspective.Player2); - - // Act - Move Player2 Pawn when it is Player1 turn. - var moveSuccess = shogi.Move(new Move("A7", "A6")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board["A6"].Should().BeNull(); - } - - [Fact] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Arrange - var shogi = new ShogiModel(); - - // Act - Move P1 Lance through P1 Pawn. - var moveSuccess = shogi.Move(new Move("A1", "A5")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Board["A5"].Should().BeNull(); - } - - [Fact] - public void PreventInvalidMoves_CaptureAlly() - { - // Arrange - var shogi = new ShogiModel(); - - // Act - P1 Knight tries to capture P1 Pawn. - var moveSuccess = shogi.Move(new Move("B1", "C3")); - - // Arrange - moveSuccess.Should().BeFalse(); - shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); - shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); - shogi.Player1Hand.Should().BeEmpty(); - shogi.Player2Hand.Should().BeEmpty(); - } - - [Fact] - public void PreventInvalidMoves_Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("G7", "G6"), - // P1 Bishop puts P2 in check - new Move("B2", "G7") - }; - var shogi = new ShogiModel(moves); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - - // Act - P2 moves Lance while in check. - var moveSuccess = shogi.Move(new Move("I9", "I8")); - - // Assert - moveSuccess.Should().BeFalse(); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I8"].Should().BeNull(); - } - - [Fact] - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("I7", "I6"), - // P1 Bishop takes P2 Pawn. - new Move("B2", "G7"), - // P2 Gold, block check from P1 Bishop. - new Move("F9", "F8"), - // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - new Move("G7", "H8", true), - // P2 Pawn again - new Move("I6", "I5"), - // P1 Bishop takes P2 Knight - new Move("H8", "H9"), - // P2 Pawn again - new Move("I5", "I4"), - // P1 Bishop takes P2 Lance - new Move("H9", "I9"), - // P2 Pawn captures P1 Pawn - new Move("I4", "I3") - }; - var shogi = new ShogiModel(moves); - shogi.Player1Hand.Count.Should().Be(4); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.WhoseTurn.Should().Be(WhichPerspective.Player1); - - // Act | Assert - Illegally placing Knight from the hand in farthest row. - shogi.Board["H9"].Should().BeNull(); - var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - - // Act | Assert - Illegally placing Knight from the hand in second farthest row. - shogi.Board["H8"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); - dropSuccess.Should().BeFalse(); - shogi.Board["H8"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - - // Act | Assert - Illegally place Lance from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - - // Act | Assert - Illegally place Pawn from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - - // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // TODO - } - - [Fact] - public void PreventInvalidDrop_Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("G7", "G6"), - // P1 Pawn, arbitrary move. - new Move("A3", "A4"), - // P2 Bishop takes P1 Bishop - new Move("H8", "B2"), - // P1 Silver takes P2 Bishop - new Move("C1", "B2"), - // P2 Pawn, arbtrary move - new Move("A7", "A6"), - // P1 drop Bishop, place P2 in check - new Move(WhichPiece.Bishop, "G7") - }; - var shogi = new ShogiModel(moves); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["E5"].Should().BeNull(); - - // Act - P2 places a Bishop while in check. - var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); - - // Assert - dropSuccess.Should().BeFalse(); - shogi.Board["E5"].Should().BeNull(); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - } - - [Fact] - public void PreventInvalidDrop_Capture() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("G7", "G6"), - // P1 Bishop capture P2 Bishop - new Move("B2", "H8"), - // P2 Pawn - new Move("G6", "G5") - }; - var shogi = new ShogiModel(moves); - using (new AssertionScope()) - { - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["I9"].Should().NotBeNull(); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2); - } - - // Act - P1 tries to place a piece where an opponent's piece resides. - var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); - - // Assert - using (new AssertionScope()) - { - dropSuccess.Should().BeFalse(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.Board["I9"].Should().NotBeNull(); - shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2); - } - } - - [Fact] - public void Check() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4"), - // P2 Pawn - new Move("G7", "G6"), - }; - var shogi = new ShogiModel(moves); - - // Act - P1 Bishop, check - shogi.Move(new Move("B2", "G7")); - - // Assert - shogi.InCheck.Should().Be(WhichPerspective.Player2); - } - - [Fact] - public void Promote() - { - // Arrange - var moves = new[] - { - // P1 Pawn - new Move("C3", "C4" ), - // P2 Pawn - new Move("G7", "G6" ) - }; - var shogi = new ShogiModel(moves); - - // Act - P1 moves across promote threshold. - var moveSuccess = shogi.Move(new Move("B2", "G7", true)); - - // Assert - using (new AssertionScope()) - { - moveSuccess.Should().BeTrue(); - shogi.Board["B2"].Should().BeNull(); - shogi.Board["G7"].Should().NotBeNull(); - shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Board["G7"].Owner.Should().Be(WhichPerspective.Player1); - shogi.Board["G7"].IsPromoted.Should().BeTrue(); - } - } - - [Fact] - public void CheckMate() - { - // Arrange - var moves = new[] - { - // P1 Rook - new Move("H2", "E2"), - // P2 Gold - new Move("F9", "G8"), - // P1 Pawn - new Move("E3", "E4"), - // P2 other Gold - new Move("D9", "C8"), - // P1 same Pawn - new Move("E4", "E5"), - // P2 Pawn - new Move("E7", "E6"), - // P1 Pawn takes P2 Pawn - new Move("E5", "E6"), - // P2 King - new Move("E9", "E8"), - // P1 Pawn promotes, threatens P2 King - new Move("E6", "E7", true), - // P2 King retreat - new Move("E8", "E9"), - }; - var shogi = new ShogiModel(moves); - output.WriteLine(shogi.PrintStateAsAscii()); - - // Act - P1 Pawn wins by checkmate. - var moveSuccess = shogi.Move(new Move("E7", "E8")); - output.WriteLine(shogi.PrintStateAsAscii()); - - // Assert - checkmate - moveSuccess.Should().BeTrue(); - shogi.IsCheckmate.Should().BeTrue(); - shogi.InCheck.Should().Be(WhichPerspective.Player2); - } - - [Fact] - public void Capture() - { - // Arrange - var moves = new[] - { - new Move("C3", "C4"), - new Move("G7", "G6") - }; - var shogi = new ShogiModel(moves); - - // Act - P1 Bishop captures P2 Bishop - var moveSuccess = shogi.Move(new Move("B2", "H8")); - - // Assert - moveSuccess.Should().BeTrue(); - shogi.Board["B2"].Should().BeNull(); - shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Board["H8"].Owner.Should().Be(WhichPerspective.Player1); - shogi.Board.Values - .Where(p => p != null) - .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); - - shogi.Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPerspective.Player1); - } - } -} diff --git a/Shogi.Domain.UnitTests/NotationShould.cs b/Shogi.Domain.UnitTests/NotationShould.cs new file mode 100644 index 0000000..4374276 --- /dev/null +++ b/Shogi.Domain.UnitTests/NotationShould.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using System.Numerics; +using Xunit; + +namespace Shogi.Domain.UnitTests +{ + public class NotationShould + { + [Fact] + public void ConvertFromNotationToVector() + { + Notation.FromBoardNotation("A1").Should().Be(new Vector2(0, 0)); + Notation.FromBoardNotation("E5").Should().Be(new Vector2(4, 4)); + Notation.FromBoardNotation("I9").Should().Be(new Vector2(8, 8)); + Notation.FromBoardNotation("C3").Should().Be(new Vector2(2, 2)); + } + + [Fact] + public void ConvertFromVectorToNotation() + { + Notation.ToBoardNotation(new Vector2(0, 0)).Should().Be("A1"); + Notation.ToBoardNotation(new Vector2(4, 4)).Should().Be("E5"); + Notation.ToBoardNotation(new Vector2(8, 8)).Should().Be("I9"); + Notation.ToBoardNotation(new Vector2(2, 2)).Should().Be("C3"); + } + } +} diff --git a/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs index 470a3cb..a2592f7 100644 --- a/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs +++ b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs @@ -9,7 +9,7 @@ namespace Shogi.Domain.UnitTests public void InitializeBoardState() { // Act - var board = new ShogiBoardState(); + var board = new BoardState(); // Assert board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance); diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs index c73d0ee..e36b046 100644 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -18,8 +18,8 @@ namespace Shogi.Domain.UnitTests public void MoveAPieceToAnEmptyPosition() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); board["A4"].Should().BeNull(); var expectedPiece = board["A3"]; expectedPiece.Should().NotBeNull(); @@ -36,8 +36,8 @@ namespace Shogi.Domain.UnitTests public void AllowValidMoves_AfterCheck() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -61,8 +61,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveFromEmptyPosition() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); board["D5"].Should().BeNull(); // Act @@ -80,8 +80,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveToCurrentPosition() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); var expectedPiece = board["A3"]; // Act - P1 "moves" pawn to the position it already exists at. @@ -101,8 +101,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveSet() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); var expectedPiece = board["A1"]; expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); @@ -124,8 +124,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_Ownership() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); var expectedPiece = board["A7"]; expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); board.WhoseTurn.Should().Be(WhichPlayer.Player1); @@ -146,8 +146,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveThroughAllies() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); var lance = board["A1"]; var pawn = board["A3"]; lance!.Owner.Should().Be(pawn!.Owner); @@ -169,8 +169,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_CaptureAlly() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); var knight = board["B1"]; var pawn = board["C3"]; knight!.Owner.Should().Be(pawn!.Owner); @@ -193,8 +193,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_Check() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -222,8 +222,8 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidDrops_MoveSet() { // Arrange - var board = new ShogiBoardState(); - var shogi = new Shogi(board); + var board = new BoardState(); + var shogi = new Session(board); // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -361,8 +361,8 @@ namespace Shogi.Domain.UnitTests public void Check() { // Arrange - var boardState = new ShogiBoardState(); - var shogi = new Shogi(boardState); + var boardState = new BoardState(); + var shogi = new Session(boardState); // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -379,8 +379,8 @@ namespace Shogi.Domain.UnitTests public void Promote() { // Arrange - var boardState = new ShogiBoardState(); - var shogi = new Shogi(boardState); + var boardState = new BoardState(); + var shogi = new Session(boardState); // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -404,8 +404,8 @@ namespace Shogi.Domain.UnitTests public void Capture() { // Arrange - var boardState = new ShogiBoardState(); - var shogi = new Shogi(boardState); + var boardState = new BoardState(); + var shogi = new Session(boardState); var p1Bishop = boardState["B2"]; p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); shogi.Move("C3", "C4", false); @@ -428,8 +428,8 @@ namespace Shogi.Domain.UnitTests public void CheckMate() { // Arrange - var boardState = new ShogiBoardState(); - var shogi = new Shogi(boardState); + var boardState = new BoardState(); + var shogi = new Session(boardState); // P1 Rook shogi.Move("H2", "E2", false); // P2 Gold diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/BoardState.cs similarity index 79% rename from Shogi.Domain/ShogiBoardState.cs rename to Shogi.Domain/BoardState.cs index 69c1645..9802010 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/BoardState.cs @@ -1,22 +1,26 @@ using Shogi.Domain.Pieces; -using System.Text.RegularExpressions; using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { - // TODO: Avoid extending dictionary. Use composition instead. - // Then validation can occur when assigning a piece to a position. - public class ShogiBoardState + public class BoardState { - private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private static readonly char A = 'A'; public delegate void ForEachDelegate(Piece element, Vector2 position); /// /// Key is position notation, such as "E4". /// private readonly Dictionary board; - public ShogiBoardState() + + public BoardState(Dictionary state) + { + board = state; + Player1Hand = new List(); + Player2Hand = new List(); + PreviousMoveTo = Vector2.Zero; + } + + public BoardState() { board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); InitializeBoardState(); @@ -25,13 +29,14 @@ namespace Shogi.Domain PreviousMoveTo = Vector2.Zero; } + public Dictionary State => board; public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - public Vector2 Player1KingPosition => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => { var piece = kvp.Value; return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; }).Key); - public Vector2 Player2KingPosition => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => { var piece = kvp.Value; return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; @@ -47,12 +52,12 @@ namespace Shogi.Domain /// /// Copy constructor. /// - public ShogiBoardState(ShogiBoardState other) : this() + public BoardState(BoardState other) : this() { foreach (var kvp in other.board) { // Replace copy constructor with static factory method in Piece.cs - board[kvp.Key] = kvp.Value == null ? null : Piece.Create(kvp.Value); + board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value); } WhoseTurn = other.WhoseTurn; InCheck = other.InCheck; @@ -71,14 +76,14 @@ namespace Shogi.Domain public Piece? this[Vector2 vector] { - get => this[ToBoardNotation(vector)]; - set => this[ToBoardNotation(vector)] = value; + get => this[Notation.ToBoardNotation(vector)]; + set => this[Notation.ToBoardNotation(vector)] = value; } public Piece? this[int x, int y] { - get => this[ToBoardNotation(x, y)]; - set => this[ToBoardNotation(x, y)] = value; + get => this[Notation.ToBoardNotation(x, y)]; + set => this[Notation.ToBoardNotation(x, y)] = value; } internal void RememberAsMostRecentMove(Vector2 from, Vector2 to) @@ -111,7 +116,7 @@ namespace Shogi.Domain internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(FromBoardNotation(kvp.Key), kvp.Value!)) + .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) .ToList(); internal void Capture(Vector2 to) @@ -144,27 +149,6 @@ namespace Shogi.Domain } return null; } - public static string ToBoardNotation(Vector2 vector) - { - return ToBoardNotation((int)vector.X, (int)vector.Y); - } - private static string ToBoardNotation(int x, int y) - { - var file = (char)(x + A); - var rank = y + 1; - return $"{file}{rank}"; - } - public static Vector2 FromBoardNotation(string notation) - { - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Vector2(file - A, rank - 1); - } - throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); - } private void InitializeBoardState() { diff --git a/Shogi.Domain/Notation.cs b/Shogi.Domain/Notation.cs new file mode 100644 index 0000000..a6fb831 --- /dev/null +++ b/Shogi.Domain/Notation.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace Shogi.Domain +{ + public static class Notation + { + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private static readonly char A = 'A'; + + public static string ToBoardNotation(Vector2 vector) + { + return ToBoardNotation((int)vector.X, (int)vector.Y); + } + + public static string ToBoardNotation(int x, int y) + { + var file = (char)(x + A); + var rank = y + 1; + return $"{file}{rank}"; + } + public static Vector2 FromBoardNotation(string notation) + { + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank - 1); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); + } + } +} diff --git a/Shogi.Domain/Pieces/Piece.cs b/Shogi.Domain/Pieces/Piece.cs index 7135edd..eb57909 100644 --- a/Shogi.Domain/Pieces/Piece.cs +++ b/Shogi.Domain/Pieces/Piece.cs @@ -9,7 +9,7 @@ namespace Shogi.Domain.Pieces /// /// Creates a clone of an existing piece. /// - public static Piece Create(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) { return piece switch @@ -25,8 +25,6 @@ namespace Shogi.Domain.Pieces _ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.") }; } - - // TODO: MoveSet doesn't account for Player2's pieces which are upside-down. public abstract IEnumerable MoveSet { get; } public WhichPiece WhichPiece { get; } public WhichPlayer Owner { get; private set; } diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Session.cs similarity index 85% rename from Shogi.Domain/Shogi.cs rename to Shogi.Domain/Session.cs index 028bb3b..0584722 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Session.cs @@ -8,21 +8,38 @@ namespace Shogi.Domain /// The board is always from Player1's perspective. /// [0,0] is the lower-left position, [8,8] is the higher-right position /// - public sealed class Shogi + public sealed class Session { - private readonly ShogiBoardState boardState; + private readonly Tuple Players; + private readonly BoardState boardState; private readonly StandardRules rules; + private readonly SessionMetadata metadata; - public Shogi() : this(new ShogiBoardState()) + public Session() : this(new BoardState()) { } - public Shogi(ShogiBoardState board) + public Session(BoardState state) : this(state, new SessionMetadata(string.Empty, false, string.Empty, string.Empty)) { - this.rules = new StandardRules(board); - this.boardState = board; } + public Session(BoardState state, SessionMetadata metadata) + { + rules = new StandardRules(state); + boardState = state; + this.metadata = metadata; + Players = new(string.Empty, string.Empty); + } + + public string Name => metadata.Name; + public string Player1Name => metadata.Player1; + public string? Player2Name => metadata.Player2; + public IDictionary BoardState => boardState.State; + public IList Player1Hand => boardState.Player1Hand; + public IList Player2Hand => boardState.Player2Hand; + public WhichPlayer? InCheck => boardState.InCheck; + public bool IsCheckMate => boardState.IsCheckmate; + /// /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. /// @@ -33,7 +50,7 @@ namespace Shogi.Domain /// public void Move(string from, string to, bool isPromotion) { - var simulationState = new ShogiBoardState(boardState); + var simulationState = new BoardState(boardState); var simulation = new StandardRules(simulationState); var moveResult = simulation.Move(from, to, isPromotion); if (!moveResult.Success) @@ -83,7 +100,7 @@ namespace Shogi.Domain throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); } - var toVector = ShogiBoardState.FromBoardNotation(to); + var toVector = Notation.FromBoardNotation(to); switch (pieceInHand) { case WhichPiece.Knight: @@ -109,7 +126,7 @@ namespace Shogi.Domain } } - var tempBoard = new ShogiBoardState(boardState); + var tempBoard = new BoardState(boardState); var simulation = new StandardRules(tempBoard); var moveResult = simulation.Move(pieceInHand, to); if (!moveResult.Success) diff --git a/Shogi.Domain/SessionMetadata.cs b/Shogi.Domain/SessionMetadata.cs new file mode 100644 index 0000000..972823c --- /dev/null +++ b/Shogi.Domain/SessionMetadata.cs @@ -0,0 +1,26 @@ +namespace Shogi.Domain +{ + /// + /// A representation of a Session without the board and game-rules. + /// + public class SessionMetadata + { + public string Name { get; } + public string Player1 { get; } + public string? Player2 { get; private set; } + public bool IsPrivate { get; } + + public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null) + { + Name = name; + IsPrivate = isPrivate; + Player1 = player1; + Player2 = player2; + } + + public void SetPlayer2(string user) + { + Player2 = user; + } + } +} diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 2699d16..9d9b588 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -6,9 +6,9 @@ namespace Shogi.Domain { internal class StandardRules { - private readonly ShogiBoardState boardState; + private readonly BoardState boardState; - internal StandardRules(ShogiBoardState board) + internal StandardRules(BoardState board) { boardState = board; } @@ -22,8 +22,8 @@ namespace Shogi.Domain /// A describing the success or failure of the move. internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false) { - var from = ShogiBoardState.FromBoardNotation(fromNotation); - var to = ShogiBoardState.FromBoardNotation(toNotation); + var from = Notation.FromBoardNotation(fromNotation); + var to = Notation.FromBoardNotation(toNotation); var fromPiece = boardState[from]; if (fromPiece == null) { @@ -69,7 +69,7 @@ namespace Shogi.Domain /// A describing the success or failure of the simulation. internal MoveResult Move(WhichPiece pieceInHand, string toNotation) { - var to = ShogiBoardState.FromBoardNotation(toNotation); + var to = Notation.FromBoardNotation(toNotation); var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { @@ -130,7 +130,7 @@ namespace Shogi.Domain // Get line equation from king through the now-unoccupied location. var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom); var slope = Math.Abs(direction.Y / direction.X); - var path = ShogiBoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction)); + var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction)); var threat = boardState.QueryFirstPieceInPath(path); if (threat == null || threat.Owner == previousMovedPiece.Owner) return false; // If absolute slope is 45°, look for a bishop along the line. @@ -253,7 +253,7 @@ namespace Shogi.Domain var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); return path .SkipLast(1) - .All(position => boardState[ShogiBoardState.ToBoardNotation(position)] == null); + .All(position => boardState[Notation.ToBoardNotation(position)] == null); } } }