using Shogi.Domain.Pathing; using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { internal class StandardRules { private readonly ShogiBoardState boardState; internal StandardRules(ShogiBoardState board) { this.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 = ShogiBoardState.FromBoardNotation(fromNotation); var to = ShogiBoardState.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.RememberAsMostRecentMove(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 = ShogiBoardState.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.PreviousMoveTo]; if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMoveTo}."); 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.PreviousMoveFrom); var slope = Math.Abs(direction.Y / direction.X); var path = ShogiBoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, 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.PreviousMoveTo); 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 IsPlayerInCheckMate(WhichPlayer whichPlayer) { if (!boardState.InCheck.HasValue) return false; // Get all pieces from "other player" who threaten the king in question. var otherPlayer = whichPlayer == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var tilesOccupiedByOtherPlayer = boardState.GetTilesOccupiedBy(otherPlayer); var kingPosition = whichPlayer == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; var threats = tilesOccupiedByOtherPlayer.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(whichPlayer); 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. */ // TODO: Implement this in the Piece class instead. var possibleSafePositions = new[] { Direction.Up, Direction.Down, Direction.Left, Direction.Right, Direction.UpLeft, Direction.UpRight, Direction.DownLeft, Direction.DownRight } .Select(direction => kingPosition + direction); foreach (var maybeSafePosition in possibleSafePositions) { } } return false; } private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) { var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); return path .SkipLast(1) .All(position => this.boardState[ShogiBoardState.ToBoardNotation(position)] == null); } public bool EvaluateCheckmate_Old() { if (!boardState.InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; //boardState.ForEachNotNull((piece, from) => // For each piece... //{ // Short circuit //if (!isCheckmate) return; //if (piece.Owner == boardState.InCheck) // ...owned by the player in check... //{ // ...evaluate if any move gets the player out of check. //PathEvery(from, (other, position) => //{ // var simulationBoard = new StandardRules(new ShogiBoardState(board)); // var fromNotation = ShogiBoardState.ToBoardNotation(from); // var toNotation = ShogiBoardState.ToBoardNotation(position); // var simulationResult = simulationBoard.Move(fromNotation, toNotation, false); // if (simulationResult.Success) // { // //if (!IsPlayerInCheckAfterMove(from, position, board.InCheck.Value)) // //{ // // isCheckmate = false; // //} // } //}); //} // TODO: Assert that a player could not place a piece from their hand to avoid check. //}); return isCheckmate; } } }