From f644795cd39b83d85d2a8f75800235c5e0a079bc Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 23 Feb 2021 18:03:23 -0600 Subject: [PATCH] checkpoint --- .gitignore | 1 + Benchmarking/Benchmarking.csproj | 16 + Benchmarking/Benchmarks.cs | 58 ++ Gameboard.ShogiUI.BoardState/Array2D.cs | 29 + Gameboard.ShogiUI.BoardState/BoardVector.cs | 62 +++ Gameboard.ShogiUI.BoardState/Extensions.cs | 19 + .../Gameboard.ShogiUI.BoardState.csproj | 7 + Gameboard.ShogiUI.BoardState/Move.cs | 10 + Gameboard.ShogiUI.BoardState/Piece.cs | 53 ++ Gameboard.ShogiUI.BoardState/Position.cs | 35 ++ Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 522 ++++++++++++++++++ Gameboard.ShogiUI.BoardState/WhichPiece.cs | 14 + Gameboard.ShogiUI.BoardState/WhichPlayer.cs | 8 + .../Socket/Messages/ListGames.cs | 2 +- .../Socket/Messages/LoadGame.cs | 2 +- .../Socket/Types/BoardState.cs | 11 + .../Socket/Types/Game.cs | 17 +- .../Socket/Types/Piece.cs | 14 + .../Socket/Types/WhichPiece.cs | 14 + Gameboard.ShogiUI.Sockets.sln | 24 +- .../Gameboard.ShogiUI.Sockets.csproj | 1 + .../Gameboard.ShogiUI.Sockets.csproj.user | 12 - .../Managers/BoardManager.cs | 33 +- .../ClientActionHandlers/CreateGameHandler.cs | 12 +- .../ClientActionHandlers/IActionHandler.cs | 2 +- .../ClientActionHandlers/JoinByCodeHandler.cs | 25 +- .../ClientActionHandlers/JoinGameHandler.cs | 16 +- .../ClientActionHandlers/ListGamesHandler.cs | 16 +- .../ClientActionHandlers/LoadGameHandler.cs | 46 +- .../ClientActionHandlers/MoveHandler.cs | 36 +- .../Managers/SocketCommunicationManager.cs | 29 +- .../Managers/Utility/Mapper.cs | 67 --- Gameboard.ShogiUI.Sockets/Models/Move.cs | 64 ++- Gameboard.ShogiUI.Sockets/Startup.cs | 1 + .../BoardState/BoardVectorShould.cs | 26 + .../BoardState/ShogiBoardShould.cs | 322 +++++++++++ .../Gameboard.ShogiUI.UnitTests.csproj | 0 .../Sockets}/CoordsModelShould.cs | 2 +- 38 files changed, 1451 insertions(+), 177 deletions(-) create mode 100644 Benchmarking/Benchmarking.csproj create mode 100644 Benchmarking/Benchmarks.cs create mode 100644 Gameboard.ShogiUI.BoardState/Array2D.cs create mode 100644 Gameboard.ShogiUI.BoardState/BoardVector.cs create mode 100644 Gameboard.ShogiUI.BoardState/Extensions.cs create mode 100644 Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj create mode 100644 Gameboard.ShogiUI.BoardState/Move.cs create mode 100644 Gameboard.ShogiUI.BoardState/Piece.cs create mode 100644 Gameboard.ShogiUI.BoardState/Position.cs create mode 100644 Gameboard.ShogiUI.BoardState/ShogiBoard.cs create mode 100644 Gameboard.ShogiUI.BoardState/WhichPiece.cs create mode 100644 Gameboard.ShogiUI.BoardState/WhichPlayer.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs create mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs create mode 100644 Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs rename Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj => Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj (100%) rename {Gameboard.ShogiUI.Sockets.UnitTests/Models => Gameboard.ShogiUI.UnitTests/Sockets}/CoordsModelShould.cs (91%) diff --git a/.gitignore b/.gitignore index 26787d3..70b8a69 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ Thumbs.db #Luke bin obj +*.user diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj new file mode 100644 index 0000000..78da0b4 --- /dev/null +++ b/Benchmarking/Benchmarking.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Exe + + + + + + + + + + + diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs new file mode 100644 index 0000000..8cb0685 --- /dev/null +++ b/Benchmarking/Benchmarks.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using Gameboard.ShogiUI.BoardState; +using System; + +namespace Benchmarking +{ + public class Benchmarks + { + private Move[] moves; + + public Benchmarks() + { + moves = new[] + { + // P1 Rook + new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + // P2 Gold + new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + // P1 Pawn + new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + // P2 other Gold + new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + // P1 same Pawn + new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + // P2 Pawn + new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + // P1 Pawn takes P2 Pawn + new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + // P2 King + new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + // P1 Pawn promotes + new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + // P2 King retreat + new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + }; + } + + [Benchmark] + public void OnlyValidMoves_NewBoard() + { + var board = new ShogiBoard(); + foreach (var move in moves) + { + board.TryMove(move); + } + } + } + + public class Program + { + public static void Main(string[] args) + { + BenchmarkRunner.Run(); + Console.WriteLine("Done"); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs new file mode 100644 index 0000000..f2d7e0c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.BoardState +{ + public class Array2D : IEnumerable + { + private readonly T[] array; + private readonly int width; + + public Array2D(int width, int height) + { + this.width = width; + array = new T[width * height]; + } + + public T this[int x, int y] + { + get => array[y * width + x]; + set => array[y * width + x] = value; + } + + IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/BoardVector.cs b/Gameboard.ShogiUI.BoardState/BoardVector.cs new file mode 100644 index 0000000..5568ac5 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/BoardVector.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; + +namespace Gameboard.ShogiUI.BoardState +{ + /// + /// Provides normalized BoardVectors relative to player. + /// "Up" for player 1 is "Down" for player 2; that sort of thing. + /// + public class Direction + { + private static readonly BoardVector PositiveX = new BoardVector(1, 0); + private static readonly BoardVector NegativeX = new BoardVector(-1, 0); + private static readonly BoardVector PositiveY = new BoardVector(0, 1); + private static readonly BoardVector NegativeY = new BoardVector(0, -1); + private static readonly BoardVector PositiveYX = new BoardVector(1, 1); + private static readonly BoardVector NegativeYX = new BoardVector(-1, -1); + private static readonly BoardVector NegativeYPositiveX = new BoardVector(1, -1); + private static readonly BoardVector PositiveYNegativeX = new BoardVector(-1, 1); + + private readonly WhichPlayer whichPlayer; + public Direction(WhichPlayer whichPlayer) + { + this.whichPlayer = whichPlayer; + } + + public BoardVector Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; + public BoardVector Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; + public BoardVector Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; + public BoardVector Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; + public BoardVector UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; + public BoardVector UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; + public BoardVector DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; + public BoardVector DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; + public BoardVector KnightLeft => whichPlayer == WhichPlayer.Player1 ? new BoardVector(-1, 2) : new BoardVector(1, -2); + public BoardVector KnightRight => whichPlayer == WhichPlayer.Player1 ? new BoardVector(1, 2) : new BoardVector(-1, -2); + + } + + [DebuggerDisplay("[{X}, {Y}]")] + public class BoardVector + { + public int X { get; set; } + public int Y { get; set; } + public bool IsValidBoardPosition => X > -1 && X < 9 && Y > -1 && Y < 9; + public bool IsHand => X < 0 && Y < 0; // TODO: Find a better way to distinguish positions vs hand. + public BoardVector(int x, int y) + { + X = x; + Y = y; + } + + public BoardVector Add(BoardVector other) => new BoardVector(X + other.X, Y + other.Y); + public override bool Equals(object obj) => (obj is BoardVector other) && other.X == X && other.Y == Y; + public override int GetHashCode() + { + // [0,3] should hash different than [3,0] + return X.GetHashCode() * 3 + Y.GetHashCode() * 5; + } + public static bool operator ==(BoardVector a, BoardVector b) => a.Equals(b); + public static bool operator !=(BoardVector a, BoardVector b) => !a.Equals(b); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Extensions.cs b/Gameboard.ShogiUI.BoardState/Extensions.cs new file mode 100644 index 0000000..19dd810 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Extensions.cs @@ -0,0 +1,19 @@ +using System; +namespace Gameboard.ShogiUI.BoardState +{ + public static class Extensions + { + public static void ForEachNotNull(this Piece[,] array, Action action) + { + for (var x = 0; x < array.GetLength(0); x++) + for (var y = 0; y < array.GetLength(1); y++) + { + var piece = array[x, y]; + if (piece != null) + { + action(piece, x, y); + } + } + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj new file mode 100644 index 0000000..f208d30 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs new file mode 100644 index 0000000..f87ebad --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -0,0 +1,10 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public class Move + { + public WhichPiece? PieceFromCaptured { get; set; } + public BoardVector From { get; set; } + public BoardVector To { get; set; } + public bool IsPromotion { get; set; } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Piece.cs new file mode 100644 index 0000000..9a7ff64 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Piece.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; + +namespace Gameboard.ShogiUI.BoardState +{ + [DebuggerDisplay("{WhichPiece} {Owner}")] + public class Piece + { + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + + public Piece(WhichPiece piece, WhichPlayer owner) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = false; + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldenGeneral; + + public string ShortName => WhichPiece switch + { + WhichPiece.King => " K ", + WhichPiece.GoldenGeneral => " G ", + WhichPiece.SilverGeneral => IsPromoted ? "^S^" : " S ", + WhichPiece.Bishop => IsPromoted ? "^B^" : " B ", + WhichPiece.Rook => IsPromoted ? "^R^" : " R ", + WhichPiece.Knight => IsPromoted ? "^k^" : " k ", + WhichPiece.Lance => IsPromoted ? "^L^" : " L ", + WhichPiece.Pawn => IsPromoted ? "^P^" : " P ", + _ => " ? ", + }; + + public void ToggleOwnership() + { + Owner = Owner == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; + } + + public void Promote() => IsPromoted = true; + + public void Demote() => IsPromoted = false; + + public void Capture() + { + ToggleOwnership(); + Demote(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Position.cs b/Gameboard.ShogiUI.BoardState/Position.cs new file mode 100644 index 0000000..71552d7 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Position.cs @@ -0,0 +1,35 @@ +using System; + +namespace Gameboard.ShogiUI.BoardState +{ + public class Position + { + private int x; + private int y; + + public int X + { + get => x; + set { + if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); + x = value; + } + } + + public int Y + { + get => y; + set + { + if (value > 8 || value < 0) throw new ArgumentOutOfRangeException(); + y = value; + } + } + + public Position(int x, int y) + { + X = x; + Y = y; + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs new file mode 100644 index 0000000..0d45761 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Gameboard.ShogiUI.BoardState +{ + /// + /// Facilitates Shogi board state transitions, cognisant of Shogi rules. + /// The board is always from Player1's perspective. + /// [0,0] is the lower-left position, [8,8] is the higher-right position + /// + public class ShogiBoard + { + private delegate void MoveSetCallback(Piece piece, BoardVector position); + private ShogiBoard validationBoard; + + public IReadOnlyDictionary> Hands { get; } + public Piece[,] Board { get; } + public List MoveHistory { get; } + public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; + public WhichPlayer? InCheck { get; private set; } + public bool IsCheckmate { get; private set; } + + public ShogiBoard() + { + Board = new Piece[9, 9]; + MoveHistory = new List(20); + Hands = new Dictionary> { + { WhichPlayer.Player1, new List()}, + { WhichPlayer.Player2, new List()}, + }; + InitializeBoardState(); + } + public ShogiBoard(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!TryMove(moves[i])) + { + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + } + } + } + + /// + /// Attempts a given move. Returns false if the move is illegal. + /// + public bool TryMove(Move move) + { + // Try making the move in a "throw away" board. + if (validationBoard == null) + { + validationBoard = new ShogiBoard(MoveHistory); + } + var isValid = move.PieceFromCaptured.HasValue + ? validationBoard.PlaceFromHand(move) + : validationBoard.PlaceFromBoard(move); + if (!isValid) + { + // Invalidate the "throw away" board. + validationBoard = null; + return false; + } + // Assert that this move does not put the moving player in check. + if (validationBoard.EvaluateCheck(WhoseTurn)) return false; + + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + // The move is valid and legal; update board state. + if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); + else PlaceFromBoard(move); + + // Evaluate check + InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; + if (InCheck.HasValue) + { + //IsCheckmate = EvaluateCheckmate(); + } + return true; + } + + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; + + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, x, y) => // For each piece... + { + if (!isCheckmate) return; // Short circuit + + var from = new BoardVector(x, y); + if (piece.Owner == InCheck) // Owned by the player in check... + { + var positionsToCheck = new List(10); + IterateMoveSet(from, (innerPiece, position) => + { + if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... + }); + + // And evaluate if any move gets the player out of check. + foreach (var position in positionsToCheck) + { + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + if (moveSuccess) + { + Console.WriteLine($"Not check mate"); + isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); + validationBoard = null; + } + } + } + }); + return isCheckmate; + } + /// True if the move was successful. + private bool PlaceFromHand(Move move) + { + if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move + var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); + if (index < 0) return false; // Invalid move + if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. + + var minimumY = 0; + switch (move.PieceFromCaptured.Value) + { + case WhichPiece.Knight: + // Knight cannot be placed onto the farthest two ranks from the hand. + minimumY = WhoseTurn == WhichPlayer.Player1 ? 2 : 6; + break; + case WhichPiece.Lance: + case WhichPiece.Pawn: + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + minimumY = WhoseTurn == WhichPlayer.Player1 ? 1 : 7; + break; + } + if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; + if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; + + // Mutate the board. + Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; + Hands[WhoseTurn].RemoveAt(index); + + return true; + } + /// True if the move was successful. + private bool PlaceFromBoard(Move move) + { + var fromPiece = Board[move.From.X, move.From.Y]; + if (fromPiece == null) return false; // Invalid move + if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. + if (ValidateMoveAgainstMoveSet(move) == false) return false; // Invalid move; move not part of move-set. + + var captured = Board[move.To.X, move.To.Y]; + if (captured != null) + { + if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. + captured.Capture(); + Hands[captured.Owner].Add(captured); + } + + //Mutate the board. + if (move.IsPromotion) + { + if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Y > 5)) + { + fromPiece.Promote(); + } + else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Y < 3)) + { + fromPiece.Promote(); + } + } + Board[move.To.X, move.To.Y] = fromPiece; + Board[move.From.X, move.From.Y] = null; + MoveHistory.Add(move); + return true; + } + public void PrintStateAsAscii() + { + var builder = new StringBuilder(); + builder.Append(" Player 2"); + builder.AppendLine(); + for (var y = 8; y > -1; y--) + { + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append('|'); + for (var x = 0; x < 9; x++) + { + var piece = Board[x, y]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", piece.ShortName); + } + builder.Append('|'); + } + builder.AppendLine(); + } + builder.Append("- "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" Player 1"); + Console.WriteLine(builder.ToString()); + } + #region Rules Validation + /// + /// Evaluate if a player is in check given the current board state. + /// + private bool EvaluateCheck(WhichPlayer whichPlayer) + { + var inCheck = false; + // Iterate every board piece... + Board.ForEachNotNull((piece, x, y) => + { + // ...that belongs to the opponent... + if (piece.Owner != whichPlayer) + { + IterateMoveSet(new BoardVector(x, y), (threatenedPiece, position) => + { + // ...and threatens the player's king. + inCheck |= + threatenedPiece?.WhichPiece == WhichPiece.King + && threatenedPiece?.Owner == whichPlayer; + }); + } + }); + return inCheck; + } + private bool EvaluateCheck2(WhichPlayer whichPlayer) + { + var inCheck = false; + MoveSetCallback checkKingThreat = (piece, position) => + { + inCheck |= + piece?.WhichPiece == WhichPiece.King + && piece?.Owner == whichPlayer; + }; + // Find interesting pieces + var longRangePiecePositions = new List(8); + Board.ForEachNotNull((piece, x, y) => + { + if (piece.Owner != whichPlayer) + { + switch (piece.WhichPiece) + { + case WhichPiece.Bishop: + case WhichPiece.Rook: + longRangePiecePositions.Add(new BoardVector(x, y)); + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) longRangePiecePositions.Add(new BoardVector(x, y)); + break; + } + } + }); + + foreach(var position in longRangePiecePositions) + { + IterateMoveSet(position, checkKingThreat); + } + + return inCheck; + } + private bool ValidateMoveAgainstMoveSet(Move move) + { + var isValid = false; + IterateMoveSet(move.From, (piece, position) => + { + if (piece?.Owner != WhoseTurn && position == move.To) + { + isValid = true; + } + }); + + return isValid; + } + /// + /// Iterate through the possible moves of a piece at a given position. + /// + private void IterateMoveSet(BoardVector from, MoveSetCallback callback) + { + // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. + // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. + var piece = Board[from.X, from.Y]; + switch (piece.WhichPiece) + { + case WhichPiece.King: + IterateKingMoveSet(from, callback); + break; + case WhichPiece.GoldenGeneral: + IterateGoldenGeneralMoveSet(from, callback); + break; + case WhichPiece.SilverGeneral: + IterateSilverGeneralMoveSet(from, callback); + break; + case WhichPiece.Bishop: + IterateBishopMoveSet(from, callback); + break; + case WhichPiece.Rook: + IterateRookMoveSet(from, callback); + break; + case WhichPiece.Knight: + IterateKnightMoveSet(from, callback); + break; + case WhichPiece.Lance: + IterateLanceMoveSet(from, callback); + break; + case WhichPiece.Pawn: + IteratePawnMoveSet(from, callback); + break; + } + } + private void IterateKingMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.Down, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + } + private void IterateGoldenGeneralMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.Down, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + } + private void IterateSilverGeneralMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + } + } + private void IterateBishopMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.UpLeft, callback); + BoardWalk(from, direction.UpRight, callback); + BoardWalk(from, direction.DownLeft, callback); + BoardWalk(from, direction.DownRight, callback); + if (piece.IsPromoted) + { + BoardStep(from, direction.Up, callback); + BoardStep(from, direction.Left, callback); + BoardStep(from, direction.Right, callback); + BoardStep(from, direction.Down, callback); + } + } + private void IterateRookMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.Up, callback); + BoardWalk(from, direction.Left, callback); + BoardWalk(from, direction.Right, callback); + BoardWalk(from, direction.Down, callback); + if (piece.IsPromoted) + { + BoardStep(from, direction.UpLeft, callback); + BoardStep(from, direction.UpRight, callback); + BoardStep(from, direction.DownLeft, callback); + BoardStep(from, direction.DownRight, callback); + } + } + private void IterateKnightMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardStep(from, direction.KnightLeft, callback); + BoardStep(from, direction.KnightRight, callback); + } + } + private void IterateLanceMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardWalk(from, direction.Up, callback); + } + } + private void IteratePawnMoveSet(BoardVector from, MoveSetCallback callback) + { + var piece = Board[from.X, from.Y]; + if (piece?.WhichPiece == WhichPiece.Pawn) + { + if (piece.IsPromoted) + { + IterateGoldenGeneralMoveSet(from, callback); + } + else + { + var direction = new Direction(piece.Owner); + BoardStep(from, direction.Up, callback); + } + } + } + /// + /// Useful for iterating the board for pieces that move many spaces. + /// + /// A function that returns true if walking should continue. + private void BoardWalk(BoardVector from, BoardVector direction, MoveSetCallback callback) + { + var foundAnotherPiece = false; + var to = from.Add(direction); + while (to.IsValidBoardPosition && !foundAnotherPiece) + { + var piece = Board[to.X, to.Y]; + callback(piece, to); + to = to.Add(direction); + foundAnotherPiece = piece != null; + } + } + + /// + /// Useful for iterating the board for pieces that move only one space. + /// + private void BoardStep(BoardVector from, BoardVector direction, MoveSetCallback callback) + { + var to = from.Add(direction); + if (to.IsValidBoardPosition) + { + callback(Board[to.X, to.Y], to); + } + } + #endregion + + #region Initialize + private void ResetEmptyRows() + { + for (int y = 3; y < 6; y++) + for (int x = 0; x < 9; x++) + Board[x, y] = null; + } + private void ResetFrontRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 2 : 6; + for (int x = 0; x < 9; x++) Board[x, y] = new Piece(WhichPiece.Pawn, player); + } + private void ResetMiddleRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 1 : 7; + + Board[0, y] = null; + for (int x = 2; x < 7; x++) Board[x, y] = null; + Board[8, y] = null; + if (player == WhichPlayer.Player1) + { + Board[1, y] = new Piece(WhichPiece.Bishop, player); + Board[7, y] = new Piece(WhichPiece.Rook, player); + } + else + { + Board[1, y] = new Piece(WhichPiece.Rook, player); + Board[7, y] = new Piece(WhichPiece.Bishop, player); + } + } + private void ResetRearRow(WhichPlayer player) + { + int y = player == WhichPlayer.Player1 ? 0 : 8; + + Board[0, y] = new Piece(WhichPiece.Lance, player); + Board[1, y] = new Piece(WhichPiece.Knight, player); + Board[2, y] = new Piece(WhichPiece.SilverGeneral, player); + Board[3, y] = new Piece(WhichPiece.GoldenGeneral, player); + Board[4, y] = new Piece(WhichPiece.King, player); + Board[5, y] = new Piece(WhichPiece.GoldenGeneral, player); + Board[6, y] = new Piece(WhichPiece.SilverGeneral, player); + Board[7, y] = new Piece(WhichPiece.Knight, player); + Board[8, y] = new Piece(WhichPiece.Lance, player); + } + private void InitializeBoardState() + { + ResetRearRow(WhichPlayer.Player1); + ResetMiddleRow(WhichPlayer.Player1); + ResetFrontRow(WhichPlayer.Player1); + ResetEmptyRows(); + ResetFrontRow(WhichPlayer.Player2); + ResetMiddleRow(WhichPlayer.Player2); + ResetRearRow(WhichPlayer.Player2); + } + #endregion + } +} diff --git a/Gameboard.ShogiUI.BoardState/WhichPiece.cs b/Gameboard.ShogiUI.BoardState/WhichPiece.cs new file mode 100644 index 0000000..a0dd88c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public enum WhichPiece + { + King, + GoldenGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Gameboard.ShogiUI.BoardState/WhichPlayer.cs b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs new file mode 100644 index 0000000..1e8de13 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Gameboard.ShogiUI.BoardState +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs index 1f6541f..c45d51c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -13,7 +13,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; private set; } public string Error { get; set; } - public IEnumerable Games { get; set; } + public ICollection Games { get; set; } public ListGamesResponse(ClientAction action) { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs index 19e6c08..c457791 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages { public string Action { get; private set; } public Game Game { get; set; } - public IEnumerable Moves { get; set; } + public IReadOnlyList Moves { get; set; } public string Error { get; set; } public LoadGameResponse(ClientAction action) diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs new file mode 100644 index 0000000..b42c398 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/BoardState.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public class BoardState + { + public Piece[,] Board { get; set; } + public IReadOnlyCollection Player1Hand { get; set; } + public IReadOnlyCollection Player2Hand { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs index a4a2ebe..3f5ddc6 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -1,8 +1,13 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +using System.Collections.Generic; + +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types { - public class Game - { - public string GameName { get; set; } - public string[] Players { get; set; } - } + public class Game + { + public string GameName { get; set; } + /// + /// Players[0] is the session owner, Players[1] is the other guy + /// + public IReadOnlyList Players { get; set; } + } } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs new file mode 100644 index 0000000..bb5ef62 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Piece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public class Piece + { + public WhichPiece WhichPiece { get; set; } + + /// + /// True if this piece is controlled by you. + /// + public bool IsControlledByMe { get; set; } + + public bool IsPromoted { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs new file mode 100644 index 0000000..b83e22e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types +{ + public enum WhichPiece + { + King, + GoldGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index adfe2f0..38ccb7d 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -9,7 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Sockets.UnitTests", "Gameboard.ShogiUI.Sockets.UnitTests\Gameboard.ShogiUI.Sockets.UnitTests.csproj", "{8D753AD0-0985-415C-80B3-CCADF3AE1DF9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.BoardState", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.BoardState.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,16 +29,24 @@ Global {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9}.Release|Any CPU.Build.0 = Release|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.Build.0 = Release|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.Build.0 = Release|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {8D753AD0-0985-415C-80B3-CCADF3AE1DF9} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index 038f52e..50c2ac9 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -15,6 +15,7 @@ + diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user deleted file mode 100644 index 2adf92b..0000000 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user +++ /dev/null @@ -1,12 +0,0 @@ - - - - ApiControllerEmptyScaffolder - root/Controller - AspShogiSockets - false - - - ProjectDebugger - - \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs index 49f21a4..494c738 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/BoardManager.cs @@ -1,6 +1,35 @@ -namespace Gameboard.ShogiUI.Sockets.Managers +using Gameboard.ShogiUI.BoardState; +using System.Collections.Concurrent; + +namespace Gameboard.ShogiUI.Sockets.Managers { - public class BoardManager + public interface IBoardManager { + void Add(string sessionName, ShogiBoard board); + ShogiBoard Get(string sessionName); + } + + public class BoardManager : IBoardManager + { + private readonly ConcurrentDictionary Boards; + + public BoardManager() + { + Boards = new ConcurrentDictionary(); + } + + public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); + + public ShogiBoard Get(string sessionName) + { + if (Boards.TryGetValue(sessionName, out var board)) + return board; + return null; + } + + public string GetBoardState() + { + return string.Empty; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs index 4b72412..d8244a7 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -1,15 +1,15 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + // TODO: This doesn't need to be a socket action. + // It can be an API route and still tell socket connections about the new session. public class CreateGameHandler : IActionHandler { private readonly ILogger logger; @@ -26,13 +26,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var postSessionResponse = await repository.PostSession(new PostSession { SessionName = request.GameName, - PlayerName = userName, // TODO : Investigate if needed by UI + PlayerName = userName, IsPrivate = request.IsPrivate }); @@ -53,9 +53,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (request.IsPrivate) { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } else { diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs index 5168598..20f98ab 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs @@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers /// /// Responsible for parsing json and handling the request. /// - Task Handle(WebSocket socket, string json, string userName); + Task Handle(string json, string userName); } public delegate IActionHandler ActionHandlerResolver(ClientAction action); diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs index c4f2869..f8edb5e 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -1,11 +1,9 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -26,7 +24,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession @@ -37,37 +35,32 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers if (joinGameResponse.JoinSucceeded) { - var gameName = (await repository.GetGame(joinGameResponse.SessionName)).Session.Name; - // Other members of the game see a regular JoinGame occur. var response = new JoinGameResponse(ClientAction.JoinGame) { PlayerName = userName, - GameName = gameName + GameName = joinGameResponse.SessionName }; - // At this time, userName hasn't subscribed and won't receive this broadcasted messages. - await communicationManager.BroadcastToGame(gameName, response); + // At this time, userName hasn't subscribed and won't receive this message. + await communicationManager.BroadcastToGame(joinGameResponse.SessionName, response); - // But the player joining sees the JoinByCode occur. + // The player joining sees the JoinByCode occur. response = new JoinGameResponse(ClientAction.JoinByCode) { PlayerName = userName, - GameName = gameName + GameName = joinGameResponse.SessionName }; - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } else { var response = new JoinGameResponse(ClientAction.JoinByCode) { PlayerName = userName, + GameName = joinGameResponse.SessionName, Error = "Error joining game." }; - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs index 96e1ed7..c00aa64 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -3,7 +3,6 @@ using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Newtonsoft.Json; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers @@ -20,13 +19,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var response = new JoinGameResponse(ClientAction.JoinGame) - { - PlayerName = userName - }; var joinGameResponse = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession { @@ -34,15 +29,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers SessionName = request.GameName }); + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName, + GameName = request.GameName + }; if (joinGameResponse.JoinSucceeded) { - response.GameName = request.GameName; + await communicationManager.BroadcastToAll(response); } else { response.Error = "Game is full."; + await communicationManager.BroadcastToPlayers(response, userName); } - await communicationManager.BroadcastToAll(response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs index 43431a0..d4379e3 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -1,26 +1,29 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Newtonsoft.Json; using System.Linq; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + // TODO: This doesn't need to be a socket action. + // It can be an HTTP route. public class ListGamesHandler : IActionHandler { + private readonly ISocketCommunicationManager communicationManager; private readonly IGameboardRepository repository; public ListGamesHandler( + ISocketCommunicationManager communicationManager, IGameboardRepository repository) { + this.communicationManager = communicationManager; this.repository = repository; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); var getGamesResponse = string.IsNullOrWhiteSpace(userName) @@ -33,11 +36,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers var response = new ListGamesResponse(ClientAction.ListGames) { - Games = games + Games = games.ToList() }; - var serialized = JsonConvert.SerializeObject(response); - await socket.SendTextAsync(serialized); + await communicationManager.BroadcastToPlayers(response, userName); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index b0fac4f..db15bfa 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -1,56 +1,66 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.BoardState; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Linq; -using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { + /// + /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. + /// public class LoadGameHandler : IActionHandler { private readonly ILogger logger; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; + private readonly IBoardManager boardManager; public LoadGameHandler( ILogger logger, ISocketCommunicationManager communicationManager, - IGameboardRepository gameboardRepository) + IGameboardRepository gameboardRepository, + IBoardManager boardManager) { this.logger = logger; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; + this.boardManager = boardManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); - var getGameResponse = await gameboardRepository.GetGame(request.GameName); - var getMovesResponse = await gameboardRepository.GetMoves(request.GameName); + var gameTask = gameboardRepository.GetGame(request.GameName); + var moveTask = gameboardRepository.GetMoves(request.GameName); - var response = new LoadGameResponse(ClientAction.LoadGame); + var getGameResponse = await gameTask; + var getMovesResponse = await moveTask; if (getGameResponse == null || getMovesResponse == null) { - response.Error = $"Could not find game."; + logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); + var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; + await communicationManager.BroadcastToPlayers(response, userName); } else { - var sessionModel = new Session(getGameResponse.Session); - communicationManager.SubscribeToGame(socket, sessionModel, userName); + var sessionModel = new Models.Session(getGameResponse.Session); + var moveModels = getMovesResponse.Moves.Select(_ => new Models.Move(_)).ToList(); - response.Game = sessionModel.ToServiceModel(); - response.Moves = getMovesResponse.Moves.Select(_ => Mapper.Map(_).ToServiceModel()); + communicationManager.SubscribeToGame(sessionModel, userName); + var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); + boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); + + var response = new LoadGameResponse(ClientAction.LoadGame) + { + Game = sessionModel.ToServiceModel(), + Moves = moveModels.Select(_ => _.ToServiceModel()).ToList(), + }; + await communicationManager.BroadcastToPlayers(response, userName); } - - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", userName, serialized); - await socket.SendTextAsync(serialized); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs index fdc535b..5fe8a11 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -1,45 +1,47 @@ using Gameboard.Shogi.Api.ServiceModels.Messages; -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers.Utility; -using Gameboard.ShogiUI.Sockets.Repositories; -using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Newtonsoft.Json; -using System.Net.WebSockets; -using System.Threading.Tasks; using Gameboard.ShogiUI.Sockets.Models; +using Gameboard.ShogiUI.Sockets.Repositories; +using Newtonsoft.Json; +using System.Threading.Tasks; +using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket; namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers { public class MoveHandler : IActionHandler { + private readonly IBoardManager boardManager; private readonly IGameboardRepository gameboardRepository; private readonly ISocketCommunicationManager communicationManager; public MoveHandler( + IBoardManager boardManager, ISocketCommunicationManager communicationManager, IGameboardRepository gameboardRepository) { + this.boardManager = boardManager; this.gameboardRepository = gameboardRepository; this.communicationManager = communicationManager; } - public async Task Handle(WebSocket socket, string json, string userName) + public async Task Handle(string json, string userName) { var request = JsonConvert.DeserializeObject(json); // Basic move validation if (request.Move.To.Equals(request.Move.From)) { - var serialized = JsonConvert.SerializeObject( - new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) - { - Error = "Error: moving piece from tile to the same tile." - }); - await socket.SendTextAsync(serialized); + var error = new Service.Messages.ErrorResponse(Service.Types.ClientAction.Move) + { + Error = "Error: moving piece from tile to the same tile." + }; + await communicationManager.BroadcastToPlayers(error, userName); return; } var moveModel = new Move(request.Move); - var session = (await gameboardRepository.GetGame(request.GameName)).Session; - await gameboardRepository.PostMove(request.GameName, new PostMove(Mapper.Map(moveModel))); + var board = boardManager.Get(request.GameName); + var boardMove = moveModel.ToBoardModel(); + //board.Move() + await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); + var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) { @@ -47,7 +49,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers PlayerName = userName, Move = moveModel.ToServiceModel() }; - await communicationManager.BroadcastToGame(session.Name, response); + await communicationManager.BroadcastToGame(request.GameName, response); } } } diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs index 6923621..3269493 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -21,10 +21,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers Task BroadcastToAll(IResponse response); Task BroadcastToGame(string gameName, IResponse response); Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); - void SubscribeToGame(WebSocket socket, Session session, string playerName); + void SubscribeToGame(Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromGame(string gameName, string playerName); + Task BroadcastToPlayers(IResponse response, params string[] playerNames); } public class SocketCommunicationManager : ISocketCommunicationManager @@ -65,7 +66,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers else { var handler = handlerResolver(request.Action); - await handler.Handle(socket, message, userName); + await handler.Handle(message, userName); } } catch (OperationCanceledException ex) @@ -100,7 +101,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers /// /// Unsubscribes the player from their current game, then subscribes to the new game. /// - public void SubscribeToGame(WebSocket socket, Session session, string playerName) + public void SubscribeToGame(Session session, string playerName) { // Unsubscribe from any other games foreach (var kvp in sessions) @@ -110,8 +111,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers } // Subscribe - var s = sessions.GetOrAdd(session.Name, session); - s.Subscriptions.TryAdd(playerName, socket); + if (connections.TryGetValue(playerName, out var socket)) + { + var s = sessions.GetOrAdd(session.Name, session); + s.Subscriptions.TryAdd(playerName, socket); + } } public void UnsubscribeFromGame(string gameName, string playerName) @@ -123,6 +127,21 @@ namespace Gameboard.ShogiUI.Sockets.Managers } } + public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (connections.TryGetValue(name, out var socket)) + { + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); + + } + } + await Task.WhenAll(tasks); + } public Task BroadcastToAll(IResponse response) { var message = JsonConvert.SerializeObject(response); diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs deleted file mode 100644 index ea59d0e..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Models; -using Microsoft.FSharp.Core; -using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Managers.Utility -{ - public static class Mapper - { - public static ShogiApi.Move Map(Move source) - { - var from = source.From; - var to = source.To; - FSharpOption pieceFromCaptured = source.PieceFromCaptured switch - { - "B" => new FSharpOption(ShogiApi.WhichPieceName.Bishop), - "G" => new FSharpOption(ShogiApi.WhichPieceName.GoldenGeneral), - "K" => new FSharpOption(ShogiApi.WhichPieceName.King), - "k" => new FSharpOption(ShogiApi.WhichPieceName.Knight), - "L" => new FSharpOption(ShogiApi.WhichPieceName.Lance), - "P" => new FSharpOption(ShogiApi.WhichPieceName.Pawn), - "R" => new FSharpOption(ShogiApi.WhichPieceName.Rook), - "S" => new FSharpOption(ShogiApi.WhichPieceName.SilverGeneral), - _ => null - }; - var target = new ShogiApi.Move - { - Origin = new ShogiApi.BoardLocation { X = from.X, Y = from.Y }, - Destination = new ShogiApi.BoardLocation { X = to.X, Y = to.Y }, - IsPromotion = source.IsPromotion, - PieceFromCaptured = pieceFromCaptured - }; - return target; - } - - public static Move Map(ShogiApi.Move source) - { - var origin = source.Origin; - var destination = source.Destination; - string pieceFromCaptured = null; - if (source.PieceFromCaptured != null) - { - pieceFromCaptured = source.PieceFromCaptured.Value switch - { - ShogiApi.WhichPieceName.Bishop => "B", - ShogiApi.WhichPieceName.GoldenGeneral => "G", - ShogiApi.WhichPieceName.King => "K", - ShogiApi.WhichPieceName.Knight => "k", - ShogiApi.WhichPieceName.Lance => "L", - ShogiApi.WhichPieceName.Pawn => "P", - ShogiApi.WhichPieceName.Rook => "R", - ShogiApi.WhichPieceName.SilverGeneral => "S", - _ => "" - }; - } - - var target = new Move - { - From = new Coords(origin.X, origin.Y), - To = new Coords(destination.X, destination.Y), - IsPromotion = source.IsPromotion, - PieceFromCaptured = pieceFromCaptured - }; - - return target; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs index 932285a..fcb7274 100644 --- a/Gameboard.ShogiUI.Sockets/Models/Move.cs +++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs @@ -1,4 +1,9 @@ -namespace Gameboard.ShogiUI.Sockets.Models +using Gameboard.ShogiUI.BoardState; +using Microsoft.FSharp.Core; +using System; +using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types; + +namespace Gameboard.ShogiUI.Sockets.Models { public class Move { @@ -15,7 +20,29 @@ PieceFromCaptured = move.PieceFromCaptured; IsPromotion = move.IsPromotion; } - + public Move(ShogiApi.Move move) + { + string pieceFromCaptured = null; + if (move.PieceFromCaptured != null) + { + pieceFromCaptured = move.PieceFromCaptured.Value switch + { + ShogiApi.WhichPieceName.Bishop => "", + ShogiApi.WhichPieceName.GoldenGeneral => "G", + ShogiApi.WhichPieceName.King => "K", + ShogiApi.WhichPieceName.Knight => "k", + ShogiApi.WhichPieceName.Lance => "L", + ShogiApi.WhichPieceName.Pawn => "P", + ShogiApi.WhichPieceName.Rook => "R", + ShogiApi.WhichPieceName.SilverGeneral => "S", + _ => "" + }; + } + From = new Coords(move.Origin.X, move.Origin.Y); + To = new Coords(move.Destination.X, move.Destination.Y); + IsPromotion = move.IsPromotion; + PieceFromCaptured = pieceFromCaptured; + } public ServiceModels.Socket.Types.Move ToServiceModel() => new ServiceModels.Socket.Types.Move { From = From.ToBoardNotation(), @@ -23,5 +50,38 @@ PieceFromCaptured = PieceFromCaptured, To = To.ToBoardNotation() }; + public ShogiApi.Move ToApiModel() + { + var pieceFromCaptured = PieceFromCaptured switch + { + "B" => new FSharpOption(ShogiApi.WhichPieceName.Bishop), + "G" => new FSharpOption(ShogiApi.WhichPieceName.GoldenGeneral), + "K" => new FSharpOption(ShogiApi.WhichPieceName.King), + "k" => new FSharpOption(ShogiApi.WhichPieceName.Knight), + "L" => new FSharpOption(ShogiApi.WhichPieceName.Lance), + "P" => new FSharpOption(ShogiApi.WhichPieceName.Pawn), + "R" => new FSharpOption(ShogiApi.WhichPieceName.Rook), + "S" => new FSharpOption(ShogiApi.WhichPieceName.SilverGeneral), + _ => null + }; + var target = new ShogiApi.Move + { + Origin = new ShogiApi.BoardLocation { X = From.X, Y = From.Y }, + Destination = new ShogiApi.BoardLocation { X = To.X, Y = To.Y }, + IsPromotion = IsPromotion, + PieceFromCaptured = pieceFromCaptured + }; + return target; + } + public BoardState.Move ToBoardModel() + { + return new BoardState.Move + { + From = new BoardVector(From.X, From.Y), + IsPromotion = IsPromotion, + PieceFromCaptured = Enum.TryParse(PieceFromCaptured, out var whichPiece) ? whichPiece : null, + To = new BoardVector(To.X, To.Y) + }; + } } } diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 7d32044..09805be 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -45,6 +45,7 @@ namespace Gameboard.ShogiUI.Sockets services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.AddSingleton(sp => action => { return action switch diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs new file mode 100644 index 0000000..85d9b99 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/BoardState/BoardVectorShould.cs @@ -0,0 +1,26 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Gameboard.ShogiUI.UnitTests.BoardState +{ + [TestClass] + public class BoardVectorShould + { + [TestMethod] + public void BeEqualWhenPropertiesAreEqual() + { + var a = new BoardVector(3, 2); + var b = new BoardVector(3, 2); + a.Should().Be(b); + a.GetHashCode().Should().Be(b.GetHashCode()); + (a == b).Should().BeTrue(); + + // Properties should not be transitively equal. + b = new BoardVector(2, 3); + a.Should().NotBe(b); + a.GetHashCode().Should().NotBe(b.GetHashCode()); + (a == b).Should().BeFalse(); + } + } +} diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs new file mode 100644 index 0000000..8d4a22d --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -0,0 +1,322 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +namespace Gameboard.ShogiUI.UnitTests.BoardState +{ + [TestClass] + public class ShogiBoardShould + { + [TestMethod] + public void InitializeBoardState() + { + // Assert + var board = new ShogiBoard().Board; + // Assert pieces do not start promoted. + foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); + + // Assert Player1. + for (var y = 0; y < 3; y++) + for (var x = 0; x < 9; x++) + board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); + board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); + board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[4, 0].WhichPiece.Should().Be(WhichPiece.King); + board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); + board[0, 1].Should().BeNull(); + board[1, 1].WhichPiece.Should().Be(WhichPiece.Bishop); + for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); + board[7, 1].WhichPiece.Should().Be(WhichPiece.Rook); + board[8, 1].Should().BeNull(); + for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + + // Assert empty locations. + for (var y = 3; y < 6; y++) + for (var x = 0; x < 9; x++) + board[x, y].Should().BeNull(); + + // Assert Player2. + for (var y = 6; y < 9; y++) + for (var x = 0; x < 9; x++) + board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); + board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[4, 8].WhichPiece.Should().Be(WhichPiece.King); + board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); + board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); + board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); + board[0, 7].Should().BeNull(); + board[1, 7].WhichPiece.Should().Be(WhichPiece.Rook); + for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); + board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + board[8, 7].Should().BeNull(); + for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void InitializeBoardStateWithMoves() + { + var moves = new[] + { + new Move + { + // Pawn + From = new BoardVector(0, 2), + To = new BoardVector(0, 3) + } + }; + var shogi = new ShogiBoard(moves); + shogi.Board[0, 2].Should().BeNull(); + shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = new ShogiBoard(); + + // Act - P1 "moves" pawn to the position it already exists at. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(0, 2), To = new BoardVector(0, 2) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_MoveSet() + { + var invalidLanceMove = new Move + { + // Lance moving adjacent + From = new BoardVector(0, 0), + To = new BoardVector(1, 5) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidLanceMove); + + moveSuccess.Should().BeFalse(); + // Assert the Lance has not actually moved. + shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + } + + [TestMethod] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = new ShogiBoard(); + + // Act - Move Player2 Pawn when it's Player1 turn. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 6), To = new BoardVector(8, 5) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.Board[8, 6].WhichPiece.Should().Be(WhichPiece.Pawn); + shogi.Board[8, 5].Should().BeNull(); + } + + [TestMethod] + public void PreventInvalidMoves_MoveThroughAllies() + { + var invalidLanceMove = new Move + { + // Lance moving through the pawn before it. + From = new BoardVector(0, 0), + To = new BoardVector(0, 5) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidLanceMove); + + moveSuccess.Should().BeFalse(); + // Assert the Lance has not actually moved. + shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); + } + + [TestMethod] + public void PreventInvalidMoves_CaptureAlly() + { + var invalidKnightMove = new Move + { + // Knight capturing allied Pawn + From = new BoardVector(1, 0), + To = new BoardVector(0, 2) + }; + + var shogi = new ShogiBoard(); + var moveSuccess = shogi.TryMove(invalidKnightMove); + + moveSuccess.Should().BeFalse(); + // Assert the Knight has not actually moved or captured. + shogi.Board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); + shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); + } + + [TestMethod] + public void PreventInvalidMoves_Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + // P1 Bishop puts P2 in check + new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) } + }; + var shogi = new ShogiBoard(moves); + + // Prerequisit + shogi.InCheck.Should().Be(WhichPlayer.Player2); + + + // Act - P2 moves Lance while remaining in check. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(8, 8), To = new BoardVector(8, 7) }); + + // Assert + moveSuccess.Should().BeFalse(); + shogi.InCheck.Should().Be(WhichPlayer.Player2); + shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); + shogi.Board[8, 7].Should().BeNull(); + } + + [TestMethod] + public void Check() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) }, + }; + var shogi = new ShogiBoard(moves); + + + // Act - P1 Bishop, check + shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6) }); + + // Assert + shogi.InCheck.Should().Be(WhichPlayer.Player2); + } + + [TestMethod] + public void Capture() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 Bishop captures P2 Bishop + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(7, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board + .Cast() + .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) + .Should() + .Be(1); + shogi.Board[1, 1].Should().BeNull(); + shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Hands[WhichPlayer.Player1] + .Should() + .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); + + + // Act - P2 Silver captures P1 Bishop + moveSuccess = shogi.TryMove(new Move { From = new BoardVector(6, 8), To = new BoardVector(7, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board[6, 8].Should().BeNull(); + shogi.Board[7, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral); + shogi.Board + .Cast() + .Count(piece => piece?.WhichPiece == WhichPiece.Bishop) + .Should().Be(0); + shogi.Hands[WhichPlayer.Player2] + .Should() + .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player2); + } + + [TestMethod] + public void Promote() + { + // Arrange + var moves = new[] + { + // P1 Pawn + new Move { From = new BoardVector(2, 2), To = new BoardVector(2, 3) }, + // P2 Pawn + new Move { From = new BoardVector(6, 6), To = new BoardVector(6, 5) } + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 moves across promote threshold. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(1, 1), To = new BoardVector(6, 6), IsPromotion = true }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.Board[1, 1].Should().BeNull(); + shogi.Board[6, 6].Should().Match(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); + } + + [TestMethod] + public void CheckMate() + { + // Arrange + var moves = new[] + { + // P1 Rook + new Move { From = new BoardVector(7, 1), To = new BoardVector(4, 1) }, + // P2 Gold + new Move { From = new BoardVector(3, 8), To = new BoardVector(2, 7) }, + // P1 Pawn + new Move { From = new BoardVector(4, 2), To = new BoardVector(4, 3) }, + // P2 other Gold + new Move { From = new BoardVector(5, 8), To = new BoardVector(6, 7) }, + // P1 same Pawn + new Move { From = new BoardVector(4, 3), To = new BoardVector(4, 4) }, + // P2 Pawn + new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 5) }, + // P1 Pawn takes P2 Pawn + new Move { From = new BoardVector(4, 4), To = new BoardVector(4, 5) }, + // P2 King + new Move { From = new BoardVector(4, 8), To = new BoardVector(4, 7) }, + // P1 Pawn promotes + new Move { From = new BoardVector(4, 5), To = new BoardVector(4, 6), IsPromotion = true }, + // P2 King retreat + new Move { From = new BoardVector(4, 7), To = new BoardVector(4, 8) }, + }; + var shogi = new ShogiBoard(moves); + + // Act - P1 Pawn wins by checkmate. + var moveSuccess = shogi.TryMove(new Move { From = new BoardVector(4, 6), To = new BoardVector(4, 7) }); + + // Assert + moveSuccess.Should().BeTrue(); + shogi.IsCheckmate.Should().BeTrue(); + + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj similarity index 100% rename from Gameboard.ShogiUI.Sockets.UnitTests/Gameboard.ShogiUI.Sockets.UnitTests.csproj rename to Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj diff --git a/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs similarity index 91% rename from Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs rename to Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs index d0c1961..f878da3 100644 --- a/Gameboard.ShogiUI.Sockets.UnitTests/Models/CoordsModelShould.cs +++ b/Gameboard.ShogiUI.UnitTests/Sockets/CoordsModelShould.cs @@ -2,7 +2,7 @@ using Gameboard.ShogiUI.Sockets.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Gameboard.ShogiUI.Sockets.UnitTests.Models +namespace Gameboard.ShogiUI.UnitTests.Sockets { [TestClass] public class CoordsModelShould