From 3593785421f48c5db35f584b427bbd593af9c834 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 20 Oct 2024 22:27:08 -0500 Subject: [PATCH] Yep --- Shogi.Database/FirstTimeSetup.sql | 6 +- Shogi.Domain/Other/BoardRules.cs | 81 -- Shogi.Domain/Other/IRulesLifecycle.cs | 15 - Shogi.Domain/Other/MoveValidationContext.cs | 14 - Shogi.Domain/Other/RulesLifecycleResult.cs | 5 - Shogi.Domain/ValueObjects/BoardState.cs | 3 +- Shogi.Domain/ValueObjects/GoldGeneral.cs | 20 +- Shogi.Domain/ValueObjects/InCheckResult.cs | 9 + Shogi.Domain/ValueObjects/MoveResult.cs | 14 +- Shogi.Domain/ValueObjects/Piece.cs | 53 +- Shogi.Domain/ValueObjects/ShogiBoard.cs | 416 ++++----- Shogi.Domain/ValueObjects/StandardRules.cs | 2 +- Shogi.Domain/ValueObjects/WhichPiece.cs | 14 +- .../YetToBeAssimilatedIntoDDD/Pathing/Path.cs | 27 +- Shogi.UI/Pages/Play/GameBoard/GameBoard.razor | 1 - .../GameBoard/GameBoardPresentation.razor | 18 - .../GameBoard/GameboardPresentation.razor.css | 18 +- .../Play/GameBoard/SeatedGameBoard.razor | 59 +- .../Play/GameBoard/SeatedGameBoard.razor.css | 14 + Shogi.UI/Pages/Play/PromotePrompt.cs | 58 -- Shogi.UI/Program.cs | 3 +- Shogi.UI/Shared/ShogiApi.cs | 2 +- Tests/UnitTests/Extensions.cs | 97 +++ Tests/UnitTests/NotationShould.cs | 4 +- Tests/UnitTests/RookShould.cs | 358 ++++---- Tests/UnitTests/ShogiBoardStateShould.cs | 2 +- Tests/UnitTests/ShogiShould.cs | 788 +++++++++--------- 27 files changed, 1020 insertions(+), 1081 deletions(-) delete mode 100644 Shogi.Domain/Other/BoardRules.cs delete mode 100644 Shogi.Domain/Other/IRulesLifecycle.cs delete mode 100644 Shogi.Domain/Other/MoveValidationContext.cs delete mode 100644 Shogi.Domain/Other/RulesLifecycleResult.cs create mode 100644 Shogi.Domain/ValueObjects/InCheckResult.cs create mode 100644 Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css delete mode 100644 Shogi.UI/Pages/Play/PromotePrompt.cs create mode 100644 Tests/UnitTests/Extensions.cs diff --git a/Shogi.Database/FirstTimeSetup.sql b/Shogi.Database/FirstTimeSetup.sql index 60b5b76..5dfe63b 100644 --- a/Shogi.Database/FirstTimeSetup.sql +++ b/Shogi.Database/FirstTimeSetup.sql @@ -10,6 +10,8 @@ /** * Local setup instructions, in order: * 1. To setup the Shogi database, use the publish menu option in visual studio with the Shogi.Database project. -* 2. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef -* 2. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update +* +* 2. Setup Entity Framework because that's what the login system uses. +* 2.a. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef +* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update */ \ No newline at end of file diff --git a/Shogi.Domain/Other/BoardRules.cs b/Shogi.Domain/Other/BoardRules.cs deleted file mode 100644 index 8639730..0000000 --- a/Shogi.Domain/Other/BoardRules.cs +++ /dev/null @@ -1,81 +0,0 @@ - -namespace Shogi.Domain.Other; - -/// -/// -/// -/// A 2D array of pieces, representing your board. Indexed as [x, y]. -public class BoardRules(TPiece?[,] boardState) where TPiece : IRulesLifecycle -{ - private readonly Vector2 MaxIndex = new(boardState.GetLength(0) - 1, boardState.GetLength(1) - 1); - - /// - /// Validates a move, invoking the callback which you should implement. - /// A move is considered valid if it could be made legally, ignoring check or check-mate rules. - /// Check and check-mate verifying happens in a different lifecycle callback. - /// - /// The position of the piece being moved. - /// The desired destination of the piece being moved. - /// TODO - /// - public RulesLifecycleResult ValidateMove(Vector2 from, Vector2 to, bool isPromotion) - { - if (IsWithinBounds(from) && IsWithinBounds(to)) - { - var piece = boardState[(int)from.X, (int)from.Y]; - if (piece == null) - { - return new RulesLifecycleResult(IsError: true, $"There is no piece at position {from}."); - } - - return piece.OnMoveValidation(new MoveValidationContext(from, to, isPromotion, boardState)); - } - - return new RulesLifecycleResult(IsError: true, "test message"); - } - - - //foreach (var piece in boardState) - //{ - // if (piece != null) - // { - // var result = piece.OnMoveValidation(new MoveValidationContext(from, to, isPromotion, boardState)); - // if (result.IsError) - // { - // return result; - // } - // } - //} - - //public int ValidateMove(Vector2 start, Vector2 end) - //{ - // if (board.GetLength(0) != boardSize.X || board.GetLength(1) != boardSize.Y) - // { - // throw new ArgumentException($"2D array dimensions must match boardSize given during {nameof(CreateNewRules)} method.", nameof(board)); - // } - // if (start - start != Vector2.Zero) - // { - // throw new ArgumentException("Negative values not allowed.", nameof(start)); - // } - // if (end - end != Vector2.Zero) - // { - // throw new ArgumentException("Negative values not allowed.", nameof(end)); - // } - // if (start.X >= boardSize.X || start.Y >= boardSize.Y) - // { - // throw new ArgumentException("Start position must be within the given boardSize.", nameof(start)); - // } - // if (end.X >= boardSize.X || end.Y >= boardSize.Y) - // { - // throw new ArgumentException("End position must be within the given boardSize.", nameof(end)); - // } - - // return 0; - //} - - private bool IsWithinBounds(Vector2 position) - { - var isPositive = position - position == Vector2.Zero; - return isPositive && position.X <= MaxIndex.X && position.Y <= MaxIndex.Y; - } -} \ No newline at end of file diff --git a/Shogi.Domain/Other/IRulesLifecycle.cs b/Shogi.Domain/Other/IRulesLifecycle.cs deleted file mode 100644 index 4255bcd..0000000 --- a/Shogi.Domain/Other/IRulesLifecycle.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Shogi.Domain.Other; - -public interface IRulesLifecycle where TPiece : IRulesLifecycle -{ - /// - /// Invoked by during the MoveValidation life cycle event. - /// If a move begins or ends outside the board space coordinates, this function is not called. - /// The purpose is to ensure a proposed board move is valid with regard to the moved piece's rules. - /// This event does not worry about check or check-mate, or if a move is legal. - /// - /// - /// A context object with information for you to use to assess whether a move is valid for your implementing piece. - /// A new object indicating whether or not the move is valid. - RulesLifecycleResult OnMoveValidation(MoveValidationContext context); -} diff --git a/Shogi.Domain/Other/MoveValidationContext.cs b/Shogi.Domain/Other/MoveValidationContext.cs deleted file mode 100644 index 32b4206..0000000 --- a/Shogi.Domain/Other/MoveValidationContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Shogi.Domain.Other; - -public record MoveValidationContext( - Vector2 From, - Vector2 To, - bool IsPromotion, - TPiece?[,] BoardState) where TPiece : IRulesLifecycle -{ - public TPiece? GetPieceByRelativePosition(Vector2 relativePosition) - { - var absolute = From + relativePosition; - return BoardState[(int)absolute.X, (int)absolute.Y]; - } -} diff --git a/Shogi.Domain/Other/RulesLifecycleResult.cs b/Shogi.Domain/Other/RulesLifecycleResult.cs deleted file mode 100644 index 5dd255c..0000000 --- a/Shogi.Domain/Other/RulesLifecycleResult.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Shogi.Domain.Other; - -public record RulesLifecycleResult(bool IsError, string ResultMessage = "") -{ -} diff --git a/Shogi.Domain/ValueObjects/BoardState.cs b/Shogi.Domain/ValueObjects/BoardState.cs index 5581ff5..bf679ec 100644 --- a/Shogi.Domain/ValueObjects/BoardState.cs +++ b/Shogi.Domain/ValueObjects/BoardState.cs @@ -39,7 +39,7 @@ public class BoardState } /// - /// Copy constructor. + /// Copy constructor. Creates a deep copy. /// public BoardState(BoardState other) { @@ -115,6 +115,7 @@ public class BoardState var fromPiece = this[fromNotation] ?? throw new InvalidOperationException($"No piece exists at position {fromNotation}."); + if (isPromotionRequested && (IsWithinPromotionZone(to) || IsWithinPromotionZone(from))) { diff --git a/Shogi.Domain/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/GoldGeneral.cs index b1d0c26..e5b0628 100644 --- a/Shogi.Domain/ValueObjects/GoldGeneral.cs +++ b/Shogi.Domain/ValueObjects/GoldGeneral.cs @@ -5,17 +5,17 @@ namespace Shogi.Domain.ValueObjects; internal record class GoldGeneral : Piece { - private static readonly ReadOnlyCollection Player1Paths = new(new List(6) - { - new Path(Direction.Forward), - new Path(Direction.ForwardLeft), - new Path(Direction.ForwardRight), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Backward) - }); + public static readonly ReadOnlyCollection Player1Paths = new( + [ + new Path(Direction.Forward), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Backward) + ]); - private static readonly ReadOnlyCollection Player2Paths = + public static readonly ReadOnlyCollection Player2Paths = Player1Paths .Select(p => p.Invert()) .ToList() diff --git a/Shogi.Domain/ValueObjects/InCheckResult.cs b/Shogi.Domain/ValueObjects/InCheckResult.cs new file mode 100644 index 0000000..c5b54da --- /dev/null +++ b/Shogi.Domain/ValueObjects/InCheckResult.cs @@ -0,0 +1,9 @@ +namespace Shogi.Domain.ValueObjects; + +[Flags] +internal enum InCheckResult +{ + NobodyInCheck = 1, // This kinda doesn't make sense from a Flags perspective, but it works. =/ + Player1InCheck = 2, + Player2InCheck = 4 +} diff --git a/Shogi.Domain/ValueObjects/MoveResult.cs b/Shogi.Domain/ValueObjects/MoveResult.cs index a2d3cae..f66140f 100644 --- a/Shogi.Domain/ValueObjects/MoveResult.cs +++ b/Shogi.Domain/ValueObjects/MoveResult.cs @@ -1,14 +1,6 @@ namespace Shogi.Domain.ValueObjects { - public class MoveResult - { - public bool Success { get; } - public string Reason { get; } - - public MoveResult(bool isSuccess, string reason = "") - { - Success = isSuccess; - Reason = reason; - } - } + public record MoveResult(bool IsSuccess, string Reason = "") + { + } } diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Piece.cs index 40b534d..42316bd 100644 --- a/Shogi.Domain/ValueObjects/Piece.cs +++ b/Shogi.Domain/ValueObjects/Piece.cs @@ -1,11 +1,10 @@ -using Shogi.Domain.Other; -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Diagnostics; namespace Shogi.Domain.ValueObjects { [DebuggerDisplay("{WhichPiece} {Owner}")] - public abstract record class Piece : IRulesLifecycle + public abstract record class Piece { public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) { @@ -65,7 +64,7 @@ namespace Shogi.Domain.ValueObjects var position = start; while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) { - position += path.NormalizedDirection; + position += path.Step; steps.Add(position); if (path.Distance == Distance.OneStep) break; @@ -76,51 +75,7 @@ namespace Shogi.Domain.ValueObjects return steps; } - return Array.Empty(); + return []; } - - #region IRulesLifecycle - public RulesLifecycleResult OnMoveValidation(MoveValidationContext ctx) - { - var paths = this.MoveSet; - - var matchingPaths = paths.Where(p => p.NormalizedDirection == Vector2.Normalize(ctx.To - ctx.From)); - if (!matchingPaths.Any()) - { - return new RulesLifecycleResult(IsError: true, "Piece cannot move like that."); - } - - var multiStepPaths = matchingPaths.Where(p => p.Distance == Distance.MultiStep).ToArray(); - foreach (var path in multiStepPaths) - { - // Assert that no pieces exist along the from -> to path. - var isPathObstructed = GetPositionsAlongPath(ctx.From, ctx.To, path) - .Any(pos => ctx.BoardState[(int)pos.X, (int)pos.Y] != null); - if (isPathObstructed) - { - return new RulesLifecycleResult(IsError: true, "Piece cannot move through other pieces."); - } - - } - - var pieceAtTo = ctx.BoardState[(int)ctx.To.X, (int)ctx.To.Y]; - if (pieceAtTo?.Owner == this.Owner) - { - return new RulesLifecycleResult(IsError: true, "Cannot capture your own pieces."); - } - - return new RulesLifecycleResult(IsError: false); - - static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, Path path) - { - var next = from; - while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9) - { - next += path.NormalizedDirection; - yield return next; - } - } - } - #endregion } } diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs index 7328dff..2f78461 100644 --- a/Shogi.Domain/ValueObjects/ShogiBoard.cs +++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs @@ -1,7 +1,4 @@ -using System.Text; -using Shogi.Domain.Other; -using Shogi.Domain.YetToBeAssimilatedIntoDDD; - +using Shogi.Domain.YetToBeAssimilatedIntoDDD; namespace Shogi.Domain.ValueObjects; /// @@ -12,6 +9,7 @@ namespace Shogi.Domain.ValueObjects; public sealed class ShogiBoard { private readonly StandardRules rules; + private static readonly Vector2 BoardSize = new Vector2(9, 9); public ShogiBoard(BoardState initialState) { @@ -29,239 +27,273 @@ public sealed class ShogiBoard /// validate legal vs illegal moves without having to worry about reverting board state. /// /// - public void Move(string from, string to, bool isPromotion = false) + public MoveResult Move(string from, string to, bool isPromotion = false) { // Validate the move - var rulesState = new Piece?[9, 9]; - for (int x = 0; x < 9; x++) - for (int y = 0; y < 9; y++) - { - rulesState[x, y] = this.BoardState[x, y]; - } - - var rules = new BoardRules(rulesState); - var validationResult = rules.ValidateMove(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to), isPromotion); - if (validationResult.IsError) + var moveResult = IsMoveValid(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to)); + if (!moveResult.IsSuccess) { - throw new InvalidOperationException(validationResult.ResultMessage); + return moveResult; } // Move is valid, but is it legal? // Check for correct player's turn. if (BoardState.WhoseTurn != BoardState[from]!.Owner) { - throw new InvalidOperationException("Not allowed to move the opponent's pieces."); + return new MoveResult(false, "Not allowed to move the opponent's pieces."); } - // Simulate the move on a throw -away state and look for "check" and "check-mate". - var simulationState = new BoardState(BoardState); - var moveResult = simulationState.Move(from, to, isPromotion); - if (!moveResult.Success) + // Simulate the move on a throw-away state and look for "check" and "check-mate". + var simState = new BoardState(BoardState); + moveResult = simState.Move(from, to, isPromotion); + if (!moveResult.IsSuccess) { - throw new InvalidOperationException(moveResult.Reason); + return moveResult; } - var simulation = new StandardRules(simulationState); - // If already in check, assert the move that resulted in check no longer results in check. - if (BoardState.InCheck == BoardState.WhoseTurn - && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) + var kings = simState.State + .Where(kvp => kvp.Value?.WhichPiece == WhichPiece.King) + .Cast>() + .ToArray(); + + if (kings.Length != 2) throw new InvalidOperationException("Unexpected scenario: board does not have two kings in play."); + + // Look for threats against the kings. + var inCheckResult = simState.State + .Where(kvp => kvp.Value != null) + .Cast>() + .Aggregate(InCheckResult.NobodyInCheck, (inCheckResult, kvp) => + { + var newInCheckResult = inCheckResult; + var threatPiece = kvp.Value; + var opposingKingPosition = Notation.FromBoardNotation(kings.Single(king => king.Value.Owner != threatPiece.Owner).Key); + var candidatePositions = threatPiece.GetPathFromStartToEnd(Notation.FromBoardNotation(kvp.Key), opposingKingPosition); + + foreach (var position in candidatePositions) + { + // No piece at this position, so pathing is unobstructed. Continue pathing. + if (simState[position] == null) continue; + + var pieceAtPosition = simState[position]!; + if (pieceAtPosition.WhichPiece == WhichPiece.King && pieceAtPosition.Owner != threatPiece.Owner) + { + newInCheckResult &= pieceAtPosition.Owner == WhichPlayer.Player1 ? InCheckResult.Player2InCheck : InCheckResult.Player1InCheck; + } + else + { + break; + } + } + + return newInCheckResult; + }); + + var playerPutThemselfInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 + ? inCheckResult.HasFlag(InCheckResult.Player1InCheck) + : inCheckResult.HasFlag(InCheckResult.Player2InCheck); + + if (playerPutThemselfInCheck) { - throw new InvalidOperationException("Unable to move because you are still in check."); + return new MoveResult(false, "This move puts the moving player in check, which is illega."); } - if (simulation.DidPlayerPutThemselfInCheck()) + // Move is legal; mutate the real state. + BoardState.Move(from, to, isPromotion); + var playerPutOpponentInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 + ? inCheckResult.HasFlag(InCheckResult.Player2InCheck) + : inCheckResult.HasFlag(InCheckResult.Player1InCheck); + if (playerPutOpponentInCheck) { - throw new InvalidOperationException("Illegal move. This move places you in check."); - } - - var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 + BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - _ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that. - if (rules.IsOpponentInCheckAfterMove()) - { - BoardState.InCheck = otherPlayer; - if (rules.IsOpponentInCheckMate()) - { - BoardState.IsCheckmate = true; - } } - else - { - BoardState.InCheck = null; - } - BoardState.WhoseTurn = otherPlayer; + + // TODO: Look for check-mate. + return new MoveResult(true); + + + + + + + + + + + //var simulation = new StandardRules(simState); + //// If already in check, assert the move that resulted in check no longer results in check. + //if (BoardState.InCheck == BoardState.WhoseTurn + // && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) + //{ + // throw new InvalidOperationException("Unable to move because you are still in check."); + //} + + //if (simulation.DidPlayerPutThemselfInCheck()) + //{ + // throw new InvalidOperationException("Illegal move. This move places you in check."); + //} + + //var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 + // ? WhichPlayer.Player2 + // : WhichPlayer.Player1; + //_ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that. + //if (rules.IsOpponentInCheckAfterMove()) + //{ + // BoardState.InCheck = otherPlayer; + // if (rules.IsOpponentInCheckMate()) + // { + // BoardState.IsCheckmate = true; + // } + //} + //else + //{ + // BoardState.InCheck = null; + //} + //BoardState.WhoseTurn = otherPlayer; } public void Move(WhichPiece pieceInHand, string to) { - var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); - if (index == -1) - { - throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); - } + //var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); + //if (index == -1) + //{ + // throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); + //} - if (BoardState[to] != null) - { - throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); - } + //if (BoardState[to] != null) + //{ + // throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); + //} - var toVector = Notation.FromBoardNotation(to); - switch (pieceInHand) - { - case WhichPiece.Knight: - { - // Knight cannot be placed onto the farthest two ranks from the hand. - if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6 - || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2) - { - throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); - } - break; - } - case WhichPiece.Lance: - case WhichPiece.Pawn: - { - // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8 - || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0) - { - throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); - } - break; - } - } + //var toVector = Notation.FromBoardNotation(to); + //switch (pieceInHand) + //{ + // case WhichPiece.Knight: + // { + // // Knight cannot be placed onto the farthest two ranks from the hand. + // if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6 + // || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2) + // { + // throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); + // } + // break; + // } + // case WhichPiece.Lance: + // case WhichPiece.Pawn: + // { + // // Lance and Pawn cannot be placed onto the farthest rank from the hand. + // if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8 + // || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0) + // { + // throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); + // } + // break; + // } + //} - var tempBoard = new BoardState(BoardState); - var simulation = new StandardRules(tempBoard); - var moveResult = simulation.Move(pieceInHand, to); - if (!moveResult.Success) - { - throw new InvalidOperationException(moveResult.Reason); - } + //var tempBoard = new BoardState(BoardState); + //var simulation = new StandardRules(tempBoard); + //var moveResult = simulation.Move(pieceInHand, to); + //if (!moveResult.Success) + //{ + // throw new InvalidOperationException(moveResult.Reason); + //} - // If already in check, assert the move that resulted in check no longer results in check. - if (BoardState.InCheck == BoardState.WhoseTurn - && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) - { - throw new InvalidOperationException("Unable to drop piece becauase you are still in check."); - } + //// If already in check, assert the move that resulted in check no longer results in check. + //if (BoardState.InCheck == BoardState.WhoseTurn + // && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) + //{ + // throw new InvalidOperationException("Unable to drop piece becauase you are still in check."); + //} - if (simulation.DidPlayerPutThemselfInCheck()) - { - throw new InvalidOperationException("Illegal move. This move places you in check."); - } + //if (simulation.DidPlayerPutThemselfInCheck()) + //{ + // throw new InvalidOperationException("Illegal move. This move places you in check."); + //} - // Update the non-simulation board. - var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 - ? WhichPlayer.Player2 - : WhichPlayer.Player1; - _ = rules.Move(pieceInHand, to); - if (rules.IsOpponentInCheckAfterMove()) - { - BoardState.InCheck = otherPlayer; - // A pawn, placed from the hand, cannot be the cause of checkmate. - if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) - { - BoardState.IsCheckmate = true; - } - } + //// Update the non-simulation board. + //var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 + // ? WhichPlayer.Player2 + // : WhichPlayer.Player1; + //_ = rules.Move(pieceInHand, to); + //if (rules.IsOpponentInCheckAfterMove()) + //{ + // BoardState.InCheck = otherPlayer; + // // A pawn, placed from the hand, cannot be the cause of checkmate. + // if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) + // { + // BoardState.IsCheckmate = true; + // } + //} - var kingPosition = otherPlayer == WhichPlayer.Player1 - ? tempBoard.Player1KingPosition - : tempBoard.Player2KingPosition; - BoardState.WhoseTurn = otherPlayer; + //var kingPosition = otherPlayer == WhichPlayer.Player1 + // ? tempBoard.Player1KingPosition + // : tempBoard.Player2KingPosition; + //BoardState.WhoseTurn = otherPlayer; } /// - /// Prints a ASCII representation of the board for debugging board state. + /// The purpose is to ensure a proposed board move is valid with regard to the moved piece's rules. + /// This event does not worry about check or check-mate, or if a move is legal according to all Shogi rules. + /// It asserts that a proposed move is possible and worthy of further validation (check, check-mate, etc). /// - /// - public string ToStringStateAsAscii() + private MoveResult IsMoveValid(Vector2 from, Vector2 to) { - var builder = new StringBuilder(); - builder.Append(" "); - builder.Append("Player 2"); - builder.AppendLine(); - for (var rank = 8; rank >= 0; rank--) + if (IsWithinBounds(from) && IsWithinBounds(to)) { - // Horizontal line - builder.Append(" - "); - for (var file = 0; file < 8; file++) builder.Append("- - "); - builder.Append("- -"); - - // Print Rank ruler. - builder.AppendLine(); - builder.Append($"{rank + 1} "); - - // Print pieces. - builder.Append(" |"); - for (var x = 0; x < 9; x++) + if (BoardState[to]?.WhichPiece == WhichPiece.King) { - var piece = BoardState[x, rank]; - if (piece == null) - { - builder.Append(" "); - } - else - { - builder.AppendFormat("{0}", ToAscii(piece)); - } - builder.Append('|'); + return new MoveResult(false, "Kings may not be captured."); } - builder.AppendLine(); + + var piece = BoardState[from]; + if (piece == null) + { + return new MoveResult(false, $"There is no piece at position {from}."); + } + var matchingPaths = piece.MoveSet.Where(p => p.NormalizedStep == Vector2.Normalize(to - from)); + if (!matchingPaths.Any()) + { + return new MoveResult(false, "Piece cannot move like that."); + } + + var multiStepPaths = matchingPaths.Where(path => path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep).ToArray(); + foreach (var path in multiStepPaths) + { + // Assert that no pieces exist along the from -> to path. + var isPathObstructed = GetPositionsAlongPath(from, to, path) + .Any(pos => BoardState[pos] != null); + if (isPathObstructed) + { + return new MoveResult(false, "Piece cannot move through other pieces."); + } + + } + + var pieceAtTo = BoardState[to]; + if (pieceAtTo?.Owner == piece.Owner) + { + return new MoveResult(false, "Cannot capture your own pieces."); + } + } - - // Horizontal line - builder.Append(" - "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append(" "); - builder.Append("Player 1"); - - builder.AppendLine(); - builder.AppendLine(); - // Print File ruler. - builder.Append(" "); - builder.Append(" A B C D E F G H I "); - - return builder.ToString(); + return new MoveResult(true); } - /// - /// - /// - /// - /// - /// A string with three characters. - /// The first character indicates promotion status. - /// The second character indicates piece. - /// The third character indicates ownership. - /// - private static string ToAscii(Piece piece) + private static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, YetToBeAssimilatedIntoDDD.Pathing.Path path) { - var builder = new StringBuilder(); - if (piece.IsPromoted) builder.Append('^'); - else builder.Append(' '); - - var name = piece.WhichPiece switch + var next = from; + while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9) { - WhichPiece.King => "K", - WhichPiece.GoldGeneral => "G", - WhichPiece.SilverGeneral => "S", - WhichPiece.Bishop => "B", - WhichPiece.Rook => "R", - WhichPiece.Knight => "k", - WhichPiece.Lance => "L", - WhichPiece.Pawn => "P", - _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), - }; - builder.Append(name); + next += path.Step; + yield return next; + } + } - if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); - else builder.Append(' '); - - return builder.ToString(); + private static bool IsWithinBounds(Vector2 position) + { + var isPositive = position - position == Vector2.Zero; + return isPositive && position.X <= BoardSize.X && position.Y <= BoardSize.Y; } } diff --git a/Shogi.Domain/ValueObjects/StandardRules.cs b/Shogi.Domain/ValueObjects/StandardRules.cs index 40712e3..911520a 100644 --- a/Shogi.Domain/ValueObjects/StandardRules.cs +++ b/Shogi.Domain/ValueObjects/StandardRules.cs @@ -147,7 +147,7 @@ namespace Shogi.Domain.ValueObjects var paths = boardState[kingPosition]!.MoveSet; return paths - .Select(path => path.NormalizedDirection + kingPosition) + .Select(path => path.Step + kingPosition) // Because the king could be on the edge of the board, where some of its paths do not make sense. .Where(newPosition => newPosition.IsInsideBoardBoundary()) // Where tile at position is empty, meaning the king could move there. diff --git a/Shogi.Domain/ValueObjects/WhichPiece.cs b/Shogi.Domain/ValueObjects/WhichPiece.cs index cf7e1b7..fab4429 100644 --- a/Shogi.Domain/ValueObjects/WhichPiece.cs +++ b/Shogi.Domain/ValueObjects/WhichPiece.cs @@ -5,15 +5,15 @@ public enum WhichPiece King, GoldGeneral, SilverGeneral, - PromotedSilverGeneral, + //PromotedSilverGeneral, Bishop, - PromotedBishop, + //PromotedBishop, Rook, - PromotedRook, + //PromotedRook, Knight, - PromotedKnight, + //PromotedKnight, Lance, - PromotedLance, - Pawn - PromotedPawn + //PromotedLance, + Pawn, + //PromotedPawn, } diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs index 765c1f7..c4bf9be 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs @@ -1,19 +1,32 @@ using System.Diagnostics; +using static Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing.Path; namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; -[DebuggerDisplay("{Direction} - {Distance}")] +[DebuggerDisplay("{Step} - {Distance}")] public record Path { - public Vector2 NormalizedDirection { get; } + public Vector2 Step { get; } + public Vector2 NormalizedStep => Vector2.Normalize(Step); public Distance Distance { get; } - public Path(Vector2 direction, Distance distance = Distance.OneStep) + /// + /// + /// + /// The smallest distance that can occur during a move. + /// + public Path(Vector2 step, Distance distance = Distance.OneStep) { - NormalizedDirection = Vector2.Normalize(direction); + Step = step; this.Distance = distance; } - public Path Invert() => new(Vector2.Negate(NormalizedDirection), Distance); + public Path Invert() => new(Vector2.Negate(Step), Distance); + + //public enum PathingResult + //{ + // Obstructed, + // CompletedWithoutObstruction + //} } public static class PathExtensions @@ -28,8 +41,8 @@ public static class PathExtensions var shortestPath = paths.First(); foreach (var path in paths.Skip(1)) { - var distance = Vector2.Distance(start + path.NormalizedDirection, end); //Normalizing the direction probably broke this. - var shortestDistance = Vector2.Distance(start + shortestPath.NormalizedDirection, end); // And this. + var distance = Vector2.Distance(start + path.Step, end); + var shortestDistance = Vector2.Distance(start + shortestPath.Step, end); if (distance < shortestDistance) { shortestPath = path; diff --git a/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor b/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor index 4e3d5c8..c4aea4b 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor +++ b/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor @@ -5,7 +5,6 @@ @implements IDisposable @inject ShogiApi ShogiApi -@inject PromotePrompt PromotePrompt @inject GameHubNode hubNode @inject NavigationManager navigator diff --git a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor b/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor index 47444cf..5cfcc63 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor +++ b/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor @@ -1,6 +1,5 @@ @using Shogi.Contracts.Types; @using System.Text.Json; -@inject PromotePrompt PromotePrompt;
@if (IsSpectating) @@ -53,16 +52,6 @@ H I - - -
-

