using Shogi.Domain.ValueObjects; using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { internal class StandardRules { private readonly BoardState boardState; internal StandardRules(BoardState board) { boardState = board; } /// /// 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"); } var path = fromPiece.GetPathFromStartToEnd(from, to); if (boardState.IsPathBlocked(path)) { return new MoveResult(false, "Another piece obstructs the desired move."); } if (boardState[to] != null) { boardState.Capture(to); } if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from))) { fromPiece.Promote(); } 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; 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."); } 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); } /// /// 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; var isCheck = false; // 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; } internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); 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; 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(); 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; } } } return true; } 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(); } 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); } } }