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);
}
}