Do you wish to promote?

-
- - - -
-
@@ -121,13 +110,6 @@
@code { - - - - - - - static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; /// diff --git a/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css b/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css index f36d333..402334f 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css +++ b/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css @@ -110,20 +110,4 @@ grid-template-rows: 3rem; place-items: center start; padding: 0.5rem; - } - -.promote-prompt { - display: none; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border: 2px solid #444; - padding: 1rem; - box-shadow: 1px 1px 1px #444; - text-align: center; -} - - .promote-prompt[data-visible="true"] { - display: block; - } + } \ No newline at end of file diff --git a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor index fa78df2..909dc79 100644 --- a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor +++ b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor @@ -2,16 +2,31 @@ @using Shogi.Contracts.Types; @using System.Text.RegularExpressions; @using System.Net; -@inject PromotePrompt PromotePrompt; @inject ShogiApi ShogiApi; - +
+ + + @if (showPromotePrompt) + { + + +
+

Do you wish to promote?

+
+ + + +
+
+ } +
@code { [Parameter, EditorRequired] @@ -21,6 +36,8 @@ private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective; private string? selectedBoardPosition; private WhichPiece? selectedPieceFromHand; + private bool showPromotePrompt; + private string? moveTo; protected override void OnParametersSet() { @@ -75,7 +92,7 @@ { // Placing a piece from the hand to an empty space. var success = await ShogiApi.Move( - Session.SessionId.ToString(), + Session.SessionId, new MovePieceCommand(selectedPieceFromHand.Value, position)); if (!success) { @@ -88,18 +105,24 @@ if (selectedBoardPosition != null) { + Console.WriteLine("pieceAtPosition is null? {0}", pieceAtPosition == null); + if (pieceAtPosition == null || pieceAtPosition?.Owner != Perspective) { // Moving to an empty space or capturing an opponent's piece. if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedBoardPosition)) { - PromotePrompt.Show( - Session.SessionId.ToString(), - new MovePieceCommand(selectedBoardPosition, position, false)); + Console.WriteLine("Prompt!"); + moveTo = position; + showPromotePrompt = true; } else { - var success = await ShogiApi.Move(Session.SessionId.ToString(), new MovePieceCommand(selectedBoardPosition, position, false)); + + Console.WriteLine("OnClick to move to {0}", position); + var success = await ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedBoardPosition, position, false)); + + Console.WriteLine("Success? {0}", success); if (!success) { selectedBoardPosition = null; @@ -125,4 +148,14 @@ StateHasChanged(); } + + private Task OnClickPromotionChoice(bool shouldPromote) + { + if (selectedBoardPosition == null && selectedPieceFromHand.HasValue && moveTo != null) + { + return ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedPieceFromHand.Value, moveTo)); + } + + throw new InvalidOperationException("Unexpected scenario during OnClickPromotionChoice."); + } } diff --git a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css new file mode 100644 index 0000000..05a3c33 --- /dev/null +++ b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css @@ -0,0 +1,14 @@ +.promote-prompt { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border: 2px solid #444; + padding: 1rem; + box-shadow: 1px 1px 1px #444; + text-align: center; + z-index: 101; + + background-color: #444; + border: 1px solid black; +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Play/PromotePrompt.cs b/Shogi.UI/Pages/Play/PromotePrompt.cs deleted file mode 100644 index 3bba99c..0000000 --- a/Shogi.UI/Pages/Play/PromotePrompt.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Shogi.Contracts.Api; -using Shogi.UI.Shared; - -namespace Shogi.UI.Pages.Play; - -public class PromotePrompt -{ - private readonly ShogiApi shogiApi; - private string? sessionName; - private MovePieceCommand? command; - - public PromotePrompt(ShogiApi shogiApi) - { - this.shogiApi = shogiApi; - this.IsVisible = false; - this.OnClickCancel = this.Hide; - } - - public bool IsVisible { get; private set; } - public Action OnClickCancel; - public Func? OnClickNo; - public Func? OnClickYes; - - public void Show(string sessionName, MovePieceCommand command) - { - this.sessionName = sessionName; - this.command = command; - this.IsVisible = true; - this.OnClickNo = this.Move; - this.OnClickYes = this.MoveAndPromote; - } - - public void Hide() - { - this.IsVisible = false; - this.OnClickNo = null; - this.OnClickYes = null; - } - - private Task Move() - { - if (this.command != null && this.sessionName != null) - { - this.command.IsPromotion = false; - return this.shogiApi.Move(this.sessionName, this.command); - } - return Task.CompletedTask; - } - private Task MoveAndPromote() - { - if (this.command != null && this.sessionName != null) - { - this.command.IsPromotion = true; - return this.shogiApi.Move(this.sessionName, this.command); - } - return Task.CompletedTask; - } -} diff --git a/Shogi.UI/Program.cs b/Shogi.UI/Program.cs index cf5a832..790888f 100644 --- a/Shogi.UI/Program.cs +++ b/Shogi.UI/Program.cs @@ -39,8 +39,7 @@ static void ConfigureDependencies(IServiceCollection services, IConfiguration co services .AddTransient() - .AddTransient() - .AddSingleton(); + .AddTransient(); // Identity services diff --git a/Shogi.UI/Shared/ShogiApi.cs b/Shogi.UI/Shared/ShogiApi.cs index e4a5cf3..abd86d7 100644 --- a/Shogi.UI/Shared/ShogiApi.cs +++ b/Shogi.UI/Shared/ShogiApi.cs @@ -52,7 +52,7 @@ public class ShogiApi(HttpClient httpClient) /// /// Returns false if the move was not accepted by the server. /// - public async Task Move(string sessionName, MovePieceCommand command) + public async Task Move(Guid sessionName, MovePieceCommand command) { var response = await httpClient.PatchAsync(Relative($"Sessions/{sessionName}/Move"), JsonContent.Create(command)); return response.IsSuccessStatusCode; diff --git a/Tests/UnitTests/Extensions.cs b/Tests/UnitTests/Extensions.cs new file mode 100644 index 0000000..1aed531 --- /dev/null +++ b/Tests/UnitTests/Extensions.cs @@ -0,0 +1,97 @@ +using Shogi.Domain.ValueObjects; +using System; +using System.Text; + +namespace UnitTests; + +public static class Extensions +{ + /// + /// Prints a ASCII representation of the board for debugging board state. + /// + /// + public static string ToStringStateAsAscii(this ShogiBoard board) + { + var boardState = board.BoardState; + var builder = new StringBuilder(); + builder.Append(" "); + builder.Append("Player 2"); + builder.AppendLine(); + for (var rank = 8; rank >= 0; rank--) + { + // Horizontal line + builder.Append(" - "); + for (var file = 0; file < 8; file++) builder.Append("- - "); + builder.Append("- -"); + + // Print Rank ruler. + builder.AppendLine(); + builder.Append($"{rank + 1} "); + + // Print pieces. + builder.Append(" |"); + for (var x = 0; x < 9; x++) + { + var piece = boardState[x, rank]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", ToAscii(piece)); + } + builder.Append('|'); + } + builder.AppendLine(); + } + + // Horizontal line + builder.Append(" - "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" "); + builder.Append("Player 1"); + + builder.AppendLine(); + builder.AppendLine(); + // Print File ruler. + builder.Append(" "); + builder.Append(" A B C D E F G H I "); + + return builder.ToString(); + } + + /// + /// A string with three characters. + /// The first character indicates promotion status. + /// The second character indicates piece. + /// The third character indicates ownership. + /// + private static string ToAscii(Piece piece) + { + var builder = new StringBuilder(); + if (piece.IsPromoted) builder.Append('^'); + else builder.Append(' '); + + var name = piece.WhichPiece switch + { + WhichPiece.King => "K", + WhichPiece.GoldGeneral => "G", + WhichPiece.SilverGeneral => "S", + WhichPiece.Bishop => "B", + WhichPiece.Rook => "R", + WhichPiece.Knight => "k", + WhichPiece.Lance => "L", + WhichPiece.Pawn => "P", + _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), + }; + builder.Append(name); + + if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); + else builder.Append(' '); + + return builder.ToString(); + } +} diff --git a/Tests/UnitTests/NotationShould.cs b/Tests/UnitTests/NotationShould.cs index 0e71e82..d42f72f 100644 --- a/Tests/UnitTests/NotationShould.cs +++ b/Tests/UnitTests/NotationShould.cs @@ -1,9 +1,9 @@ using System.Numerics; using Shogi.Domain.YetToBeAssimilatedIntoDDD; -namespace Shogi.Domain.UnitTests +namespace UnitTests { - public class NotationShould + public class NotationShould { [Fact] public void ConvertFromNotationToVector() diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index e1d9b81..c48b0ea 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -2,221 +2,221 @@ using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Numerics; -namespace Shogi.Domain.UnitTests; +namespace UnitTests; public class RookShould { - public class MoveSet - { - private readonly Rook rook1; - private readonly Rook rook2; + public class MoveSet + { + private readonly Rook rook1; + private readonly Rook rook2; - public MoveSet() - { - this.rook1 = new Rook(WhichPlayer.Player1); - this.rook2 = new Rook(WhichPlayer.Player2); - } + public MoveSet() + { + rook1 = new Rook(WhichPlayer.Player1); + rook2 = new Rook(WhichPlayer.Player2); + } - [Fact] - public void Player1_HasCorrectMoveSet() - { - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); - } + [Fact] + public void Player1_HasCorrectMoveSet() + { + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + } - [Fact] - public void Player1_Promoted_HasCorrectMoveSet() - { - // Arrange - rook1.Promote(); - rook1.IsPromoted.Should().BeTrue(); + [Fact] + public void Player1_Promoted_HasCorrectMoveSet() + { + // Arrange + rook1.Promote(); + rook1.IsPromoted.Should().BeTrue(); - // Assert - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); - } + // Assert + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); + } - [Fact] - public void Player2_HasCorrectMoveSet() - { - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); - } + [Fact] + public void Player2_HasCorrectMoveSet() + { + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + } - [Fact] - public void Player2_Promoted_HasCorrectMoveSet() - { - // Arrange - rook2.Promote(); - rook2.IsPromoted.Should().BeTrue(); + [Fact] + public void Player2_Promoted_HasCorrectMoveSet() + { + // Arrange + rook2.Promote(); + rook2.IsPromoted.Should().BeTrue(); - // Assert - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); - } + // Assert + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); + } - } - private readonly Rook rookPlayer1; + } + private readonly Rook rookPlayer1; - public RookShould() - { - this.rookPlayer1 = new Rook(WhichPlayer.Player1); - } + public RookShould() + { + rookPlayer1 = new Rook(WhichPlayer.Player1); + } - [Fact] - public void Promote() - { - this.rookPlayer1.IsPromoted.Should().BeFalse(); - this.rookPlayer1.CanPromote.Should().BeTrue(); - this.rookPlayer1.Promote(); - this.rookPlayer1.IsPromoted.Should().BeTrue(); - this.rookPlayer1.CanPromote.Should().BeFalse(); - } + [Fact] + public void Promote() + { + rookPlayer1.IsPromoted.Should().BeFalse(); + rookPlayer1.CanPromote.Should().BeTrue(); + rookPlayer1.Promote(); + rookPlayer1.IsPromoted.Should().BeTrue(); + rookPlayer1.CanPromote.Should().BeFalse(); + } - [Fact] - public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); + [Fact] + public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } - [Fact] - public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); + [Fact] + public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); - } + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); + } - [Fact] - public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); + [Fact] + public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } - [Fact] - public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); + [Fact] + public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); - } + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); + } - [Fact] - public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); + [Fact] + public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } - [Fact] - public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); + [Fact] + public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); - } + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); + } - [Fact] - public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); + [Fact] + public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } - [Fact] - public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); + [Fact] + public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); - } + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); + } } diff --git a/Tests/UnitTests/ShogiBoardStateShould.cs b/Tests/UnitTests/ShogiBoardStateShould.cs index f3cfb83..a4488e8 100644 --- a/Tests/UnitTests/ShogiBoardStateShould.cs +++ b/Tests/UnitTests/ShogiBoardStateShould.cs @@ -1,6 +1,6 @@ using Shogi.Domain.ValueObjects; -namespace Shogi.Domain.UnitTests; +namespace UnitTests; public class ShogiBoardStateShould { diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 640fb56..918c505 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -2,453 +2,453 @@ using System; using System.Linq; -namespace Shogi.Domain.UnitTests +namespace UnitTests { - public class ShogiShould - { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) - { - this.console = console; - } + public class ShogiShould + { + private readonly ITestOutputHelper console; + public ShogiShould(ITestOutputHelper console) + { + this.console = console; + } - [Fact] - public void MoveAPieceToAnEmptyPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; + [Fact] + public void MoveAPieceToAnEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; - board["A4"].Should().BeNull(); - var expectedPiece = board["A3"]; - expectedPiece.Should().NotBeNull(); + board["A4"].Should().BeNull(); + var expectedPiece = board["A3"]; + expectedPiece.Should().NotBeNull(); - // Act - shogi.Move("A3", "A4", false); + // Act + shogi.Move("A3", "A4", false); - // Assert - board["A3"].Should().BeNull(); - board["A4"].Should().Be(expectedPiece); - } + // Assert + board["A3"].Should().BeNull(); + board["A4"].Should().Be(expectedPiece); + } - [Fact] - public void AllowValidMoves_AfterCheck() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop puts P2 in check - shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); + [Fact] + public void AllowValidMoves_AfterCheck() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop puts P2 in check + shogi.Move("B2", "G7", false); + board.InCheck.Should().Be(WhichPlayer.Player2); - // Act - P2 is able to un-check theirself. - /// P2 King moves out of check - shogi.Move("E9", "E8", false); + // Act - P2 is able to un-check theirself. + /// P2 King moves out of check + shogi.Move("E9", "E8", false); - // Assert - using (new AssertionScope()) - { - board.InCheck.Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + board.InCheck.Should().BeNull(); + } + } - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - board["D5"].Should().BeNull(); + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + board["D5"].Should().BeNull(); - // Act - var act = () => shogi.Move("D5", "D6", false); + // Act + var act = () => shogi.Move("D5", "D6", false); - // Assert - act.Should().Throw(); - board["D5"].Should().BeNull(); - board["D6"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } + // Assert + act.Should().Throw(); + board["D5"].Should().BeNull(); + board["D6"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } - [Fact] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["A3"]; + [Fact] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A3"]; - // Act - P1 "moves" pawn to the position it already exists at. - var act = () => shogi.Move("A3", "A3", false); + // Act - P1 "moves" pawn to the position it already exists at. + var act = () => shogi.Move("A3", "A3", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A3"].Should().Be(expectedPiece); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board["A3"].Should().Be(expectedPiece); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } + } - [Fact] - public void PreventInvalidMoves_MoveSet() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["A1"]; - expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A1"]; + expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); - // Act - Move Lance illegally - var act = () => shogi.Move("A1", "D5", false); + // Act - Move Lance illegally + var act = () => shogi.Move("A1", "D5", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(expectedPiece); - board["A5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board["A1"].Should().Be(expectedPiece); + board["A5"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } + } - [Fact] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["A7"]; - expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); + [Fact] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A7"]; + expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); - // Act - Move Player2 Pawn when it is Player1 turn. - var act = () => shogi.Move("A7", "A6", false); + // Act - Move Player2 Pawn when it is Player1 turn. + var act = () => shogi.Move("A7", "A6", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A7"].Should().Be(expectedPiece); - board["A6"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board["A7"].Should().Be(expectedPiece); + board["A6"].Should().BeNull(); + } + } - [Fact] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var lance = board["A1"]; - var pawn = board["A3"]; - lance!.Owner.Should().Be(pawn!.Owner); + [Fact] + public void PreventInvalidMoves_MoveThroughAllies() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var lance = board["A1"]; + var pawn = board["A3"]; + lance!.Owner.Should().Be(pawn!.Owner); - // Act - Move P1 Lance through P1 Pawn. - var act = () => shogi.Move("A1", "A5", false); + // Act - Move P1 Lance through P1 Pawn. + var act = () => shogi.Move("A1", "A5", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(lance); - board["A3"].Should().Be(pawn); - board["A5"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board["A1"].Should().Be(lance); + board["A3"].Should().Be(pawn); + board["A5"].Should().BeNull(); + } + } - [Fact] - public void PreventInvalidMoves_CaptureAlly() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var knight = board["B1"]; - var pawn = board["C3"]; - knight!.Owner.Should().Be(pawn!.Owner); + [Fact] + public void PreventInvalidMoves_CaptureAlly() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var knight = board["B1"]; + var pawn = board["C3"]; + knight!.Owner.Should().Be(pawn!.Owner); - // Act - P1 Knight tries to capture P1 Pawn. - var act = () => shogi.Move("B1", "C3", false); + // Act - P1 Knight tries to capture P1 Pawn. + var act = () => shogi.Move("B1", "C3", false); - // Arrange - using (new AssertionScope()) - { - act.Should().Throw(); - board["B1"].Should().Be(knight); - board["C3"].Should().Be(pawn); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Arrange + using (new AssertionScope()) + { + act.Should().Throw(); + board["B1"].Should().Be(knight); + board["C3"].Should().Be(pawn); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } + } - [Fact] - public void PreventInvalidMoves_Check() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop puts P2 in check - shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); - var lance = board["I9"]; + [Fact] + public void PreventInvalidMoves_Check() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop puts P2 in check + shogi.Move("B2", "G7", false); + board.InCheck.Should().Be(WhichPlayer.Player2); + var lance = board["I9"]; - // Act - P2 moves Lance while in check. - var act = () => shogi.Move("I9", "I8", false); + // Act - P2 moves Lance while in check. + var act = () => shogi.Move("I9", "I8", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().Be(lance); - board["I8"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board.InCheck.Should().Be(WhichPlayer.Player2); + board["I9"].Should().Be(lance); + board["I8"].Should().BeNull(); + } + } - [Fact] - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("I7", "I6", false); - // P1 Bishop takes P2 Pawn. - shogi.Move("B2", "G7", false); - // P2 Gold, block check from P1 Bishop. - shogi.Move("F9", "F8", false); - // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - shogi.Move("G7", "H8", true); - // P2 Pawn again - shogi.Move("I6", "I5", false); - // P1 Bishop takes P2 Knight - shogi.Move("H8", "H9", false); - // P2 Pawn again - shogi.Move("I5", "I4", false); - // P1 Bishop takes P2 Lance - shogi.Move("H9", "I9", false); - // P2 Pawn captures P1 Pawn - shogi.Move("I4", "I3", false); - board.Player1Hand.Count.Should().Be(4); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); + [Fact] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("I7", "I6", false); + // P1 Bishop takes P2 Pawn. + shogi.Move("B2", "G7", false); + // P2 Gold, block check from P1 Bishop. + shogi.Move("F9", "F8", false); + // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + shogi.Move("G7", "H8", true); + // P2 Pawn again + shogi.Move("I6", "I5", false); + // P1 Bishop takes P2 Knight + shogi.Move("H8", "H9", false); + // P2 Pawn again + shogi.Move("I5", "I4", false); + // P1 Bishop takes P2 Lance + shogi.Move("H9", "I9", false); + // P2 Pawn captures P1 Pawn + shogi.Move("I4", "I3", false); + board.Player1Hand.Count.Should().Be(4); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); - // Act | Assert - Illegally placing Knight from the hand in farthest rank. - board["H9"].Should().BeNull(); - var act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in farthest rank. + board["H9"].Should().BeNull(); + var act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally placing Knight from the hand in second farthest row. - board["H8"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H8"); - act.Should().Throw(); - board["H8"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in second farthest row. + board["H8"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H8"); + act.Should().Throw(); + board["H8"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally place Lance from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + // Act | Assert - Illegally place Lance from the hand. + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - // Act | Assert - Illegally place Pawn from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Pawn, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // Act | Assert - Illegally place Pawn from the hand. + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Pawn, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // // TODO - } + // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // // TODO + } - [Fact] - public void PreventInvalidDrop_Check() - { - // Arrange - var shogi = MockShogiBoard(); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Pawn, arbitrary move. - shogi.Move("A3", "A4", false); - // P2 Bishop takes P1 Bishop - shogi.Move("H8", "B2", false); - // P1 Silver takes P2 Bishop - shogi.Move("C1", "B2", false); - // P2 Pawn, arbtrary move - shogi.Move("A7", "A6", false); - // P1 drop Bishop, place P2 in check - shogi.Move(WhichPiece.Bishop, "G7"); - shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); - shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["E5"].Should().BeNull(); + [Fact] + public void PreventInvalidDrop_Check() + { + // Arrange + var shogi = MockShogiBoard(); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Pawn, arbitrary move. + shogi.Move("A3", "A4", false); + // P2 Bishop takes P1 Bishop + shogi.Move("H8", "B2", false); + // P1 Silver takes P2 Bishop + shogi.Move("C1", "B2", false); + // P2 Pawn, arbtrary move + shogi.Move("A7", "A6", false); + // P1 drop Bishop, place P2 in check + shogi.Move(WhichPiece.Bishop, "G7"); + shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); + shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.BoardState["E5"].Should().BeNull(); - // Act - P2 places a Bishop while in check. - var act = () => shogi.Move(WhichPiece.Bishop, "E5"); + // Act - P2 places a Bishop while in check. + var act = () => shogi.Move(WhichPiece.Bishop, "E5"); - // Assert - using var scope = new AssertionScope(); - act.Should().Throw(); - shogi.BoardState["E5"].Should().BeNull(); - shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); - shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - } + // Assert + using var scope = new AssertionScope(); + act.Should().Throw(); + shogi.BoardState["E5"].Should().BeNull(); + shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); + shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + } - [Fact] - public void PreventInvalidDrop_Capture() - { - // Arrange - var shogi = MockShogiBoard(); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop capture P2 Bishop - shogi.Move("B2", "H8", false); - // P2 Pawn - shogi.Move("G6", "G5", false); - shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["I9"].Should().NotBeNull(); - shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + [Fact] + public void PreventInvalidDrop_Capture() + { + // Arrange + var shogi = MockShogiBoard(); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop capture P2 Bishop + shogi.Move("B2", "H8", false); + // P2 Pawn + shogi.Move("G6", "G5", false); + shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.BoardState["I9"].Should().NotBeNull(); + shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); + shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - // Act - P1 tries to place a piece where an opponent's piece resides. - var act = () => shogi.Move(WhichPiece.Bishop, "I9"); + // Act - P1 tries to place a piece where an opponent's piece resides. + var act = () => shogi.Move(WhichPiece.Bishop, "I9"); - // Assert - using var scope = new AssertionScope(); - act.Should().Throw(); - shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["I9"].Should().NotBeNull(); - shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - } + // Assert + using var scope = new AssertionScope(); + act.Should().Throw(); + shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.BoardState["I9"].Should().NotBeNull(); + shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); + shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + } - [Fact] - public void Check() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); + [Fact] + public void Check() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); - // Act - P1 Bishop, check - shogi.Move("B2", "G7", false); + // Act - P1 Bishop, check + shogi.Move("B2", "G7", false); - // Assert - board.InCheck.Should().Be(WhichPlayer.Player2); - } + // Assert + board.InCheck.Should().Be(WhichPlayer.Player2); + } - [Fact] - public void Promote() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); + [Fact] + public void Promote() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); - // Act - P1 moves across promote threshold. - shogi.Move("B2", "G7", true); + // Act - P1 moves across promote threshold. + shogi.Move("B2", "G7", true); - // Assert - using (new AssertionScope()) - { - board["B2"].Should().BeNull(); - board["G7"].Should().NotBeNull(); - board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G7"]!.IsPromoted.Should().BeTrue(); - } - } + // Assert + using (new AssertionScope()) + { + board["B2"].Should().BeNull(); + board["G7"].Should().NotBeNull(); + board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); + board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); + board["G7"]!.IsPromoted.Should().BeTrue(); + } + } - [Fact] - public void Capture() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var p1Bishop = board["B2"]; - p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Move("C3", "C4", false); - shogi.Move("G7", "G6", false); + [Fact] + public void Capture() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var p1Bishop = board["B2"]; + p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Move("C3", "C4", false); + shogi.Move("G7", "G6", false); - // Act - P1 Bishop captures P2 Bishop - shogi.Move("B2", "H8", false); + // Act - P1 Bishop captures P2 Bishop + shogi.Move("B2", "H8", false); - // Assert - board["B2"].Should().BeNull(); - board["H8"].Should().Be(p1Bishop); + // Assert + board["B2"].Should().BeNull(); + board["H8"].Should().Be(p1Bishop); - board - .Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - } + board + .Player1Hand + .Should() + .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + } - [Fact] - public void CheckMate() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Rook - shogi.Move("H2", "E2", false); - // P2 Gold - shogi.Move("F9", "G8", false); - // P1 Pawn - shogi.Move("E3", "E4", false); - // P2 other Gold - shogi.Move("D9", "C8", false); - // P1 same Pawn - shogi.Move("E4", "E5", false); - // P2 Pawn - shogi.Move("E7", "E6", false); - // P1 Pawn takes P2 Pawn - shogi.Move("E5", "E6", false); - // P2 King - shogi.Move("E9", "E8", false); - // P1 Pawn promotes; threatens P2 King - shogi.Move("E6", "E7", true); - // P2 King retreat - shogi.Move("E8", "E9", false); + [Fact] + public void CheckMate() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Rook + shogi.Move("H2", "E2", false); + // P2 Gold + shogi.Move("F9", "G8", false); + // P1 Pawn + shogi.Move("E3", "E4", false); + // P2 other Gold + shogi.Move("D9", "C8", false); + // P1 same Pawn + shogi.Move("E4", "E5", false); + // P2 Pawn + shogi.Move("E7", "E6", false); + // P1 Pawn takes P2 Pawn + shogi.Move("E5", "E6", false); + // P2 King + shogi.Move("E9", "E8", false); + // P1 Pawn promotes; threatens P2 King + shogi.Move("E6", "E7", true); + // P2 King retreat + shogi.Move("E8", "E9", false); - // Act - P1 Pawn wins by checkmate. - shogi.Move("E7", "E8", false); + // Act - P1 Pawn wins by checkmate. + shogi.Move("E7", "E8", false); - // Assert - checkmate - console.WriteLine(shogi.ToStringStateAsAscii()); - console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString()))); - board.IsCheckmate.Should().BeTrue(); - board.InCheck.Should().Be(WhichPlayer.Player2); - } + // Assert - checkmate + console.WriteLine(shogi.ToStringStateAsAscii()); + console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString()))); + board.IsCheckmate.Should().BeTrue(); + board.InCheck.Should().Be(WhichPlayer.Player2); + } - private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); - } + private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); + } }