using Shogi.Domain.YetToBeAssimilatedIntoDDD; 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 readonly StandardRules rules; private static readonly Vector2 BoardSize = new Vector2(9, 9); public ShogiBoard(BoardState initialState) { BoardState = initialState; rules = new StandardRules(BoardState); } 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 = false) { // 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; } 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."); // Look for threats against the kings. var inCheckResult = 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 candidatePositions = threatPiece.GetPathFromStartToEnd(Notation.FromBoardNotation(kvp.Key), opposingKingPosition); foreach (var position in candidatePositions) { // No piece at this position, so pathing is unobstructed. Continue pathing. if (simState[position] == null) continue; var pieceAtPosition = simState[position]!; if (pieceAtPosition.WhichPiece == WhichPiece.King && pieceAtPosition.Owner != threatPiece.Owner) { newInCheckResult &= pieceAtPosition.Owner == WhichPlayer.Player1 ? InCheckResult.Player2InCheck : InCheckResult.Player1InCheck; } else { break; } } return newInCheckResult; }); 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 illega."); } // Move is legal; mutate the real state. BoardState.Move(from, to, isPromotion); var playerPutOpponentInCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? inCheckResult.HasFlag(InCheckResult.Player2InCheck) : inCheckResult.HasFlag(InCheckResult.Player1InCheck); if (playerPutOpponentInCheck) { BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; } // TODO: Look for check-mate. return new MoveResult(true); //var simulation = new StandardRules(simState); //// 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)) //{ // throw new InvalidOperationException("Unable to move because you are still in check."); //} //if (simulation.DidPlayerPutThemselfInCheck()) //{ // throw new InvalidOperationException("Illegal move. This move places you in check."); //} //var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 // ? WhichPlayer.Player2 // : WhichPlayer.Player1; //_ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that. //if (rules.IsOpponentInCheckAfterMove()) //{ // BoardState.InCheck = otherPlayer; // if (rules.IsOpponentInCheckMate()) // { // BoardState.IsCheckmate = true; // } //} //else //{ // BoardState.InCheck = null; //} //BoardState.WhoseTurn = otherPlayer; } public void Move(WhichPiece pieceInHand, string to) { //var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); //if (index == -1) //{ // throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); //} //if (BoardState[to] != null) //{ // throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); //} //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) // { // throw new InvalidOperationException("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) // { // throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); // } // break; // } //} //var tempBoard = new BoardState(BoardState); //var simulation = new StandardRules(tempBoard); //var moveResult = simulation.Move(pieceInHand, to); //if (!moveResult.Success) //{ // throw new InvalidOperationException(moveResult.Reason); //} //// 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)) //{ // throw new InvalidOperationException("Unable to drop piece becauase you are still in check."); //} //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; // } //} //var kingPosition = otherPlayer == WhichPlayer.Player1 // ? tempBoard.Player1KingPosition // : tempBoard.Player2KingPosition; //BoardState.WhoseTurn = otherPlayer; } /// /// 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; } }