using System; using System.Collections.Generic; 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, BoardVector position); private ShogiBoard validationBoard; public IReadOnlyDictionary> Hands { get; } public Piece[,] 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 Piece[9, 9]; MoveHistory = new List(20); Hands = new Dictionary> { { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; InitializeBoardState(); } public ShogiBoard(IList moves) : this() { for (var i = 0; i < moves.Count; i++) { if (!TryMove(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } } /// /// Attempts a given move. Returns false if the move is illegal. /// public bool TryMove(Move move) { // Try making the move in a "throw away" board. if (validationBoard == null) { validationBoard = new ShogiBoard(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; var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); // Evaluate check InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; if (InCheck.HasValue) { //IsCheckmate = EvaluateCheckmate(); } 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 BoardVector(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) { var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); if (moveSuccess) { Console.WriteLine($"Not check mate"); 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) == 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; 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 inCheck = false; // Iterate every board piece... Board.ForEachNotNull((piece, x, y) => { // ...that belongs to the opponent... if (piece.Owner != whichPlayer) { IterateMoveSet(new BoardVector(x, y), (threatenedPiece, position) => { // ...and threatens the player's king. inCheck |= threatenedPiece?.WhichPiece == WhichPiece.King && threatenedPiece?.Owner == whichPlayer; }); } }); return inCheck; } private bool EvaluateCheck2(WhichPlayer whichPlayer) { var inCheck = false; MoveSetCallback checkKingThreat = (piece, position) => { inCheck |= piece?.WhichPiece == WhichPiece.King && piece?.Owner == whichPlayer; }; // Find interesting pieces var longRangePiecePositions = new List(8); Board.ForEachNotNull((piece, x, y) => { if (piece.Owner != whichPlayer) { switch (piece.WhichPiece) { case WhichPiece.Bishop: case WhichPiece.Rook: longRangePiecePositions.Add(new BoardVector(x, y)); break; case WhichPiece.Lance: if (!piece.IsPromoted) longRangePiecePositions.Add(new BoardVector(x, y)); break; } } }); foreach(var position in longRangePiecePositions) { IterateMoveSet(position, checkKingThreat); } return inCheck; } private bool ValidateMoveAgainstMoveSet(Move move) { var isValid = false; IterateMoveSet(move.From, (piece, position) => { if (piece?.Owner != WhoseTurn && position == move.To) { isValid = true; } }); return isValid; } /// /// Iterate through the possible moves of a piece at a given position. /// private void IterateMoveSet(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector 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(BoardVector from, BoardVector direction, MoveSetCallback callback) { var foundAnotherPiece = false; var to = from.Add(direction); while (to.IsValidBoardPosition && !foundAnotherPiece) { var piece = Board[to.X, to.Y]; callback(piece, to); to = to.Add(direction); foundAnotherPiece = piece != null; } } /// /// Useful for iterating the board for pieces that move only one space. /// private void BoardStep(BoardVector from, BoardVector direction, MoveSetCallback callback) { var to = from.Add(direction); if (to.IsValidBoardPosition) { 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 } }