using Shogi.Domain.YetToBeAssimilatedIntoDDD; using System.Threading.Tasks; namespace Shogi.Domain.ValueObjects; /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. /// The board is always from Player1's perspective. /// [0,0] is the lower-left position, [8,8] is the higher-right position /// public sealed class ShogiBoard { private static readonly int[] zeroToEight = [0, 1, 2, 3, 4, 5, 6, 7, 8]; private static readonly Vector2 BoardSize = new(9, 9); public ShogiBoard(BoardState initialState) { BoardState = initialState; } public BoardState BoardState { get; } /// /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. /// /// /// The strategy involves simulating a move on a throw-away board state that can be used to /// validate legal vs illegal moves without having to worry about reverting board state. /// /// public MoveResult Move(string from, string to, bool isPromotion) { // Validate the move var moveResult = IsMoveValid(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to)); if (!moveResult.IsSuccess) { return moveResult; } // Move is valid, but is it legal? // Check for correct player's turn. if (BoardState.WhoseTurn != BoardState[from]!.Owner) { 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 simState = new BoardState(BoardState); moveResult = simState.Move(from, to, isPromotion); if (!moveResult.IsSuccess) { return moveResult; } // Look for threats against the kings. InCheckResult inCheckResult = IsEitherPlayerInCheck(simState); var playerPutThemselfInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? inCheckResult.HasFlag(InCheckResult.Player1InCheck) : inCheckResult.HasFlag(InCheckResult.Player2InCheck); if (playerPutThemselfInCheck) { return new MoveResult(false, "This move puts the moving player in check, which is illegal."); } var playerPutOpponentInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? inCheckResult.HasFlag(InCheckResult.Player2InCheck) : inCheckResult.HasFlag(InCheckResult.Player1InCheck); // Move is legal; mutate the real state. BoardState.Move(from, to, isPromotion); if (playerPutOpponentInCheck) { BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; } return new MoveResult(true); } public MoveResult Move(WhichPiece pieceInHand, string to) { 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, $"Tried to play a piece from the hand to an occupied position."); } 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) { return new MoveResult(false, "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) { return new MoveResult(false, $"Illegal move. {pieceInHand} has no valid moves after placement."); } break; } } if (pieceInHand == WhichPiece.Pawn) { // Pawns cannot be placed into a column with another unpromoted pawn controlled by the moving player. var columnAlreadyHasPawn = zeroToEight .Select(y => BoardState[new Vector2(toVector.X, y)]) .Where(piece => piece?.WhichPiece == WhichPiece.Pawn) .Where(piece => piece?.Owner == BoardState.WhoseTurn) .Where(piece => piece?.IsPromoted == false) .Any(); if (columnAlreadyHasPawn) { return new MoveResult(false, "A player may not have two unpromoted pawns in the same file."); } } var simState = new BoardState(BoardState); var moveResult = simState.Move(pieceInHand, to); if (!moveResult.IsSuccess) { return moveResult; } var inCheckResult = IsEitherPlayerInCheck(simState); var playerPutThemselfInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? inCheckResult.HasFlag(InCheckResult.Player1InCheck) : inCheckResult.HasFlag(InCheckResult.Player2InCheck); if (playerPutThemselfInCheck) { return new MoveResult(false, "This move puts the moving player in check, which is illegal."); } var playerPutOpponentInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? inCheckResult.HasFlag(InCheckResult.Player2InCheck) : inCheckResult.HasFlag(InCheckResult.Player1InCheck); // Move is legal; mutate the real state. BoardState.Move(pieceInHand, to); if (playerPutOpponentInCheck) { BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; } // A pawn, placed from the hand, cannot be the cause of checkmate. // if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) // { // BoardState.IsCheckmate = true; // } return new MoveResult(true); } private async Task EvaluateGameOver() { if (!BoardState.InCheck.HasValue) { return GameOverResult.GameIsNotOver; } var kingInCheck = BoardState.State .Single(kvp => kvp.Value?.WhichPiece == WhichPiece.King && kvp.Value?.Owner == BoardState.InCheck); var kingInCheckVectorPosition = Notation.FromBoardNotation(kingInCheck.Key); var piecesOfPlayerInCheck = BoardState.State .Where(kvp => kvp.Value?.Owner == BoardState.InCheck) .Cast>(); foreach (var (notation, piece) in piecesOfPlayerInCheck) { // Get possible locations this piece could move to. var allPossibleMoves = GetPossiblePositionsForPiece(Notation.FromBoardNotation(notation), piece); // Try to make a legal move, disproving checkmate. foreach (var move in allPossibleMoves) { var simState = new BoardState(BoardState); simState.Move(notation, Notation.ToBoardNotation(move), false); var inCheckResult = IsEitherPlayerInCheck(simState); if (inCheckResult == InCheckResult.) } } // while (tasks.Any()) var result = Task.WhenAny(tasks); // Then check for GameIsNotOver and maybe return early. var gameOverResult = BoardState.State .Where(kvp => kvp.Value != null) .Cast>() .Aggregate(GameOverResult.GameIsNotOver, (inCheckResult, kvp) => { var newInCheckResult = inCheckResult; var threatPiece = kvp.Value; var opposingKingPosition = Notation.FromBoardNotation(kingInCheck.Single(king => king.Value.Owner != threatPiece.Owner).Key); var positionsThreatened = threatPiece.GetPathFromStartToEnd(Notation.FromBoardNotation(kvp.Key), opposingKingPosition); foreach (var position in positionsThreatened) { // No piece at this position, so pathing is unobstructed. Continue pathing. if (simState[position] == null) continue; var threatenedPiece = simState[position]!; if (threatenedPiece.WhichPiece == WhichPiece.King && threatenedPiece.Owner != threatPiece.Owner) { newInCheckResult |= threatenedPiece.Owner == WhichPlayer.Player1 ? InCheckResult.Player1InCheck : InCheckResult.Player2InCheck; } else { break; } } return newInCheckResult; }); return GameOverResult.GameIsNotOver; Vector2[] GetPossiblePositionsForPiece(Vector2 piecePosition, Piece piece) { var paths = piece.MoveSet; return paths .SelectMany(path => { var list = new List(10); var position = path.Step + piecePosition; while (position.IsInsideBoardBoundary()) { list.Add(position); position += path.Step; } return list; }) // Where tile at position is empty, meaning the piece could move there. .Where(newPosition => BoardState[newPosition] == null) .ToArray(); } } private static InCheckResult IsEitherPlayerInCheck(BoardState simState) { 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."); return 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 positionsThreatened = threatPiece.GetPathFromStartToEnd(Notation.FromBoardNotation(kvp.Key), opposingKingPosition); foreach (var position in positionsThreatened) { // No piece at this position, so pathing is unobstructed. Continue pathing. if (simState[position] == null) continue; var threatenedPiece = simState[position]!; if (threatenedPiece.WhichPiece == WhichPiece.King && threatenedPiece.Owner != threatPiece.Owner) { newInCheckResult |= threatenedPiece.Owner == WhichPlayer.Player1 ? InCheckResult.Player1InCheck : InCheckResult.Player2InCheck; } else { break; } } return newInCheckResult; }); } /// /// 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). /// private MoveResult IsMoveValid(Vector2 from, Vector2 to) { if (IsWithinBounds(from) && IsWithinBounds(to)) { if (BoardState[to]?.WhichPiece == WhichPiece.King) { return new MoveResult(false, "Kings may not be captured."); } 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."); } } return new MoveResult(true); } private static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, YetToBeAssimilatedIntoDDD.Pathing.Path path) { var next = from; while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9) { next += path.Step; yield return next; } } private static bool IsWithinBounds(Vector2 position) { var isPositive = position - position == Vector2.Zero; return isPositive && position.X <= BoardSize.X && position.Y <= BoardSize.Y; } }