From 3257b420e9ef1402aea12693692b59715d0765b8 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 9 Nov 2022 08:56:54 -0600 Subject: [PATCH] yep --- .../Stored Procedures/CreateBoardState.sql | 16 - .../Stored Procedures/CreateSession.sql | 5 +- Shogi.Database/Session/Tables/Session.sql | 10 +- Shogi.Database/Shogi.Database.sqlproj | 1 - Shogi.Domain/Aggregates/Session.cs | 253 +--------- Shogi.Domain/BoardState.cs | 446 +++++++++--------- Shogi.Domain/StandardRules.cs | 17 +- Shogi.Domain/ValueObjects/Move.cs | 8 + Shogi.Domain/ValueObjects/Piece.cs | 6 +- Shogi.Domain/ValueObjects/ShogiBoard.cs | 235 +++++++++ .../Controllers/SessionController.cs | 15 +- .../Repositories/Dto/BoardStateDto.cs | 22 + Shogi.Sockets/Repositories/Dto/SessionDto.cs | 24 +- Shogi.Sockets/Repositories/QueryRepository.cs | 10 - .../Repositories/SessionRepository.cs | 41 +- ...tFixture - Copy.cs => GuestTestFixture.cs} | 0 Tests/UnitTests/ShogiShould.cs | 30 +- 17 files changed, 601 insertions(+), 538 deletions(-) delete mode 100644 Shogi.Database/Session/Stored Procedures/CreateBoardState.sql create mode 100644 Shogi.Domain/ValueObjects/Move.cs create mode 100644 Shogi.Domain/ValueObjects/ShogiBoard.cs create mode 100644 Shogi.Sockets/Repositories/Dto/BoardStateDto.cs rename Tests/AcceptanceTests/TestSetup/{MsalTestFixture - Copy.cs => GuestTestFixture.cs} (100%) diff --git a/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql b/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql deleted file mode 100644 index c906a33..0000000 --- a/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE PROCEDURE [dbo].[CreateBoardState] - @boardStateDocument NVARCHAR(max), - @sessionName NVARCHAR(100) -AS -BEGIN - -SET NOCOUNT ON - -INSERT INTO [session].[BoardState] (Document, SessionId) - SELECT - @boardStateDocument, - Id - FROM [session].[Session] - WHERE [Name] = @sessionName; - -END \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/CreateSession.sql b/Shogi.Database/Session/Stored Procedures/CreateSession.sql index b40d6aa..d349e4f 100644 --- a/Shogi.Database/Session/Stored Procedures/CreateSession.sql +++ b/Shogi.Database/Session/Stored Procedures/CreateSession.sql @@ -1,6 +1,7 @@ CREATE PROCEDURE [session].[CreateSession] - @InitialBoardStateDocument [session].[JsonDocument], - @Player1Name [user].[UserName] + @Name [session].[SessionName], + @Player1Name [user].[UserName], + @InitialBoardStateDocument [session].[JsonDocument] AS BEGIN SET NOCOUNT ON diff --git a/Shogi.Database/Session/Tables/Session.sql b/Shogi.Database/Session/Tables/Session.sql index 17ba415..4b1912f 100644 --- a/Shogi.Database/Session/Tables/Session.sql +++ b/Shogi.Database/Session/Tables/Session.sql @@ -1,17 +1,17 @@ CREATE TABLE [session].[Session] ( Id BIGINT NOT NULL PRIMARY KEY IDENTITY, - Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(), + [Name] [session].[SessionName] UNIQUE, Player1Id BIGINT NOT NULL, Player2Id BIGINT NULL, BoardState [session].[JsonDocument] NOT NULL, - [Name] AS JSON_VALUE(BoardState, '$.Name') UNIQUE, + Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(), CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1), - CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id) - ON DELETE CASCADE + CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id) + ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id) + CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id) ON DELETE NO ACTION ON UPDATE NO ACTION ) diff --git a/Shogi.Database/Shogi.Database.sqlproj b/Shogi.Database/Shogi.Database.sqlproj index eee8fd4..01ae76c 100644 --- a/Shogi.Database/Shogi.Database.sqlproj +++ b/Shogi.Database/Shogi.Database.sqlproj @@ -72,7 +72,6 @@ - diff --git a/Shogi.Domain/Aggregates/Session.cs b/Shogi.Domain/Aggregates/Session.cs index f5bf0b2..1c6d529 100644 --- a/Shogi.Domain/Aggregates/Session.cs +++ b/Shogi.Domain/Aggregates/Session.cs @@ -1,240 +1,27 @@ using Shogi.Domain.ValueObjects; -using System.Text; -namespace Shogi.Domain; +namespace Shogi.Domain.Aggregates; -/// -/// 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 ShogiBoard +public class Session { - private readonly StandardRules rules; + public Session( + string name, + string player1Name, + ShogiBoard board) + { + Name = name; + Player1 = player1Name; + Board = board; + } - public ShogiBoard(string name, BoardState initialState, string player1, string? player2 = null) - { - Name = name; - Player1 = player1; - Player2 = player2; - BoardState = initialState; - rules = new StandardRules(BoardState); - } + public string Name { get; } + public ShogiBoard Board { get; } + public string Player1 { get; } + public string? Player2 { get; private set; } - public BoardState BoardState { get; } - public string Name { get; } - - /// - /// 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 BoardState(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 = Notation.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 BoardState(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. - /// - private 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(); - } + public void AddPlayer2(string player2Name) + { + if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player."); + Player2 = player2Name; + } } diff --git a/Shogi.Domain/BoardState.cs b/Shogi.Domain/BoardState.cs index fb50200..3850bec 100644 --- a/Shogi.Domain/BoardState.cs +++ b/Shogi.Domain/BoardState.cs @@ -1,251 +1,253 @@ using Shogi.Domain.ValueObjects; +using System.Collections.ObjectModel; using BoardTile = System.Collections.Generic.KeyValuePair; -namespace Shogi.Domain +namespace Shogi.Domain; + +public class BoardState { - public class BoardState - { - /// - /// Board state before any moves have been made, using standard setup and rules. - /// - public static readonly BoardState StandardStarting = new(); + /// + /// Board state before any moves have been made, using standard setup and rules. + /// + public static readonly BoardState StandardStarting = new( + state: BuildStandardStartingBoardState(), + player1Hand: new(), + player2Hand: new(), + whoseTurn: WhichPlayer.Player1, + playerInCheck: null, + previousMove: new Move()); - public delegate void ForEachDelegate(Piece element, Vector2 position); - /// - /// Key is position notation, such as "E4". - /// - private readonly Dictionary board; + public delegate void ForEachDelegate(Piece element, Vector2 position); + /// + /// Key is position notation, such as "E4". + /// + private readonly Dictionary board; + public BoardState( + Dictionary state, + List player1Hand, + List player2Hand, + WhichPlayer whoseTurn, + WhichPlayer? playerInCheck, + Move previousMove) + { + board = state; + Player1Hand = player1Hand; + Player2Hand = player2Hand; + PreviousMove = previousMove; + WhoseTurn = whoseTurn; + InCheck = playerInCheck; + } - public BoardState(Dictionary state) - { - board = state; - Player1Hand = new List(); - Player2Hand = new List(); - PreviousMoveTo = Vector2.Zero; - } + /// + /// Copy constructor. + /// + public BoardState(BoardState other) + { + board = new(81); + foreach (var kvp in other.board) + { + var piece = kvp.Value; + board[kvp.Key] = piece == null ? null : Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + } + WhoseTurn = other.WhoseTurn; + InCheck = other.InCheck; + IsCheckmate = other.IsCheckmate; + PreviousMove = other.PreviousMove; + Player1Hand = new(other.Player1Hand); + Player2Hand = new(other.Player2Hand); + } - public BoardState() - { - board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); - InitializeBoardState(); - Player1Hand = new List(); - Player2Hand = new List(); - PreviousMoveTo = Vector2.Zero; - } + public ReadOnlyDictionary State => new(board); + public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; + }).Key); + public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; + }).Key); + public List Player1Hand { get; } + public List Player2Hand { get; } + public Move PreviousMove { get; set; } + public WhichPlayer WhoseTurn { get; set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } - public Dictionary State => board; - public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; - }).Key); - public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; - }).Key); - public List Player1Hand { get; } - public List Player2Hand { get; } - public Vector2 PreviousMoveFrom { get; private set; } - public Vector2 PreviousMoveTo { get; private set; } - public WhichPlayer WhoseTurn { get; set; } - public WhichPlayer? InCheck { get; set; } - public bool IsCheckmate { get; set; } + public Piece? this[string notation] + { + // TODO: Validate "notation" here and throw an exception if invalid. + get => board[notation]; + set => board[notation] = value; + } - /// - /// Copy constructor. - /// - public BoardState(BoardState other) : this() - { - foreach (var kvp in other.board) - { - // Replace copy constructor with static factory method in Piece.cs - board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value); - } - WhoseTurn = other.WhoseTurn; - InCheck = other.InCheck; - IsCheckmate = other.IsCheckmate; - PreviousMoveTo = other.PreviousMoveTo; - Player1Hand.AddRange(other.Player1Hand); - Player2Hand.AddRange(other.Player2Hand); - } + public Piece? this[Vector2 vector] + { + get => this[Notation.ToBoardNotation(vector)]; + set => this[Notation.ToBoardNotation(vector)] = value; + } - public Piece? this[string notation] - { - // TODO: Validate "notation" here and throw an exception if invalid. - get => board[notation]; - set => board[notation] = value; - } + public Piece? this[int x, int y] + { + get => this[Notation.ToBoardNotation(x, y)]; + set => this[Notation.ToBoardNotation(x, y)] = value; + } - public Piece? this[Vector2 vector] - { - get => this[Notation.ToBoardNotation(vector)]; - set => this[Notation.ToBoardNotation(vector)] = value; - } + /// + /// Returns true if the given path can be traversed without colliding into a piece. + /// + public bool IsPathBlocked(IEnumerable path) + { + return !path.Any() + || path.SkipLast(1).Any(position => this[position] != null) + || this[path.Last()]?.Owner == WhoseTurn; + } - public Piece? this[int x, int y] - { - get => this[Notation.ToBoardNotation(x, y)]; - set => this[Notation.ToBoardNotation(x, y)] = value; - } + internal bool IsWithinPromotionZone(Vector2 position) + { + // TODO: Move this promotion zone logic into the StandardRules class. + return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) + || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); + } - internal void RememberAsMostRecentMove(Vector2 from, Vector2 to) - { - PreviousMoveFrom = from; - PreviousMoveTo = to; - } + internal static bool IsWithinBoardBoundary(Vector2 position) + { + return position.X <= 8 && position.X >= 0 + && position.Y <= 8 && position.Y >= 0; + } - /// - /// Returns true if the given path can be traversed without colliding into a piece. - /// - public bool IsPathBlocked(IEnumerable path) - { - return !path.Any() - || path.SkipLast(1).Any(position => this[position] != null) - || this[path.Last()]?.Owner == WhoseTurn; - } + internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + .Where(kvp => kvp.Value?.Owner == whichPlayer) + .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) + .ToList(); - internal bool IsWithinPromotionZone(Vector2 position) - { - return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) - || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); - } + internal void Capture(Vector2 to) + { + var piece = this[to]; + if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); - internal static bool IsWithinBoardBoundary(Vector2 position) - { - return position.X <= 8 && position.X >= 0 - && position.Y <= 8 && position.Y >= 0; - } + piece.Capture(WhoseTurn); + ActivePlayerHand.Add(piece); + } - internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board - .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) - .ToList(); + /// + /// Does not include the start position. + /// + internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) + { + var next = start; + while (IsWithinBoardBoundary(next + direction)) + { + next += direction; + yield return next; + } + } - internal void Capture(Vector2 to) - { - var piece = this[to]; - if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); + internal Piece? QueryFirstPieceInPath(IEnumerable path) + { + foreach (var step in path) + { + if (this[step] != null) return this[step]; + } + return null; + } - piece.Capture(WhoseTurn); - ActivePlayerHand.Add(piece); - } + private static Dictionary BuildStandardStartingBoardState() + { + return new Dictionary(81) + { + ["A1"] = new Lance(WhichPlayer.Player1), + ["B1"] = new Knight(WhichPlayer.Player1), + ["C1"] = new SilverGeneral(WhichPlayer.Player1), + ["D1"] = new GoldGeneral(WhichPlayer.Player1), + ["E1"] = new King(WhichPlayer.Player1), + ["F1"] = new GoldGeneral(WhichPlayer.Player1), + ["G1"] = new SilverGeneral(WhichPlayer.Player1), + ["H1"] = new Knight(WhichPlayer.Player1), + ["I1"] = new Lance(WhichPlayer.Player1), - /// - /// Does not include the start position. - /// - internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) - { - var next = start; - while (IsWithinBoardBoundary(next + direction)) - { - next += direction; - yield return next; - } - } + ["A2"] = null, + ["B2"] = new Bishop(WhichPlayer.Player1), + ["C2"] = null, + ["D2"] = null, + ["E2"] = null, + ["F2"] = null, + ["G2"] = null, + ["H2"] = new Rook(WhichPlayer.Player1), + ["I2"] = null, - internal Piece? QueryFirstPieceInPath(IEnumerable path) - { - foreach (var step in path) - { - if (this[step] != null) return this[step]; - } - return null; - } + ["A3"] = new Pawn(WhichPlayer.Player1), + ["B3"] = new Pawn(WhichPlayer.Player1), + ["C3"] = new Pawn(WhichPlayer.Player1), + ["D3"] = new Pawn(WhichPlayer.Player1), + ["E3"] = new Pawn(WhichPlayer.Player1), + ["F3"] = new Pawn(WhichPlayer.Player1), + ["G3"] = new Pawn(WhichPlayer.Player1), + ["H3"] = new Pawn(WhichPlayer.Player1), + ["I3"] = new Pawn(WhichPlayer.Player1), - private void InitializeBoardState() - { - this["A1"] = new Lance(WhichPlayer.Player1); - this["B1"] = new Knight(WhichPlayer.Player1); - this["C1"] = new SilverGeneral(WhichPlayer.Player1); - this["D1"] = new GoldGeneral(WhichPlayer.Player1); - this["E1"] = new King(WhichPlayer.Player1); - this["F1"] = new GoldGeneral(WhichPlayer.Player1); - this["G1"] = new SilverGeneral(WhichPlayer.Player1); - this["H1"] = new Knight(WhichPlayer.Player1); - this["I1"] = new Lance(WhichPlayer.Player1); + ["A4"] = null, + ["B4"] = null, + ["C4"] = null, + ["D4"] = null, + ["E4"] = null, + ["F4"] = null, + ["G4"] = null, + ["H4"] = null, + ["I4"] = null, - this["A2"] = null; - this["B2"] = new Bishop(WhichPlayer.Player1); - this["C2"] = null; - this["D2"] = null; - this["E2"] = null; - this["F2"] = null; - this["G2"] = null; - this["H2"] = new Rook(WhichPlayer.Player1); - this["I2"] = null; + ["A5"] = null, + ["B5"] = null, + ["C5"] = null, + ["D5"] = null, + ["E5"] = null, + ["F5"] = null, + ["G5"] = null, + ["H5"] = null, + ["I5"] = null, - this["A3"] = new Pawn(WhichPlayer.Player1); - this["B3"] = new Pawn(WhichPlayer.Player1); - this["C3"] = new Pawn(WhichPlayer.Player1); - this["D3"] = new Pawn(WhichPlayer.Player1); - this["E3"] = new Pawn(WhichPlayer.Player1); - this["F3"] = new Pawn(WhichPlayer.Player1); - this["G3"] = new Pawn(WhichPlayer.Player1); - this["H3"] = new Pawn(WhichPlayer.Player1); - this["I3"] = new Pawn(WhichPlayer.Player1); + ["A6"] = null, + ["B6"] = null, + ["C6"] = null, + ["D6"] = null, + ["E6"] = null, + ["F6"] = null, + ["G6"] = null, + ["H6"] = null, + ["I6"] = null, - this["A4"] = null; - this["B4"] = null; - this["C4"] = null; - this["D4"] = null; - this["E4"] = null; - this["F4"] = null; - this["G4"] = null; - this["H4"] = null; - this["I4"] = null; + ["A7"] = new Pawn(WhichPlayer.Player2), + ["B7"] = new Pawn(WhichPlayer.Player2), + ["C7"] = new Pawn(WhichPlayer.Player2), + ["D7"] = new Pawn(WhichPlayer.Player2), + ["E7"] = new Pawn(WhichPlayer.Player2), + ["F7"] = new Pawn(WhichPlayer.Player2), + ["G7"] = new Pawn(WhichPlayer.Player2), + ["H7"] = new Pawn(WhichPlayer.Player2), + ["I7"] = new Pawn(WhichPlayer.Player2), - this["A5"] = null; - this["B5"] = null; - this["C5"] = null; - this["D5"] = null; - this["E5"] = null; - this["F5"] = null; - this["G5"] = null; - this["H5"] = null; - this["I5"] = null; + ["A8"] = null, + ["B8"] = new Rook(WhichPlayer.Player2), + ["C8"] = null, + ["D8"] = null, + ["E8"] = null, + ["F8"] = null, + ["G8"] = null, + ["H8"] = new Bishop(WhichPlayer.Player2), + ["I8"] = null, - this["A6"] = null; - this["B6"] = null; - this["C6"] = null; - this["D6"] = null; - this["E6"] = null; - this["F6"] = null; - this["G6"] = null; - this["H6"] = null; - this["I6"] = null; - - this["A7"] = new Pawn(WhichPlayer.Player2); - this["B7"] = new Pawn(WhichPlayer.Player2); - this["C7"] = new Pawn(WhichPlayer.Player2); - this["D7"] = new Pawn(WhichPlayer.Player2); - this["E7"] = new Pawn(WhichPlayer.Player2); - this["F7"] = new Pawn(WhichPlayer.Player2); - this["G7"] = new Pawn(WhichPlayer.Player2); - this["H7"] = new Pawn(WhichPlayer.Player2); - this["I7"] = new Pawn(WhichPlayer.Player2); - - this["A8"] = null; - this["B8"] = new Rook(WhichPlayer.Player2); - this["C8"] = null; - this["D8"] = null; - this["E8"] = null; - this["F8"] = null; - this["G8"] = null; - this["H8"] = new Bishop(WhichPlayer.Player2); - this["I8"] = null; - - this["A9"] = new Lance(WhichPlayer.Player2); - this["B9"] = new Knight(WhichPlayer.Player2); - this["C9"] = new SilverGeneral(WhichPlayer.Player2); - this["D9"] = new GoldGeneral(WhichPlayer.Player2); - this["E9"] = new King(WhichPlayer.Player2); - this["F9"] = new GoldGeneral(WhichPlayer.Player2); - this["G9"] = new SilverGeneral(WhichPlayer.Player2); - this["H9"] = new Knight(WhichPlayer.Player2); - this["I9"] = new Lance(WhichPlayer.Player2); - } - } + ["A9"] = new Lance(WhichPlayer.Player2), + ["B9"] = new Knight(WhichPlayer.Player2), + ["C9"] = new SilverGeneral(WhichPlayer.Player2), + ["D9"] = new GoldGeneral(WhichPlayer.Player2), + ["E9"] = new King(WhichPlayer.Player2), + ["F9"] = new GoldGeneral(WhichPlayer.Player2), + ["G9"] = new SilverGeneral(WhichPlayer.Player2), + ["H9"] = new Knight(WhichPlayer.Player2), + ["I9"] = new Lance(WhichPlayer.Player2) + }; + } } diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 2149dd0..15e5597 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -1,10 +1,9 @@ -using Shogi.Domain.Pathing; -using Shogi.Domain.ValueObjects; +using Shogi.Domain.ValueObjects; using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { - internal class StandardRules + internal class StandardRules { private readonly BoardState boardState; @@ -55,7 +54,7 @@ namespace Shogi.Domain boardState[to] = fromPiece; boardState[from] = null; - boardState.RememberAsMostRecentMove(from, to); + boardState.PreviousMove = new Move(from, to); var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; boardState.WhoseTurn = otherPlayer; @@ -121,16 +120,16 @@ namespace Shogi.Domain /// internal bool IsPlayerInCheckAfterMove() { - var previousMovedPiece = boardState[boardState.PreviousMoveTo]; - if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMoveTo}."); + var previousMovedPiece = boardState[boardState.PreviousMove.To]; + if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}."); var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; var isCheck = false; // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom); + var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From); var slope = Math.Abs(direction.Y / direction.X); - var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction)); + var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From, Vector2.Normalize(direction)); var threat = boardState.QueryFirstPieceInPath(path); if (threat == null || threat.Owner == previousMovedPiece.Owner) return false; // If absolute slope is 45°, look for a bishop along the line. @@ -165,7 +164,7 @@ namespace Shogi.Domain return isCheck; } - internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo); + internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); internal bool IsOpposingKingThreatenedByPosition(Vector2 position) { diff --git a/Shogi.Domain/ValueObjects/Move.cs b/Shogi.Domain/ValueObjects/Move.cs new file mode 100644 index 0000000..384c344 --- /dev/null +++ b/Shogi.Domain/ValueObjects/Move.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain.ValueObjects; + +/// +/// Represents a single piece being moved by a player from to . +/// +public record struct Move(Vector2 From, Vector2 To) +{ +} diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Piece.cs index 0ba7769..d7686ab 100644 --- a/Shogi.Domain/ValueObjects/Piece.cs +++ b/Shogi.Domain/ValueObjects/Piece.cs @@ -6,10 +6,6 @@ namespace Shogi.Domain.ValueObjects [DebuggerDisplay("{WhichPiece} {Owner}")] public abstract record class Piece { - /// - /// Creates a clone of an existing piece. - /// - public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) { return piece switch @@ -54,7 +50,7 @@ namespace Shogi.Domain.ValueObjects } /// - /// Respecting the move-set of the Piece, collect all positions from start to end. + /// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end. /// Useful if you need to iterate a move-set. /// /// diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs new file mode 100644 index 0000000..cd4923e --- /dev/null +++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs @@ -0,0 +1,235 @@ +using System.Text; + +namespace Shogi.Domain.ValueObjects; + +/// +/// 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 ShogiBoard +{ + private readonly StandardRules rules; + + public ShogiBoard(BoardState initialState) + { + BoardState = initialState; + rules = new StandardRules(BoardState); + } + + public BoardState BoardState { get; } + + /// + /// 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 BoardState(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.PreviousMove.To)) + { + 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 = Notation.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 BoardState(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. + /// + private 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(); + } +} diff --git a/Shogi.Sockets/Controllers/SessionController.cs b/Shogi.Sockets/Controllers/SessionController.cs index c9993d3..f16db55 100644 --- a/Shogi.Sockets/Controllers/SessionController.cs +++ b/Shogi.Sockets/Controllers/SessionController.cs @@ -19,17 +19,20 @@ public class SessionController : ControllerBase private readonly IModelMapper mapper; private readonly ISessionRepository sessionRepository; private readonly IQueryRespository queryRespository; + private readonly ILogger logger; public SessionController( ISocketConnectionManager communicationManager, IModelMapper mapper, ISessionRepository sessionRepository, - IQueryRespository queryRespository) + IQueryRespository queryRespository, + ILogger logger) { this.communicationManager = communicationManager; this.mapper = mapper; this.sessionRepository = sessionRepository; this.queryRespository = queryRespository; + this.logger = logger; } [HttpPost] @@ -37,13 +40,17 @@ public class SessionController : ControllerBase { var userId = User.GetShogiUserId(); if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized(); - var session = new Domain.ShogiBoard(request.Name, Domain.BoardState.StandardStarting, userId); + var session = new Domain.Aggregates.Session( + request.Name, + userId, + new Domain.ValueObjects.ShogiBoard(Domain.BoardState.StandardStarting)); try { - await sessionRepository.CreateSession(session); + await sessionRepository.CreateShogiBoard(board, request.Name, userId); } - catch (SqlException) + catch (SqlException e) { + logger.LogError(exception: e, message: "Uh oh"); return this.Conflict(); } diff --git a/Shogi.Sockets/Repositories/Dto/BoardStateDto.cs b/Shogi.Sockets/Repositories/Dto/BoardStateDto.cs new file mode 100644 index 0000000..312e791 --- /dev/null +++ b/Shogi.Sockets/Repositories/Dto/BoardStateDto.cs @@ -0,0 +1,22 @@ +using Shogi.Domain; +using Shogi.Domain.ValueObjects; +using System.Collections.ObjectModel; + +namespace Shogi.Api.Repositories.Dto; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +public class BoardStateDto +{ + public ReadOnlyDictionary State { get; set; } + + public List Player1Hand { get; set; } + + public List Player2Hand { get; set; } + + public Move PreviousMove { get; } + + public WhichPlayer WhoseTurn { get; set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } +} +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/Shogi.Sockets/Repositories/Dto/SessionDto.cs b/Shogi.Sockets/Repositories/Dto/SessionDto.cs index c0aef66..bdf8e5d 100644 --- a/Shogi.Sockets/Repositories/Dto/SessionDto.cs +++ b/Shogi.Sockets/Repositories/Dto/SessionDto.cs @@ -1,14 +1,14 @@ -namespace Shogi.Api.Repositories.Dto +namespace Shogi.Api.Repositories.Dto; + +/// +/// Useful with Dapper to read from database. +/// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +public class SessionDto { - /// - /// Useful with Dapper to read from database. - /// - public class SessionDto - { - public string Name { get; set; } - public string Player1 { get; set; } - public string Player2 { get; set; } - public bool GameOver { get; set; } - public string BoardState { get; set; } - } + public string Name { get; set; } + public string Player1 { get; set; } + public string Player2 { get; set; } + public BoardStateDto BoardState { get; set; } } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. \ No newline at end of file diff --git a/Shogi.Sockets/Repositories/QueryRepository.cs b/Shogi.Sockets/Repositories/QueryRepository.cs index 0be7b4d..ac9bfbe 100644 --- a/Shogi.Sockets/Repositories/QueryRepository.cs +++ b/Shogi.Sockets/Repositories/QueryRepository.cs @@ -21,19 +21,9 @@ public class QueryRepository : IQueryRespository "session.ReadAllSessionsMetadata", commandType: System.Data.CommandType.StoredProcedure); } - - public async Task ReadSession(string name) - { - using var connection = new SqlConnection(connectionString); - var results = await connection.QueryAsync( - "session.ReadSession", - commandType: System.Data.CommandType.StoredProcedure); - return results.SingleOrDefault(); - } } public interface IQueryRespository { Task> ReadAllSessionsMetadata(); - Task ReadSession(string name); } \ No newline at end of file diff --git a/Shogi.Sockets/Repositories/SessionRepository.cs b/Shogi.Sockets/Repositories/SessionRepository.cs index e1e2a83..e78b327 100644 --- a/Shogi.Sockets/Repositories/SessionRepository.cs +++ b/Shogi.Sockets/Repositories/SessionRepository.cs @@ -1,6 +1,8 @@ using Dapper; using Shogi.Api.Repositories.Dto; using Shogi.Domain; +using Shogi.Domain.Aggregates; +using Shogi.Domain.ValueObjects; using System.Data; using System.Data.SqlClient; using System.Text.Json; @@ -16,22 +18,53 @@ public class SessionRepository : ISessionRepository connectionString = configuration.GetConnectionString("ShogiDatabase"); } - public async Task CreateSession(ShogiBoard session, string player1) + public async Task CreateSession(Session session) { - var initialBoardState = JsonSerializer.Serialize(session.BoardState); + var boardStateDto = new BoardStateDto + { + InCheck = session.BoardState.InCheck, + IsCheckmate = session.BoardState.IsCheckmate, + Player1Hand = session.BoardState.Player1Hand, + Player2Hand = session.BoardState.Player2Hand, + State = session.BoardState.State, + WhoseTurn = session.BoardState.WhoseTurn, + }; + using var connection = new SqlConnection(connectionString); await connection.ExecuteAsync( "session.CreateSession", new { - InitialBoardStateDocument = initialBoardState, + Name = sessionName, + InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto), Player1Name = player1, }, commandType: CommandType.StoredProcedure); } + + public async Task ReadShogiBoard(string name) + { + using var connection = new SqlConnection(connectionString); + var results = await connection.QueryAsync( + "session.ReadSession", + commandType: CommandType.StoredProcedure); + var dto = results.SingleOrDefault(); + if (dto == null) return null; + + var boardState = new BoardState( + state: new(dto.BoardState.State), + player1Hand: dto.BoardState.Player1Hand, + player2Hand: dto.BoardState.Player2Hand, + whoseTurn: dto.BoardState.WhoseTurn, + playerInCheck: dto.BoardState.InCheck, + previousMove: dto.BoardState.PreviousMove); + var session = new ShogiBoard(boardState); + return session; + } } public interface ISessionRepository { - Task CreateSession(ShogiBoard session, string player1); + Task CreateSession(Session session); + Task ReadShogiBoard(string name); } \ No newline at end of file diff --git a/Tests/AcceptanceTests/TestSetup/MsalTestFixture - Copy.cs b/Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs similarity index 100% rename from Tests/AcceptanceTests/TestSetup/MsalTestFixture - Copy.cs rename to Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 946dbed..cc6ea13 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -14,7 +14,7 @@ namespace Shogi.Domain.UnitTests public void MoveAPieceToAnEmptyPosition() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; board["A4"].Should().BeNull(); @@ -33,7 +33,7 @@ namespace Shogi.Domain.UnitTests public void AllowValidMoves_AfterCheck() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Pawn shogi.Move("C3", "C4", false); @@ -58,7 +58,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveFromEmptyPosition() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; board["D5"].Should().BeNull(); @@ -77,7 +77,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveToCurrentPosition() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var expectedPiece = board["A3"]; @@ -98,7 +98,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveSet() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var expectedPiece = board["A1"]; expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); @@ -121,7 +121,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_Ownership() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var expectedPiece = board["A7"]; expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); @@ -143,7 +143,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_MoveThroughAllies() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var lance = board["A1"]; var pawn = board["A3"]; @@ -166,7 +166,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_CaptureAlly() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var knight = board["B1"]; var pawn = board["C3"]; @@ -190,7 +190,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidMoves_Check() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Pawn shogi.Move("C3", "C4", false); @@ -219,7 +219,7 @@ namespace Shogi.Domain.UnitTests public void PreventInvalidDrops_MoveSet() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Pawn shogi.Move("C3", "C4", false); @@ -358,7 +358,7 @@ namespace Shogi.Domain.UnitTests public void Check() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Pawn shogi.Move("C3", "C4", false); @@ -376,7 +376,7 @@ namespace Shogi.Domain.UnitTests public void Promote() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Pawn shogi.Move("C3", "C4", false); @@ -401,7 +401,7 @@ namespace Shogi.Domain.UnitTests public void Capture() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; var p1Bishop = board["B2"]; p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); @@ -425,7 +425,7 @@ namespace Shogi.Domain.UnitTests public void CheckMate() { // Arrange - var shogi = MockSession(); + var shogi = MockShogiBoard(); var board = shogi.BoardState; // P1 Rook shogi.Move("H2", "E2", false); @@ -457,6 +457,6 @@ namespace Shogi.Domain.UnitTests board.InCheck.Should().Be(WhichPlayer.Player2); } - private static ShogiBoard MockSession() => new ShogiBoard("Test Session", BoardState.StandardStarting, "Test P1", "Test P2"); + private static ShogiBoard MockShogiBoard() => new ShogiBoard("Test Session", BoardState.StandardStarting); } }