using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Gameboard.ShogiUI.Sockets.Utilities; 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; private List Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand; public List Player1Hand { get; } public List Player2Hand { get; } public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method public List MoveHistory { get; } public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2; public WhichPerspective? InCheck { get; private set; } public bool IsCheckmate { get; private set; } public string Error { get; private set; } public Shogi() { Board = new CoordsToNotationCollection(); MoveHistory = new List(20); Player1Hand = new List(); Player2Hand = new List(); pathFinder = new PathFinder2D(Board, 9, 9); player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); 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 CoordsToNotationCollection(); foreach (var kvp in toCopy.Board) { Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); } pathFinder = new PathFinder2D(Board, 9, 9); MoveHistory = new List(toCopy.MoveHistory); Player1Hand = new List(toCopy.Player1Hand); Player2Hand = new List(toCopy.Player2Hand); player1King = toCopy.player1King; player2King = toCopy.player2King; Error = toCopy.Error; } public bool Move(Move move) { var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) { return false; } // Evaluate check if (EvaluateCheckAfterMove(move, otherPlayer)) { InCheck = otherPlayer; IsCheckmate = EvaluateCheckmate(); } else { InCheck = null; } 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) { // Surface the error description. Error = validationBoard.Error; // 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) { var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); if (index < 0) { Error = $"{move.PieceFromHand} does not exist in the hand."; return false; } if (Board[move.To] != null) { Error = $"Illegal move - attempting to capture while playing a piece from the hand."; return false; } switch (move.PieceFromHand!.Value) { case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6) || (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2)) { Error = $"Knight has no valid moves after placed."; return false; } break; } case WhichPiece.Lance: case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8) || (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0)) { Error = $"{move.PieceFromHand} has no valid moves after placed."; return false; } break; } } // Mutate the board. Board[move.To] = Hand[index]; Hand.RemoveAt(index); MoveHistory.Add(move); return true; } /// True if the move was successful. private bool PlaceFromBoard(Move move) { var fromPiece = Board[move.From!.Value]; 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]; if (captured != null) { if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. captured.Capture(); Hand.Add(captured); } //Mutate the board. if (move.IsPromotion) { if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5)) { fromPiece.Promote(); } else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3)) { fromPiece.Promote(); } } Board[move.To] = fromPiece; Board[move.From!.Value] = null; if (fromPiece.WhichPiece == WhichPiece.King) { if (fromPiece.Owner == WhichPerspective.Player1) { player1King.X = move.To.X; player1King.Y = move.To.Y; } else if (fromPiece.Owner == WhichPerspective.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]; 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, WhichPerspective WhichPerspective) { if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. var isCheck = false; var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King; // Check if the move put the king in check. if (pathFinder.PathTo(move.To, kingPosition)) return true; if (move.From.HasValue) { // 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 != WhichPerspective) { 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 != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) { isCheck = true; } }); } else if (slope == 0) { pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) { isCheck = true; } }); } } else { // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. // Go read the shogi rules to be sure this is true. } return isCheck; } private bool EvaluateCheckmate() { if (!InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; Board.ForEachNotNull((piece, from) => // For each piece... { // Short circuit if (!isCheckmate) return; 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 private void InitializeBoardState() { Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1); Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1); Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1); Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1); Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1); Board["A2"] = null; Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1); Board["C2"] = null; Board["D2"] = null; Board["E2"] = null; Board["F2"] = null; Board["G2"] = null; Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1); Board["I2"] = null; Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1); Board["A4"] = null; Board["B4"] = null; Board["C4"] = null; Board["D4"] = null; Board["E4"] = null; Board["F4"] = null; Board["G4"] = null; Board["H4"] = null; Board["I4"] = null; Board["A5"] = null; Board["B5"] = null; Board["C5"] = null; Board["D5"] = null; Board["E5"] = null; Board["F5"] = null; Board["G5"] = null; Board["H5"] = null; Board["I5"] = null; Board["A6"] = null; Board["B6"] = null; Board["C6"] = null; Board["D6"] = null; Board["E6"] = null; Board["F6"] = null; Board["G6"] = null; Board["H6"] = null; Board["I6"] = null; Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2); Board["A8"] = null; Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2); Board["C8"] = null; Board["D8"] = null; Board["E8"] = null; Board["F8"] = null; Board["G8"] = null; Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2); Board["I8"] = null; Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2); Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2); Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2); Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2); Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2); } public BoardState ToServiceModel() { return new BoardState { Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()), PlayerInCheck = InCheck, WhoseTurn = WhoseTurn, Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(), Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList() }; } } }