From 6a600bf2f79cc44326c52b4e93706e713f75192f Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 3 Feb 2023 18:22:05 -0600 Subject: [PATCH] Finish logic around placing pieces from the hand. --- Shogi.Domain/BoardState.cs | 1 - Shogi.Domain/StandardRules.cs | 451 ++++++++++++------------ Shogi.Domain/ValueObjects/Move.cs | 16 +- Shogi.Domain/ValueObjects/ShogiBoard.cs | 53 +-- Tests/UnitTests/ShogiShould.cs | 33 +- 5 files changed, 289 insertions(+), 265 deletions(-) diff --git a/Shogi.Domain/BoardState.cs b/Shogi.Domain/BoardState.cs index c2b8ca4..2c54c74 100644 --- a/Shogi.Domain/BoardState.cs +++ b/Shogi.Domain/BoardState.cs @@ -17,7 +17,6 @@ public class BoardState playerInCheck: null, previousMove: new Move()); - public delegate void ForEachDelegate(Piece element, Vector2 position); /// /// Key is position notation, such as "E4". /// diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 15e5597..841720b 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -3,256 +3,261 @@ using BoardTile = System.Collections.Generic.KeyValuePair - /// Move a piece from a board tile to another board tile ignorant of check or check-mate. - /// - /// The position of the piece being moved expressed in board notation. - /// The target position expressed in board notation. - /// True if a promotion is expected as a result of this move. - /// A describing the success or failure of the move. - internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false) - { - var from = Notation.FromBoardNotation(fromNotation); - var to = Notation.FromBoardNotation(toNotation); - var fromPiece = boardState[from]; - if (fromPiece == null) - { - return new MoveResult(false, $"Tile [{fromNotation}] is empty. There is no piece to move."); - } + /// + /// Move a piece from a board tile to another board tile ignorant of check or check-mate. + /// + /// The position of the piece being moved expressed in board notation. + /// The target position expressed in board notation. + /// True if a promotion is expected as a result of this move. + /// A describing the success or failure of the move. + internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false) + { + var from = Notation.FromBoardNotation(fromNotation); + var to = Notation.FromBoardNotation(toNotation); + var fromPiece = boardState[from]; + if (fromPiece == null) + { + return new MoveResult(false, $"Tile [{fromNotation}] is empty. There is no piece to move."); + } - if (fromPiece.Owner != boardState.WhoseTurn) - { - return new MoveResult(false, "Not allowed to move the opponents piece"); - } + if (fromPiece.Owner != boardState.WhoseTurn) + { + return new MoveResult(false, "Not allowed to move the opponents piece"); + } - var path = fromPiece.GetPathFromStartToEnd(from, to); + var path = fromPiece.GetPathFromStartToEnd(from, to); - if (boardState.IsPathBlocked(path)) - { - return new MoveResult(false, "Another piece obstructs the desired move."); - } + if (boardState.IsPathBlocked(path)) + { + return new MoveResult(false, "Another piece obstructs the desired move."); + } - if (boardState[to] != null) - { - boardState.Capture(to); - } + if (boardState[to] != null) + { + boardState.Capture(to); + } - if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from))) - { - fromPiece.Promote(); - } + if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from))) + { + fromPiece.Promote(); + } - boardState[to] = fromPiece; - boardState[from] = null; + boardState[to] = fromPiece; + boardState[from] = null; - boardState.PreviousMove = new Move(from, to); - var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - boardState.WhoseTurn = otherPlayer; + boardState.PreviousMove = new Move(from, to); + var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + boardState.WhoseTurn = otherPlayer; - return new MoveResult(true); - } + return new MoveResult(true); + } - /// Move a piece from the hand to the board ignorant if check or check-mate. - /// - /// - /// The target position expressed in board notation. - /// A describing the success or failure of the simulation. - internal MoveResult Move(WhichPiece pieceInHand, string toNotation) - { - var to = Notation.FromBoardNotation(toNotation); - var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); - if (index == -1) - { - return new MoveResult(false, $"{pieceInHand} does not exist in the hand."); - } - if (boardState[to] != null) - { - return new MoveResult(false, $"Illegal move - attempting to capture while playing a piece from the hand."); - } + /// Move a piece from the hand to the board ignorant if check or check-mate. + /// + /// + /// The target position expressed in board notation. + /// A describing the success or failure of the simulation. + internal MoveResult Move(WhichPiece pieceInHand, string toNotation) + { + var to = Notation.FromBoardNotation(toNotation); + var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); + if (index == -1) + { + return new MoveResult(false, $"{pieceInHand} does not exist in the hand."); + } + if (boardState[to] != null) + { + return new MoveResult(false, "Illegal move - attempting to capture while playing a piece from the hand."); + } - switch (pieceInHand) - { - case WhichPiece.Knight: - { - // Knight cannot be placed onto the farthest two ranks from the hand. - if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) - || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) - { - return new MoveResult(false, "Knight has no valid moves after placed."); - } - 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 && to.Y == 8) - || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) - { - return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); - } - break; - } - } + switch (pieceInHand) + { + case WhichPiece.Knight: + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) + || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) + { + return new MoveResult(false, "Knight has no valid moves after placed."); + } + 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 && to.Y == 8) + || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) + { + return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); + } + break; + } + } - // Mutate the board. - boardState[to] = boardState.ActivePlayerHand[index]; - boardState.ActivePlayerHand.RemoveAt(index); - //MoveHistory.Add(move); - return new MoveResult(true); - } + // Mutate the board. + boardState[to] = boardState.ActivePlayerHand[index]; + boardState.ActivePlayerHand.RemoveAt(index); + boardState.PreviousMove = new Move(pieceInHand, to); + return new MoveResult(true); + } - /// - /// Determines if the last move put the player who moved in check. - /// - /// - /// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance. - /// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check. - /// - internal bool IsPlayerInCheckAfterMove() - { - var previousMovedPiece = boardState[boardState.PreviousMove.To]; - if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}."); - var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; + /// + /// Determines if the last move put the player who moved in check. + /// + /// + /// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance. + /// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check. + /// + internal bool DidPlayerPutThemselfInCheck() + { + if (boardState.PreviousMove.From == null) + { + // You can't place yourself in check by placing a piece from your hand. + return false; + } - var isCheck = false; + var previousMovedPiece = boardState[boardState.PreviousMove.To]; + if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}."); + var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From); - var slope = Math.Abs(direction.Y / direction.X); - var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From, Vector2.Normalize(direction)); - var threat = boardState.QueryFirstPieceInPath(path); - if (threat == null || threat.Owner == previousMovedPiece.Owner) return false; - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) - { - isCheck = threat.WhichPiece switch - { - WhichPiece.Lance => !threat.IsPromoted, - WhichPiece.Rook => true, - _ => false - }; - } - else if (slope == 1) - { - isCheck = threat.WhichPiece switch - { - WhichPiece.Bishop => true, - _ => false - }; - } - else if (slope == 0) - { - isCheck = threat.WhichPiece switch - { - WhichPiece.Rook => true, - _ => false - }; - } - return isCheck; - } + var isDiscoverCheck = false; + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From.Value); + var slope = Math.Abs(direction.Y / direction.X); + var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From.Value, Vector2.Normalize(direction)); + var threat = boardState.QueryFirstPieceInPath(path); + if (threat == null || threat.Owner == previousMovedPiece.Owner) return false; + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Lance => !threat.IsPromoted, + WhichPiece.Rook => true, + _ => false + }; + } + else if (slope == 1) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Bishop => true, + _ => false + }; + } + else if (slope == 0) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Rook => true, + _ => false + }; + } + return isDiscoverCheck; + } - internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); + internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); - internal bool IsOpposingKingThreatenedByPosition(Vector2 position) - { - var previousMovedPiece = boardState[position]; - if (previousMovedPiece == null) return false; + internal bool IsOpposingKingThreatenedByPosition(Vector2 position) + { + var previousMovedPiece = boardState[position]; + if (previousMovedPiece == null) return false; - var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition; - var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition); - var threatenedPiece = boardState.QueryFirstPieceInPath(path); - if (!path.Any() || threatenedPiece == null) return false; + var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition; + var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition); + var threatenedPiece = boardState.QueryFirstPieceInPath(path); + if (!path.Any() || threatenedPiece == null) return false; - return threatenedPiece.WhichPiece == WhichPiece.King; - } + return threatenedPiece.WhichPiece == WhichPiece.King; + } - internal bool IsOpponentInCheckMate() - { - // Assume checkmate, then try to disprove. - if (!boardState.InCheck.HasValue) return false; - // Get all pieces from opponent who threaten the king in question. - var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent); - var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1 - ? boardState.Player1KingPosition - : boardState.Player2KingPosition; - var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList(); - if (threats.Count == 1) - { - /* If there is exactly one threat it is possible to block the check. - * Foreach piece owned by whichPlayer - * if piece can intercept check, return false; - */ - var threat = threats.Single(); - var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition); - var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn); - foreach (var threatBlockingPosition in pathFromThreatToKing) - { - var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat - .Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition)) - .ToList(); + internal bool IsOpponentInCheckMate() + { + // Assume checkmate, then try to disprove. + if (!boardState.InCheck.HasValue) return false; + // Get all pieces from opponent who threaten the king in question. + var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent); + var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1 + ? boardState.Player1KingPosition + : boardState.Player2KingPosition; + var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList(); + if (threats.Count == 1) + { + /* If there is exactly one threat it is possible to block the check. + * Foreach piece owned by whichPlayer + * if piece can intercept check, return false; + */ + var threat = threats.Single(); + var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition); + var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn); + foreach (var threatBlockingPosition in pathFromThreatToKing) + { + var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat + .Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition)) + .ToList(); - if (tilesThatDoBlockThreat.Any()) - { - return false; // Cannot be check-mate if a piece can intercept the threat. - } - } - } - else - { - /* - * If no ability to block the check, maybe the king can evade check by moving. - */ + if (tilesThatDoBlockThreat.Any()) + { + return false; // Cannot be check-mate if a piece can intercept the threat. + } + } + } + else + { + /* + * If no ability to block the check, maybe the king can evade check by moving. + */ - foreach (var maybeSafePosition in GetPossiblePositionsForKing(this.boardState.WhoseTurn)) - { - threats = tilesOccupiedByOpponent - .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) - .ToList(); - if (!threats.Any()) - { - return false; - } - } + foreach (var maybeSafePosition in GetPossiblePositionsForKing(this.boardState.WhoseTurn)) + { + threats = tilesOccupiedByOpponent + .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) + .ToList(); + if (!threats.Any()) + { + return false; + } + } - } + } - return true; - } + return true; + } - private IList GetPossiblePositionsForKing(WhichPlayer whichPlayer) - { - var kingPosition = whichPlayer == WhichPlayer.Player1 - ? boardState.Player1KingPosition - : boardState.Player2KingPosition; + private IList GetPossiblePositionsForKing(WhichPlayer whichPlayer) + { + var kingPosition = whichPlayer == WhichPlayer.Player1 + ? boardState.Player1KingPosition + : boardState.Player2KingPosition; - return King.KingPaths - .Select(path => path.Direction + kingPosition) - .Where(newPosition => newPosition.IsInsideBoardBoundary()) - // Where tile at position is empty, meaning the king could move there. - .Where(newPosition => boardState[newPosition] == null) - .ToList(); - } + return King.KingPaths + .Select(path => path.Direction + kingPosition) + .Where(newPosition => newPosition.IsInsideBoardBoundary()) + // Where tile at position is empty, meaning the king could move there. + .Where(newPosition => boardState[newPosition] == null) + .ToList(); + } - private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) - { - var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); - return path - .SkipLast(1) - .All(position => boardState[Notation.ToBoardNotation(position)] == null); - } - } + private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) + { + var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); + return path + .SkipLast(1) + .All(position => boardState[Notation.ToBoardNotation(position)] == null); + } + } } diff --git a/Shogi.Domain/ValueObjects/Move.cs b/Shogi.Domain/ValueObjects/Move.cs index 384c344..a9d7e45 100644 --- a/Shogi.Domain/ValueObjects/Move.cs +++ b/Shogi.Domain/ValueObjects/Move.cs @@ -3,6 +3,20 @@ /// /// Represents a single piece being moved by a player from to . /// -public record struct Move(Vector2 From, Vector2 To) +public readonly record struct Move { + public Move(Vector2 from, Vector2 to) + { + From = from; + To = to; + } + public Move(WhichPiece pieceFromHand, Vector2 to) + { + PieceFromHand = pieceFromHand; + To = to; + } + + public Vector2? From { get; } + public Vector2 To { get; } + public WhichPiece PieceFromHand { get; } } diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs index cd4923e..116a962 100644 --- a/Shogi.Domain/ValueObjects/ShogiBoard.cs +++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs @@ -44,12 +44,14 @@ public sealed class ShogiBoard throw new InvalidOperationException("Unable to move because you are still in check."); } - var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (simulation.IsPlayerInCheckAfterMove()) + if (simulation.DidPlayerPutThemselfInCheck()) { throw new InvalidOperationException("Illegal move. This move places you in check."); } + var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1; _ = rules.Move(from, to, isPromotion); if (rules.IsOpponentInCheckAfterMove()) { @@ -113,31 +115,36 @@ public sealed class ShogiBoard throw new InvalidOperationException(moveResult.Reason); } - var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (BoardState.InCheck == BoardState.WhoseTurn) + // 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)) { - //if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn)) - //{ - // throw new InvalidOperationException("Illegal move. You're still in check!"); - //} + throw new InvalidOperationException("Unable to drop piece becauase you are still in check."); } - var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition; - //if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer)) - //{ + 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; + } + } - //rules.Move(from, to, isPromotion); - //if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) - //{ - // board.InCheck = otherPlayer; - // board.IsCheckmate = rules.EvaluateCheckmate(); - //} - //else - //{ - // board.InCheck = null; - //} + var kingPosition = otherPlayer == WhichPlayer.Player1 + ? tempBoard.Player1KingPosition + : tempBoard.Player2KingPosition; BoardState.WhoseTurn = otherPlayer; } @@ -149,7 +156,7 @@ public sealed class ShogiBoard { var builder = new StringBuilder(); builder.Append(" "); - builder.Append("Player 2(.)"); + builder.Append("Player 2"); builder.AppendLine(); for (var rank = 8; rank >= 0; rank--) { diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index dc17499..640fb56 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -1,5 +1,6 @@ using Shogi.Domain.ValueObjects; using System; +using System.Linq; namespace Shogi.Domain.UnitTests { @@ -299,15 +300,16 @@ namespace Shogi.Domain.UnitTests 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. - shogi.Move(WhichPiece.Bishop, "E5"); + 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); @@ -326,25 +328,21 @@ namespace Shogi.Domain.UnitTests shogi.Move("B2", "H8", false); // P2 Pawn shogi.Move("G6", "G5", false); - using (new AssertionScope()) - { - 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); - } + 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. - shogi.Move(WhichPiece.Bishop, "I9"); + var act = () => shogi.Move(WhichPiece.Bishop, "I9"); // Assert - using (new AssertionScope()) - { - 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); - } + 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] @@ -446,6 +444,7 @@ namespace Shogi.Domain.UnitTests // 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); }