using Gameboard.ShogiUI.BoardState.Pieces; using PathFinding; using System; using System.Collections.Generic; using System.Numerics; using System.Text; namespace Gameboard.ShogiUI.BoardState { /// /// 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 class ShogiBoard { private delegate void MoveSetCallback(Piece piece, Vector2 position); private readonly PathFinder2D pathFinder; public ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } public Array2D Board { get; } public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer? InCheck { get; private set; } public bool IsCheckmate { get; private set; } public ShogiBoard() { Board = new Array2D(9, 9); MoveHistory = new List(20); Hands = new Dictionary> { { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; pathFinder = new PathFinder2D(Board); InitializeBoardState(); player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); } public ShogiBoard(IList moves) : this() { for (var i = 0; i < moves.Count; i++) { if (!Move(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } } private ShogiBoard(ShogiBoard toCopy) { Board = new Array2D(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) Board[x, y] = toCopy.Board[x, y]?.DeepClone(); pathFinder = new PathFinder2D(Board); MoveHistory = new List(toCopy.MoveHistory); Hands = new Dictionary> { { WhichPlayer.Player1, new List(toCopy.Hands[WhichPlayer.Player1]) }, { WhichPlayer.Player2, new List(toCopy.Hands[WhichPlayer.Player2]) } }; player1King = toCopy.player1King; player2King = toCopy.player2King; } public bool Move(Move move) { var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) { return false; } // Evaluate check if (EvaluateCheckAfterMove(move, otherPlayer)) { InCheck = otherPlayer; IsCheckmate = EvaluateCheckmate(); } return true; } /// /// Attempts a given move. Returns false if the move is illegal. /// private bool TryMove(Move move) { // Try making the move in a "throw away" board. if (validationBoard == null) { validationBoard = new ShogiBoard(this); } var isValid = move.PieceFromCaptured.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); if (!isValid) { // Invalidate the "throw away" board. validationBoard = null; return false; } // If already in check, assert the move that resulted in check no longer results in check. if (InCheck == WhoseTurn) { if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) { // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; return false; } } // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } /// True if the move was successful. private bool PlaceFromHand(Move move) { if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); if (index < 0) return false; // Invalid move if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; switch (move.PieceFromCaptured.Value) { case WhichPiece.Knight: // Knight cannot be placed onto the farthest two ranks from the hand. minimumY = WhoseTurn == WhichPlayer.Player1 ? 2 : 6; break; case WhichPiece.Lance: case WhichPiece.Pawn: // Lance and Pawn cannot be placed onto the farthest rank from the hand. minimumY = WhoseTurn == WhichPlayer.Player1 ? 1 : 7; break; } if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false; if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; // Mutate the board. Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; Hands[WhoseTurn].RemoveAt(index); return true; } /// True if the move was successful. private bool PlaceFromBoard(Move move) { var fromPiece = Board[move.From.X, move.From.Y]; if (fromPiece == null) return false; // Invalid move if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. if (IsPathable(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) { if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. captured.Capture(); Hands[captured.Owner].Add(captured); } //Mutate the board. if (move.IsPromotion) { if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Y > 5)) { fromPiece.Promote(); } else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Y < 3)) { fromPiece.Promote(); } } Board[move.To.X, move.To.Y] = fromPiece; Board[move.From.X, move.From.Y] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPlayer.Player1) { player1King.X = move.To.X; player1King.Y = move.To.Y; } else if (fromPiece.Owner == WhichPlayer.Player2) { player2King.X = move.To.X; player2King.Y = move.To.Y; } } MoveHistory.Add(move); return true; } private bool IsPathable(Vector2 from, Vector2 to) { var piece = Board[from.X, from.Y]; if (piece == null) return false; var isObstructed = false; var isPathable = pathFinder.PathTo(from, to, (other, position) => { if (other.Owner == piece.Owner) isObstructed = true; }); return !isObstructed && isPathable; } public void PrintStateAsAscii() { var builder = new StringBuilder(); builder.Append(" Player 2"); builder.AppendLine(); for (var y = 8; y > -1; y--) { builder.Append("- "); for (var x = 0; x < 8; x++) builder.Append("- - "); builder.Append("- -"); builder.AppendLine(); builder.Append('|'); for (var x = 0; x < 9; x++) { var piece = Board[x, y]; if (piece == null) { builder.Append(" "); } else { builder.AppendFormat("{0}", piece.ShortName); } builder.Append('|'); } builder.AppendLine(); } builder.Append("- "); for (var x = 0; x < 8; x++) builder.Append("- - "); builder.Append("- -"); builder.AppendLine(); builder.Append(" Player 1"); Console.WriteLine(builder.ToString()); } #region Rules Validation private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) { var isCheck = false; var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; // Check if the move put the king in check. if (pathFinder.PathTo(move.To, kingPosition)) return true; // Get line equation from king through the now-unoccupied location. var direction = Vector2.Subtract(kingPosition, move.From); var slope = Math.Abs(direction.Y / direction.X); // 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)) { // if slope of the move is also infinity...can skip this? pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { if (piece.Owner != whichPlayer) { switch (piece.WhichPiece) { case WhichPiece.Rook: isCheck = true; break; case WhichPiece.Lance: if (!piece.IsPromoted) isCheck = true; break; } } }); } else if (slope == 1) { pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) { isCheck = true; } }); } else if (slope == 0) { pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) { isCheck = true; } }); } return isCheck; } private bool EvaluateCheckmate() { if (!InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; Board.ForEachNotNull((piece, x, y) => // For each piece... { // Short circuit if (!isCheckmate) return; var from = new Vector2(x, y); if (piece.Owner == InCheck) // ...owned by the player in check... { // ...evaluate if any move gets the player out of check. pathFinder.PathEvery(from, (other, position) => { if (validationBoard == null) validationBoard = new ShogiBoard(this); var moveToTry = new Move { From = from, To = position }; var moveSuccess = validationBoard.TryMove(moveToTry); if (moveSuccess) { validationBoard = null; if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) { isCheckmate = false; } } }); } }); return isCheckmate; } #endregion #region Initialize private void ResetEmptyRows() { for (int y = 3; y < 6; y++) for (int x = 0; x < 9; x++) Board[x, y] = null; } private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 2 : 6; for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); } private void ResetMiddleRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 1 : 7; Board[0, y] = null; for (int x = 2; x < 7; x++) Board[x, y] = null; Board[8, y] = null; if (player == WhichPlayer.Player1) { Board[1, y] = new Bishop(player); Board[7, y] = new Rook(player); } else { Board[1, y] = new Rook(player); Board[7, y] = new Bishop(player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 0 : 8; Board[0, y] = new Lance(player); Board[1, y] = new Knight(player); Board[2, y] = new SilverGeneral(player); Board[3, y] = new GoldenGeneral(player); Board[4, y] = new King(player); Board[5, y] = new GoldenGeneral(player); Board[6, y] = new SilverGeneral(player); Board[7, y] = new Knight(player); Board[8, y] = new Lance(player); } private void InitializeBoardState() { ResetRearRow(WhichPlayer.Player1); ResetMiddleRow(WhichPlayer.Player1); ResetFrontRow(WhichPlayer.Player1); ResetEmptyRows(); ResetFrontRow(WhichPlayer.Player2); ResetMiddleRow(WhichPlayer.Player2); ResetRearRow(WhichPlayer.Player2); } #endregion } }