using Shogi.Domain.YetToBeAssimilatedIntoDDD; using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain.ValueObjects { internal class StandardRules { private readonly BoardState boardState; internal StandardRules(BoardState board) { boardState = board; } /// /// 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 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 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 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(boardState.WhoseTurn)) { threats = tilesOccupiedByOpponent .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) .ToList(); if (!threats.Any()) { return false; } } } return true; } private List GetPossiblePositionsForKing(WhichPlayer whichPlayer) { var kingPosition = whichPlayer == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; var paths = boardState[kingPosition]!.MoveSet; return paths .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. .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); } } }