using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using PathFinding; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; namespace Gameboard.ShogiUI.Sockets.Models { /// /// 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 Shogi { private delegate void MoveSetCallback(Piece piece, Vector2 position); private readonly PathFinder2D pathFinder; private Shogi? validationBoard; private Vector2 player1King; private Vector2 player2King; public IReadOnlyDictionary> Hands { get; } public PlanarCollection Board { get; } //TODO: Hide this being a getter method 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 string Error { get; private set; } public Shogi() { Board = new PlanarCollection(9, 9); MoveHistory = new List(20); Hands = new Dictionary> { { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; pathFinder = new PathFinder2D(Board); player1King = new Vector2(4, 8); player2King = new Vector2(4, 0); Error = string.Empty; InitializeBoardState(); } public Shogi(IList moves) : this() { for (var i = 0; i < moves.Count; i++) { if (!Move(moves[i])) { // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); } } } private Shogi(Shogi toCopy) { Board = new PlanarCollection(9, 9); for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) { var piece = toCopy.Board[y, x]; if (piece != null) { Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted); } } 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; Error = toCopy.Error; } 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 Shogi(this); } var isValid = move.PieceFromHand.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.PieceFromHand.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } /// True if the move was successful. private bool PlaceFromHand(Move move) { if (move.PieceFromHand.HasValue == false) return false; //Invalid move var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) return false; // Invalid move if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand. var minimumY = 0; switch (move.PieceFromHand.Value) { case WhichPiece.Knight: // Knight cannot be placed onto the farthest two ranks from the hand. minimumY = WhoseTurn == WhichPlayer.Player1 ? 6 : 2; break; case WhichPiece.Lance: case WhichPiece.Pawn: // Lance and Pawn cannot be placed onto the farthest rank from the hand. minimumY = WhoseTurn == WhichPlayer.Player1 ? 7 : 1; 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.Y, move.To.X] = 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.Value.Y, move.From.Value.X]; if (fromPiece == null) { Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; return false; // Invalid move } if (fromPiece.Owner != WhoseTurn) { Error = "Not allowed to move the opponents piece"; return false; // Invalid move; cannot move other players pieces. } if (IsPathable(move.From.Value, move.To) == false) { Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; return false; // Invalid move; move not part of move-set. } var captured = Board[move.To.Y, move.To.X]; 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 < 3 || move.From.Value.Y < 3)) { fromPiece.Promote(); } else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5)) { fromPiece.Promote(); } } Board[move.To.Y, move.To.X] = fromPiece; Board[move.From.Value.Y, move.From.Value.X] = 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.Y, from.X]; 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; } #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.Value); 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 Shogi(this); var moveToTry = new Move(from, 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[y, x] = null; } private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 6 : 2; for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player); } private void ResetMiddleRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 7 : 1; Board[y, 0] = null; for (int x = 2; x < 7; x++) Board[y, x] = null; Board[y, 8] = null; if (player == WhichPlayer.Player1) { Board[y, 1] = new Piece(WhichPiece.Bishop, player); Board[y, 7] = new Piece(WhichPiece.Rook, player); } else { Board[y, 1] = new Piece(WhichPiece.Rook, player); Board[y, 7] = new Piece(WhichPiece.Bishop, player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 8 : 0; Board[y, 0] = new Piece(WhichPiece.Lance, player); Board[y, 1] = new Piece(WhichPiece.Knight, player); Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player); Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player); Board[y, 4] = new Piece(WhichPiece.King, player); Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player); Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player); Board[y, 7] = new Piece(WhichPiece.Knight, player); Board[y, 8] = new Piece(WhichPiece.Lance, player); } private void InitializeBoardState() { ResetRearRow(WhichPlayer.Player2); ResetMiddleRow(WhichPlayer.Player2); ResetFrontRow(WhichPlayer.Player2); ResetEmptyRows(); ResetFrontRow(WhichPlayer.Player1); ResetMiddleRow(WhichPlayer.Player1); ResetRearRow(WhichPlayer.Player1); } #endregion public BoardState ToServiceModel() { var board = new ServiceModels.Socket.Types.Piece[9, 9]; for (var x = 0; x < 9; x++) for (var y = 0; y < 9; y++) { var piece = Board[y, x]; if (piece != null) { board[y, x] = piece.ToServiceModel(); } } return new BoardState { Board = board, PlayerInCheck = InCheck, WhoseTurn = WhoseTurn, Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(), Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList() }; } } }