using Shogi.Domain.Pieces; using System.Text; namespace Shogi.Domain { /// /// 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 sealed class Shogi { private readonly ShogiBoardState boardState; private readonly StandardRules rules; public Shogi() : this(new ShogiBoardState()) { } public Shogi(ShogiBoardState board) { this.rules = new StandardRules(board); this.boardState = board; } /// /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. /// /// /// The strategy involves simulating a move on a throw-away board state that can be used to /// validate legal vs illegal moves without having to worry about reverting board state. /// /// public void Move(string from, string to, bool isPromotion) { var simulationState = new ShogiBoardState(boardState); var simulation = new StandardRules(simulationState); var moveResult = simulation.Move(from, to, isPromotion); if (!moveResult.Success) { throw new InvalidOperationException(moveResult.Reason); } // If already in check, assert the move that resulted in check no longer results in check. if (boardState.InCheck == boardState.WhoseTurn && simulation.IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo)) { throw new InvalidOperationException("Unable to move because you are still in check."); } var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; if (simulation.IsPlayerInCheckAfterMove()) { throw new InvalidOperationException("Illegal move. This move places you in check."); } _ = rules.Move(from, to, isPromotion); if (rules.IsOpponentInCheckAfterMove()) { boardState.InCheck = otherPlayer; if (rules.IsOpponentInCheckMate()) { boardState.IsCheckmate = true; } } else { boardState.InCheck = null; } boardState.WhoseTurn = otherPlayer; } public void Move(WhichPiece pieceInHand, string to) { var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); } if (boardState[to] != null) { throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); } var toVector = ShogiBoardState.FromBoardNotation(to); switch (pieceInHand) { case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6) || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)) { throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); } break; } case WhichPiece.Lance: case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8) || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)) { throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); } break; } } var tempBoard = new ShogiBoardState(boardState); var simulation = new StandardRules(tempBoard); var moveResult = simulation.Move(pieceInHand, to); if (!moveResult.Success) { throw new InvalidOperationException(moveResult.Reason); } var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; if (boardState.InCheck == boardState.WhoseTurn) { //if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn)) //{ // throw new InvalidOperationException("Illegal move. You're still in check!"); //} } var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition; //if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer)) //{ //} //rules.Move(from, to, isPromotion); //if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) //{ // board.InCheck = otherPlayer; // board.IsCheckmate = rules.EvaluateCheckmate(); //} //else //{ // board.InCheck = null; //} boardState.WhoseTurn = otherPlayer; } /// /// Prints a ASCII representation of the board for debugging board state. /// /// public string ToStringStateAsAscii() { var builder = new StringBuilder(); builder.Append(" "); builder.Append("Player 2(.)"); builder.AppendLine(); for (var rank = 8; rank >= 0; rank--) { // Horizontal line builder.Append(" - "); for (var file = 0; file < 8; file++) builder.Append("- - "); builder.Append("- -"); // Print Rank ruler. builder.AppendLine(); builder.Append($"{rank + 1} "); // Print pieces. builder.Append(" |"); for (var x = 0; x < 9; x++) { var piece = boardState[x, rank]; if (piece == null) { builder.Append(" "); } else { builder.AppendFormat("{0}", ToAscii(piece)); } builder.Append('|'); } builder.AppendLine(); } // Horizontal line builder.Append(" - "); for (var x = 0; x < 8; x++) builder.Append("- - "); builder.Append("- -"); builder.AppendLine(); builder.Append(" "); builder.Append("Player 1"); builder.AppendLine(); builder.AppendLine(); // Print File ruler. builder.Append(" "); builder.Append(" A B C D E F G H I "); return builder.ToString(); } /// /// /// /// /// /// A string with three characters. /// The first character indicates promotion status. /// The second character indicates piece. /// The third character indicates ownership. /// public static string ToAscii(Piece piece) { var builder = new StringBuilder(); if (piece.IsPromoted) builder.Append('^'); else builder.Append(' '); var name = piece.WhichPiece switch { WhichPiece.King => "K", WhichPiece.GoldGeneral => "G", WhichPiece.SilverGeneral => "S", WhichPiece.Bishop => "B", WhichPiece.Rook => "R", WhichPiece.Knight => "k", WhichPiece.Lance => "L", WhichPiece.Pawn => "P", _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), }; builder.Append(name); if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); else builder.Append(' '); return builder.ToString(); } } }