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