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 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()}, }; InitializeBoardState(); player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); } public bool Move(Move move) { var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) return false; // Evaluate check InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; if (InCheck.HasValue) { //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 = ConstructWithMoves(MoveHistory); } var isValid = move.PieceFromCaptured.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); if (!isValid) { // Invalidate the "throw away" board. validationBoard = null; return false; } // Assert that this move does not put the moving player in check. if (validationBoard.EvaluateCheck(WhoseTurn)) return false; // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } 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... { if (!isCheckmate) return; // Short circuit var from = new Vector2(x, y); if (piece.Owner == InCheck) // Owned by the player in check... { var positionsToCheck = new List(10); IterateMoveSet(from, (innerPiece, position) => { if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... }); // And evaluate if any move gets the player out of check. foreach (var position in positionsToCheck) { if (validationBoard == null) validationBoard = ConstructWithMoves(MoveHistory); var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); if (moveSuccess) { isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); validationBoard = null; } } } }); return isCheckmate; } /// 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 (ValidateMoveAgainstMoveSet(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; } 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 /// /// Evaluate if a player is in check given the current board state. /// private bool EvaluateCheck(WhichPlayer whichPlayer) { var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; var inCheck = false; // Iterate every board piece... Board.ForEachNotNull((piece, x, y) => { var v = new Vector2(x, y); // ...that belongs to the opponent within range... if (piece.Owner != whichPlayer && (piece.IsRanged || Vector2.Distance(kingPosition, v) < 3)) { IterateMoveSet(new Vector2(x, y), (threatenedPiece, position) => { // ...and threatens the player's king. inCheck |= threatenedPiece?.WhichPiece == WhichPiece.King && threatenedPiece?.Owner == whichPlayer; }); } }); return inCheck; } private bool ValidateMoveAgainstMoveSet(Vector2 from, Vector2 to) { var isValid = false; IterateMoveSet(from, (piece, position) => { if (piece?.Owner != WhoseTurn && position == to) { isValid = true; } }); return isValid; } /// /// Iterate through the possible moves of a piece at a given position. /// private void IterateMoveSet(Vector2 from, MoveSetCallback callback) { // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. var piece = Board[from.X, from.Y]; switch (piece?.WhichPiece) { case WhichPiece.King: IterateKingMoveSet(from, callback); break; case WhichPiece.GoldenGeneral: IterateGoldenGeneralMoveSet(from, callback); break; case WhichPiece.SilverGeneral: IterateSilverGeneralMoveSet(from, callback); break; case WhichPiece.Bishop: IterateBishopMoveSet(from, callback); break; case WhichPiece.Rook: IterateRookMoveSet(from, callback); break; case WhichPiece.Knight: IterateKnightMoveSet(from, callback); break; case WhichPiece.Lance: IterateLanceMoveSet(from, callback); break; case WhichPiece.Pawn: IteratePawnMoveSet(from, callback); break; } } private void IterateKingMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); BoardStep(from, direction.Up, callback); BoardStep(from, direction.UpLeft, callback); BoardStep(from, direction.UpRight, callback); BoardStep(from, direction.Down, callback); BoardStep(from, direction.DownLeft, callback); BoardStep(from, direction.DownRight, callback); BoardStep(from, direction.Left, callback); BoardStep(from, direction.Right, callback); } private void IterateGoldenGeneralMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); BoardStep(from, direction.Up, callback); BoardStep(from, direction.UpLeft, callback); BoardStep(from, direction.UpRight, callback); BoardStep(from, direction.Down, callback); BoardStep(from, direction.Left, callback); BoardStep(from, direction.Right, callback); } private void IterateSilverGeneralMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); if (piece.IsPromoted) { IterateGoldenGeneralMoveSet(from, callback); } else { BoardStep(from, direction.Up, callback); BoardStep(from, direction.UpLeft, callback); BoardStep(from, direction.UpRight, callback); BoardStep(from, direction.DownLeft, callback); BoardStep(from, direction.DownRight, callback); } } private void IterateBishopMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); BoardWalk(from, direction.UpLeft, callback); BoardWalk(from, direction.UpRight, callback); BoardWalk(from, direction.DownLeft, callback); BoardWalk(from, direction.DownRight, callback); if (piece.IsPromoted) { BoardStep(from, direction.Up, callback); BoardStep(from, direction.Left, callback); BoardStep(from, direction.Right, callback); BoardStep(from, direction.Down, callback); } } private void IterateRookMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; var direction = new Direction(piece.Owner); BoardWalk(from, direction.Up, callback); BoardWalk(from, direction.Left, callback); BoardWalk(from, direction.Right, callback); BoardWalk(from, direction.Down, callback); if (piece.IsPromoted) { BoardStep(from, direction.UpLeft, callback); BoardStep(from, direction.UpRight, callback); BoardStep(from, direction.DownLeft, callback); BoardStep(from, direction.DownRight, callback); } } private void IterateKnightMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece.IsPromoted) { IterateGoldenGeneralMoveSet(from, callback); } else { var direction = new Direction(piece.Owner); BoardStep(from, direction.KnightLeft, callback); BoardStep(from, direction.KnightRight, callback); } } private void IterateLanceMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece.IsPromoted) { IterateGoldenGeneralMoveSet(from, callback); } else { var direction = new Direction(piece.Owner); BoardWalk(from, direction.Up, callback); } } private void IteratePawnMoveSet(Vector2 from, MoveSetCallback callback) { var piece = Board[from.X, from.Y]; if (piece?.WhichPiece == WhichPiece.Pawn) { if (piece.IsPromoted) { IterateGoldenGeneralMoveSet(from, callback); } else { var direction = new Direction(piece.Owner); BoardStep(from, direction.Up, callback); } } } /// /// Useful for iterating the board for pieces that move many spaces. /// /// A function that returns true if walking should continue. private void BoardWalk(Vector2 from, Vector2 direction, MoveSetCallback callback) { var foundAnotherPiece = false; var to = Vector2.Add(from, direction); while (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9 && !foundAnotherPiece) { var piece = Board[to.X, to.Y]; callback(piece, to); to = Vector2.Add(to, direction); foundAnotherPiece = piece != null; } } /// /// Useful for iterating the board for pieces that move only one space. /// private void BoardStep(Vector2 from, Vector2 direction, MoveSetCallback callback) { var to = Vector2.Add(from, direction); if (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9) { callback(Board[to.X, to.Y], to); } } #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 Piece(WhichPiece.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 Piece(WhichPiece.Bishop, player); Board[7, y] = new Piece(WhichPiece.Rook, player); } else { Board[1, y] = new Piece(WhichPiece.Rook, player); Board[7, y] = new Piece(WhichPiece.Bishop, player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 0 : 8; Board[0, y] = new Piece(WhichPiece.Lance, player); Board[1, y] = new Piece(WhichPiece.Knight, player); Board[2, y] = new Piece(WhichPiece.SilverGeneral, player); Board[3, y] = new Piece(WhichPiece.GoldenGeneral, player); Board[4, y] = new Piece(WhichPiece.King, player); Board[5, y] = new Piece(WhichPiece.GoldenGeneral, player); Board[6, y] = new Piece(WhichPiece.SilverGeneral, player); Board[7, y] = new Piece(WhichPiece.Knight, player); Board[8, y] = new Piece(WhichPiece.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 public static ShogiBoard ConstructWithMoves(IList moves) { var s = new ShogiBoard(); for (var i = 0; i < moves.Count; i++) { if (!s.Move(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } return s; } } }