From faff4049d569a05e41fcb0f224303f7e7155877a Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 20 Feb 2023 18:54:53 -0600 Subject: [PATCH] Organize domain project. --- Shogi.Api/Extensions/ContractsExtensions.cs | 45 ++- Shogi.Api/Managers/ModelMapper.cs | 6 +- Shogi.Api/Repositories/Dto/MoveDto.cs | 2 +- Shogi.Domain/BoardState.cs | 252 ---------------- Shogi.Domain/DomainExtensions.cs | 19 -- Shogi.Domain/MoveResult.cs | 14 - Shogi.Domain/Notation.cs | 33 -- Shogi.Domain/Pathing/Direction.cs | 22 -- Shogi.Domain/Pathing/Distance.cs | 8 - Shogi.Domain/Pathing/Path.cs | 41 --- Shogi.Domain/ValueObjects/Bishop.cs | 2 +- Shogi.Domain/ValueObjects/BoardState.cs | 252 ++++++++++++++++ Shogi.Domain/ValueObjects/GoldGeneral.cs | 2 +- Shogi.Domain/ValueObjects/King.cs | 2 +- Shogi.Domain/ValueObjects/Knight.cs | 2 +- Shogi.Domain/ValueObjects/Lance.cs | 2 +- Shogi.Domain/ValueObjects/MoveResult.cs | 14 + Shogi.Domain/ValueObjects/Pawn.cs | 2 +- Shogi.Domain/ValueObjects/Piece.cs | 2 +- Shogi.Domain/ValueObjects/Rook.cs | 2 +- Shogi.Domain/ValueObjects/ShogiBoard.cs | 1 + Shogi.Domain/ValueObjects/SilverGeneral.cs | 2 +- .../{ => ValueObjects}/StandardRules.cs | 14 +- Shogi.Domain/ValueObjects/WhichPiece.cs | 14 + Shogi.Domain/ValueObjects/WhichPlayer.cs | 8 + Shogi.Domain/WhichPiece.cs | 14 - Shogi.Domain/WhichPlayer.cs | 8 - .../DomainExtensions.cs | 19 ++ .../YetToBeAssimilatedIntoDDD/Notation.cs | 33 ++ .../Pathing/Direction.cs | 22 ++ .../Pathing/Distance.cs | 8 + .../YetToBeAssimilatedIntoDDD/Pathing/Path.cs | 41 +++ .../YetToBeAssimilatedIntoDDD/ReadMe.md | 4 + Tests/UnitTests/NotationShould.cs | 3 +- Tests/UnitTests/RookShould.cs | 283 +++++++++--------- Tests/UnitTests/ShogiBoardStateShould.cs | 4 +- 36 files changed, 603 insertions(+), 599 deletions(-) delete mode 100644 Shogi.Domain/BoardState.cs delete mode 100644 Shogi.Domain/DomainExtensions.cs delete mode 100644 Shogi.Domain/MoveResult.cs delete mode 100644 Shogi.Domain/Notation.cs delete mode 100644 Shogi.Domain/Pathing/Direction.cs delete mode 100644 Shogi.Domain/Pathing/Distance.cs delete mode 100644 Shogi.Domain/Pathing/Path.cs create mode 100644 Shogi.Domain/ValueObjects/BoardState.cs create mode 100644 Shogi.Domain/ValueObjects/MoveResult.cs rename Shogi.Domain/{ => ValueObjects}/StandardRules.cs (96%) create mode 100644 Shogi.Domain/ValueObjects/WhichPiece.cs create mode 100644 Shogi.Domain/ValueObjects/WhichPlayer.cs delete mode 100644 Shogi.Domain/WhichPiece.cs delete mode 100644 Shogi.Domain/WhichPlayer.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs create mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md diff --git a/Shogi.Api/Extensions/ContractsExtensions.cs b/Shogi.Api/Extensions/ContractsExtensions.cs index 9750799..b645b77 100644 --- a/Shogi.Api/Extensions/ContractsExtensions.cs +++ b/Shogi.Api/Extensions/ContractsExtensions.cs @@ -1,33 +1,32 @@ using Shogi.Contracts.Types; using System.Collections.ObjectModel; -using System.Linq; namespace Shogi.Api.Extensions; public static class ContractsExtensions { - public static WhichPlayer ToContract(this Domain.WhichPlayer player) + public static WhichPlayer ToContract(this Domain.ValueObjects.WhichPlayer player) { return player switch { - Domain.WhichPlayer.Player1 => WhichPlayer.Player1, - Domain.WhichPlayer.Player2 => WhichPlayer.Player2, + Domain.ValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1, + Domain.ValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2, _ => throw new NotImplementedException(), }; } - public static WhichPiece ToContract(this Domain.WhichPiece piece) + public static WhichPiece ToContract(this Domain.ValueObjects.WhichPiece piece) { return piece switch { - Domain.WhichPiece.King => WhichPiece.King, - Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, - Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, - Domain.WhichPiece.Bishop => WhichPiece.Bishop, - Domain.WhichPiece.Rook => WhichPiece.Rook, - Domain.WhichPiece.Knight => WhichPiece.Knight, - Domain.WhichPiece.Lance => WhichPiece.Lance, - Domain.WhichPiece.Pawn => WhichPiece.Pawn, + Domain.ValueObjects.WhichPiece.King => WhichPiece.King, + Domain.ValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + Domain.ValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + Domain.ValueObjects.WhichPiece.Bishop => WhichPiece.Bishop, + Domain.ValueObjects.WhichPiece.Rook => WhichPiece.Rook, + Domain.ValueObjects.WhichPiece.Knight => WhichPiece.Knight, + Domain.ValueObjects.WhichPiece.Lance => WhichPiece.Lance, + Domain.ValueObjects.WhichPiece.Pawn => WhichPiece.Pawn, _ => throw new NotImplementedException(), }; } @@ -50,19 +49,19 @@ public static class ContractsExtensions public static Dictionary ToContract(this ReadOnlyDictionary boardState) => boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract()); - public static Domain.WhichPiece? ToDomain(this WhichPiece? piece) => piece.HasValue ? piece.Value.ToDomain() : null; - public static Domain.WhichPiece ToDomain(this WhichPiece piece) + public static Domain.ValueObjects.WhichPiece? ToDomain(this WhichPiece? piece) => piece.HasValue ? piece.Value.ToDomain() : null; + public static Domain.ValueObjects.WhichPiece ToDomain(this WhichPiece piece) { return piece switch { - WhichPiece.King => Domain.WhichPiece.King, - WhichPiece.GoldGeneral => Domain.WhichPiece.GoldGeneral, - WhichPiece.SilverGeneral => Domain.WhichPiece.SilverGeneral, - WhichPiece.Bishop => Domain.WhichPiece.Bishop, - WhichPiece.Rook => Domain.WhichPiece.Rook, - WhichPiece.Knight => Domain.WhichPiece.Knight, - WhichPiece.Lance => Domain.WhichPiece.Lance, - WhichPiece.Pawn => Domain.WhichPiece.Pawn, + WhichPiece.King => Domain.ValueObjects.WhichPiece.King, + WhichPiece.GoldGeneral => Domain.ValueObjects.WhichPiece.GoldGeneral, + WhichPiece.SilverGeneral => Domain.ValueObjects.WhichPiece.SilverGeneral, + WhichPiece.Bishop => Domain.ValueObjects.WhichPiece.Bishop, + WhichPiece.Rook => Domain.ValueObjects.WhichPiece.Rook, + WhichPiece.Knight => Domain.ValueObjects.WhichPiece.Knight, + WhichPiece.Lance => Domain.ValueObjects.WhichPiece.Lance, + WhichPiece.Pawn => Domain.ValueObjects.WhichPiece.Pawn, _ => throw new NotImplementedException(), }; } diff --git a/Shogi.Api/Managers/ModelMapper.cs b/Shogi.Api/Managers/ModelMapper.cs index 61ad350..2a31cca 100644 --- a/Shogi.Api/Managers/ModelMapper.cs +++ b/Shogi.Api/Managers/ModelMapper.cs @@ -1,11 +1,11 @@ using Shogi.Contracts.Types; -using DomainWhichPiece = Shogi.Domain.WhichPiece; -using DomainWhichPlayer = Shogi.Domain.WhichPlayer; +using DomainWhichPiece = Shogi.Domain.ValueObjects.WhichPiece; +using DomainWhichPlayer = Shogi.Domain.ValueObjects.WhichPlayer; using Piece = Shogi.Contracts.Types.Piece; namespace Shogi.Api.Managers { - public class ModelMapper : IModelMapper + public class ModelMapper : IModelMapper { public WhichPlayer Map(DomainWhichPlayer whichPlayer) { diff --git a/Shogi.Api/Repositories/Dto/MoveDto.cs b/Shogi.Api/Repositories/Dto/MoveDto.cs index 69b19fa..2c6b086 100644 --- a/Shogi.Api/Repositories/Dto/MoveDto.cs +++ b/Shogi.Api/Repositories/Dto/MoveDto.cs @@ -1,4 +1,4 @@ -using Shogi.Domain; +using Shogi.Domain.ValueObjects; namespace Shogi.Api.Repositories.Dto; diff --git a/Shogi.Domain/BoardState.cs b/Shogi.Domain/BoardState.cs deleted file mode 100644 index 2c54c74..0000000 --- a/Shogi.Domain/BoardState.cs +++ /dev/null @@ -1,252 +0,0 @@ -using Shogi.Domain.ValueObjects; -using System.Collections.ObjectModel; -using BoardTile = System.Collections.Generic.KeyValuePair; - -namespace Shogi.Domain; - -public class BoardState -{ - /// - /// Board state before any moves have been made, using standard setup and rules. - /// - public static BoardState StandardStarting => new( - state: BuildStandardStartingBoardState(), - player1Hand: new(), - player2Hand: new(), - whoseTurn: WhichPlayer.Player1, - playerInCheck: null, - previousMove: new Move()); - - /// - /// 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; - } - - /// - /// 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 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 Piece? this[string notation] - { - // TODO: Validate "notation" here and throw an exception if invalid. - get => board[notation]; - set => board[notation] = value; - } - - public Piece? this[Vector2 vector] - { - get => this[Notation.ToBoardNotation(vector)]; - set => this[Notation.ToBoardNotation(vector)] = value; - } - - public Piece? this[int x, int y] - { - get => this[Notation.ToBoardNotation(x, y)]; - set => this[Notation.ToBoardNotation(x, y)] = 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; - } - - 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 static bool IsWithinBoardBoundary(Vector2 position) - { - return position.X <= 8 && position.X >= 0 - && position.Y <= 8 && position.Y >= 0; - } - - internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board - .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) - .ToList(); - - internal void Capture(Vector2 to) - { - var piece = this[to]; - if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); - - piece.Capture(WhoseTurn); - ActivePlayerHand.Add(piece); - } - - /// - /// 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 Piece? QueryFirstPieceInPath(IEnumerable path) - { - foreach (var step in path) - { - if (this[step] != null) return this[step]; - } - return null; - } - - 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), - - ["A2"] = null, - ["B2"] = new Bishop(WhichPlayer.Player1), - ["C2"] = null, - ["D2"] = null, - ["E2"] = null, - ["F2"] = null, - ["G2"] = null, - ["H2"] = new Rook(WhichPlayer.Player1), - ["I2"] = 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), - - ["A4"] = null, - ["B4"] = null, - ["C4"] = null, - ["D4"] = null, - ["E4"] = null, - ["F4"] = null, - ["G4"] = null, - ["H4"] = null, - ["I4"] = null, - - ["A5"] = null, - ["B5"] = null, - ["C5"] = null, - ["D5"] = null, - ["E5"] = null, - ["F5"] = null, - ["G5"] = null, - ["H5"] = null, - ["I5"] = null, - - ["A6"] = null, - ["B6"] = null, - ["C6"] = null, - ["D6"] = null, - ["E6"] = null, - ["F6"] = null, - ["G6"] = null, - ["H6"] = null, - ["I6"] = 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), - - ["A8"] = null, - ["B8"] = new Rook(WhichPlayer.Player2), - ["C8"] = null, - ["D8"] = null, - ["E8"] = null, - ["F8"] = null, - ["G8"] = null, - ["H8"] = new Bishop(WhichPlayer.Player2), - ["I8"] = null, - - ["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/DomainExtensions.cs b/Shogi.Domain/DomainExtensions.cs deleted file mode 100644 index f8f9599..0000000 --- a/Shogi.Domain/DomainExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Shogi.Domain.ValueObjects; - -namespace Shogi.Domain -{ - internal static class DomainExtensions - { - public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; - - public static bool IsBetween(this float self, float min, float max) - { - return self >= min && self <= max; - } - - public static bool IsInsideBoardBoundary(this Vector2 self) - { - return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8); - } - } -} diff --git a/Shogi.Domain/MoveResult.cs b/Shogi.Domain/MoveResult.cs deleted file mode 100644 index fed07d6..0000000 --- a/Shogi.Domain/MoveResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Shogi.Domain -{ - public class MoveResult - { - public bool Success { get; } - public string Reason { get; } - - public MoveResult(bool isSuccess, string reason = "") - { - Success = isSuccess; - Reason = reason; - } - } -} diff --git a/Shogi.Domain/Notation.cs b/Shogi.Domain/Notation.cs deleted file mode 100644 index a6fb831..0000000 --- a/Shogi.Domain/Notation.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Shogi.Domain -{ - public static class Notation - { - private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private static readonly char A = 'A'; - - public static string ToBoardNotation(Vector2 vector) - { - return ToBoardNotation((int)vector.X, (int)vector.Y); - } - - public static string ToBoardNotation(int x, int y) - { - var file = (char)(x + A); - var rank = y + 1; - return $"{file}{rank}"; - } - public static Vector2 FromBoardNotation(string notation) - { - if (Regex.IsMatch(notation, BoardNotationRegex)) - { - var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase); - char file = match.Groups["file"].Value[0]; - int rank = int.Parse(match.Groups["rank"].Value); - return new Vector2(file - A, rank - 1); - } - throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); - } - } -} diff --git a/Shogi.Domain/Pathing/Direction.cs b/Shogi.Domain/Pathing/Direction.cs deleted file mode 100644 index 4aae4e5..0000000 --- a/Shogi.Domain/Pathing/Direction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Numerics; - -namespace Shogi.Domain.Pathing -{ - /// - /// Directions are relative to the perspective of Player 1. - /// Up points towards player 1. Down points towards player 2. - /// - public static class Direction - { - public static readonly Vector2 Up = new(0, 1); - public static readonly Vector2 Down = new(0, -1); - public static readonly Vector2 Left = new(-1, 0); - public static readonly Vector2 Right = new(1, 0); - public static readonly Vector2 UpLeft = new(-1, 1); - public static readonly Vector2 UpRight = new(1, 1); - public static readonly Vector2 DownLeft = new(-1, -1); - public static readonly Vector2 DownRight = new(1, -1); - public static readonly Vector2 KnightLeft = new(-1, 2); - public static readonly Vector2 KnightRight = new(1, 2); - } -} diff --git a/Shogi.Domain/Pathing/Distance.cs b/Shogi.Domain/Pathing/Distance.cs deleted file mode 100644 index b1078f5..0000000 --- a/Shogi.Domain/Pathing/Distance.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Shogi.Domain.Pathing -{ - public enum Distance - { - OneStep, - MultiStep - } -} \ No newline at end of file diff --git a/Shogi.Domain/Pathing/Path.cs b/Shogi.Domain/Pathing/Path.cs deleted file mode 100644 index 5959c8f..0000000 --- a/Shogi.Domain/Pathing/Path.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics; - -namespace Shogi.Domain.Pathing -{ - [DebuggerDisplay("{Direction} - {Distance}")] - public class Path - { - public Vector2 Direction { get; } - public Distance Distance { get; } - public Path(Vector2 direction, Distance distance = Distance.OneStep) - { - Direction = direction; - Distance = distance; - } - - public Path Invert() => new(Vector2.Negate(Direction), Distance); - } - - public static class PathExtensions - { - public static Path GetNearestPath(this IEnumerable paths, Vector2 start, Vector2 end) - { - if (!paths.DefaultIfEmpty().Any()) - { - throw new ArgumentException("No paths to get nearest path from."); - } - - var shortestPath = paths.First(); - foreach (var path in paths.Skip(1)) - { - var distance = Vector2.Distance(start + path.Direction, end); - var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); - if (distance < shortestDistance) - { - shortestPath = path; - } - } - return shortestPath; - } - } -} diff --git a/Shogi.Domain/ValueObjects/Bishop.cs b/Shogi.Domain/ValueObjects/Bishop.cs index 076ceeb..8302db9 100644 --- a/Shogi.Domain/ValueObjects/Bishop.cs +++ b/Shogi.Domain/ValueObjects/Bishop.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/BoardState.cs b/Shogi.Domain/ValueObjects/BoardState.cs new file mode 100644 index 0000000..e1b4ba9 --- /dev/null +++ b/Shogi.Domain/ValueObjects/BoardState.cs @@ -0,0 +1,252 @@ +using System.Collections.ObjectModel; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; +using BoardTile = System.Collections.Generic.KeyValuePair; + +namespace Shogi.Domain.ValueObjects; + +public class BoardState +{ + /// + /// Board state before any moves have been made, using standard setup and rules. + /// + public static BoardState StandardStarting => new( + state: BuildStandardStartingBoardState(), + player1Hand: new(), + player2Hand: new(), + whoseTurn: WhichPlayer.Player1, + playerInCheck: null, + previousMove: new Move()); + + /// + /// 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; + } + + /// + /// 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 ReadOnlyDictionary State => new(board); + public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public Vector2 Player1KingPosition => Notation.FromBoardNotation(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(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 Piece? this[string notation] + { + // TODO: Validate "notation" here and throw an exception if invalid. + get => board[notation]; + set => board[notation] = value; + } + + public Piece? this[Vector2 vector] + { + get => this[Notation.ToBoardNotation(vector)]; + set => this[Notation.ToBoardNotation(vector)] = value; + } + + public Piece? this[int x, int y] + { + get => this[Notation.ToBoardNotation(x, y)]; + set => this[Notation.ToBoardNotation(x, y)] = 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; + } + + 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 static bool IsWithinBoardBoundary(Vector2 position) + { + return position.X <= 8 && position.X >= 0 + && position.Y <= 8 && position.Y >= 0; + } + + internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + .Where(kvp => kvp.Value?.Owner == whichPlayer) + .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) + .ToList(); + + internal void Capture(Vector2 to) + { + var piece = this[to]; + if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); + + piece.Capture(WhoseTurn); + ActivePlayerHand.Add(piece); + } + + /// + /// 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 Piece? QueryFirstPieceInPath(IEnumerable path) + { + foreach (var step in path) + { + if (this[step] != null) return this[step]; + } + return null; + } + + 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), + + ["A2"] = null, + ["B2"] = new Bishop(WhichPlayer.Player1), + ["C2"] = null, + ["D2"] = null, + ["E2"] = null, + ["F2"] = null, + ["G2"] = null, + ["H2"] = new Rook(WhichPlayer.Player1), + ["I2"] = 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), + + ["A4"] = null, + ["B4"] = null, + ["C4"] = null, + ["D4"] = null, + ["E4"] = null, + ["F4"] = null, + ["G4"] = null, + ["H4"] = null, + ["I4"] = null, + + ["A5"] = null, + ["B5"] = null, + ["C5"] = null, + ["D5"] = null, + ["E5"] = null, + ["F5"] = null, + ["G5"] = null, + ["H5"] = null, + ["I5"] = null, + + ["A6"] = null, + ["B6"] = null, + ["C6"] = null, + ["D6"] = null, + ["E6"] = null, + ["F6"] = null, + ["G6"] = null, + ["H6"] = null, + ["I6"] = 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), + + ["A8"] = null, + ["B8"] = new Rook(WhichPlayer.Player2), + ["C8"] = null, + ["D8"] = null, + ["E8"] = null, + ["F8"] = null, + ["G8"] = null, + ["H8"] = new Bishop(WhichPlayer.Player2), + ["I8"] = null, + + ["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/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/GoldGeneral.cs index 52c19be..38c22d1 100644 --- a/Shogi.Domain/ValueObjects/GoldGeneral.cs +++ b/Shogi.Domain/ValueObjects/GoldGeneral.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/King.cs b/Shogi.Domain/ValueObjects/King.cs index af3c91b..8ecb744 100644 --- a/Shogi.Domain/ValueObjects/King.cs +++ b/Shogi.Domain/ValueObjects/King.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Knight.cs b/Shogi.Domain/ValueObjects/Knight.cs index dbdcf74..d54cb98 100644 --- a/Shogi.Domain/ValueObjects/Knight.cs +++ b/Shogi.Domain/ValueObjects/Knight.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Lance.cs b/Shogi.Domain/ValueObjects/Lance.cs index 52cc1f0..3f008d3 100644 --- a/Shogi.Domain/ValueObjects/Lance.cs +++ b/Shogi.Domain/ValueObjects/Lance.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/MoveResult.cs b/Shogi.Domain/ValueObjects/MoveResult.cs new file mode 100644 index 0000000..a2d3cae --- /dev/null +++ b/Shogi.Domain/ValueObjects/MoveResult.cs @@ -0,0 +1,14 @@ +namespace Shogi.Domain.ValueObjects +{ + public class MoveResult + { + public bool Success { get; } + public string Reason { get; } + + public MoveResult(bool isSuccess, string reason = "") + { + Success = isSuccess; + Reason = reason; + } + } +} diff --git a/Shogi.Domain/ValueObjects/Pawn.cs b/Shogi.Domain/ValueObjects/Pawn.cs index 566792b..016e86a 100644 --- a/Shogi.Domain/ValueObjects/Pawn.cs +++ b/Shogi.Domain/ValueObjects/Pawn.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Piece.cs index d7686ab..e620d6a 100644 --- a/Shogi.Domain/ValueObjects/Piece.cs +++ b/Shogi.Domain/ValueObjects/Piece.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Diagnostics; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Rook.cs b/Shogi.Domain/ValueObjects/Rook.cs index 505d944..f87dd19 100644 --- a/Shogi.Domain/ValueObjects/Rook.cs +++ b/Shogi.Domain/ValueObjects/Rook.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects; diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs index 116a962..fbc793c 100644 --- a/Shogi.Domain/ValueObjects/ShogiBoard.cs +++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs @@ -1,4 +1,5 @@ using System.Text; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; namespace Shogi.Domain.ValueObjects; diff --git a/Shogi.Domain/ValueObjects/SilverGeneral.cs b/Shogi.Domain/ValueObjects/SilverGeneral.cs index 71b55df..9d332a4 100644 --- a/Shogi.Domain/ValueObjects/SilverGeneral.cs +++ b/Shogi.Domain/ValueObjects/SilverGeneral.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.Pathing; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/ValueObjects/StandardRules.cs similarity index 96% rename from Shogi.Domain/StandardRules.cs rename to Shogi.Domain/ValueObjects/StandardRules.cs index 841720b..5cd8349 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/ValueObjects/StandardRules.cs @@ -1,7 +1,7 @@ -using Shogi.Domain.ValueObjects; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; using BoardTile = System.Collections.Generic.KeyValuePair; -namespace Shogi.Domain +namespace Shogi.Domain.ValueObjects { internal class StandardRules { @@ -84,8 +84,8 @@ namespace Shogi.Domain case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. - if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) - || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) + if (boardState.WhoseTurn == WhichPlayer.Player1 && to.Y > 6 + || boardState.WhoseTurn == WhichPlayer.Player2 && to.Y < 2) { return new MoveResult(false, "Knight has no valid moves after placed."); } @@ -95,8 +95,8 @@ namespace Shogi.Domain case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y == 8) - || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) + if (boardState.WhoseTurn == WhichPlayer.Player1 && to.Y == 8 + || boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0) { return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); } @@ -222,7 +222,7 @@ namespace Shogi.Domain * If no ability to block the check, maybe the king can evade check by moving. */ - foreach (var maybeSafePosition in GetPossiblePositionsForKing(this.boardState.WhoseTurn)) + foreach (var maybeSafePosition in GetPossiblePositionsForKing(boardState.WhoseTurn)) { threats = tilesOccupiedByOpponent .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) diff --git a/Shogi.Domain/ValueObjects/WhichPiece.cs b/Shogi.Domain/ValueObjects/WhichPiece.cs new file mode 100644 index 0000000..604909b --- /dev/null +++ b/Shogi.Domain/ValueObjects/WhichPiece.cs @@ -0,0 +1,14 @@ +namespace Shogi.Domain.ValueObjects +{ + public enum WhichPiece + { + King, + GoldGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn + } +} diff --git a/Shogi.Domain/ValueObjects/WhichPlayer.cs b/Shogi.Domain/ValueObjects/WhichPlayer.cs new file mode 100644 index 0000000..796c095 --- /dev/null +++ b/Shogi.Domain/ValueObjects/WhichPlayer.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain.ValueObjects +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Shogi.Domain/WhichPiece.cs b/Shogi.Domain/WhichPiece.cs deleted file mode 100644 index 58a1669..0000000 --- a/Shogi.Domain/WhichPiece.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Shogi.Domain -{ - public enum WhichPiece - { - King, - GoldGeneral, - SilverGeneral, - Bishop, - Rook, - Knight, - Lance, - Pawn - } -} diff --git a/Shogi.Domain/WhichPlayer.cs b/Shogi.Domain/WhichPlayer.cs deleted file mode 100644 index 584d852..0000000 --- a/Shogi.Domain/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Shogi.Domain -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs new file mode 100644 index 0000000..dd5c72a --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs @@ -0,0 +1,19 @@ +using Shogi.Domain.ValueObjects; + +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD +{ + internal static class DomainExtensions + { + public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; + + public static bool IsBetween(this float self, float min, float max) + { + return self >= min && self <= max; + } + + public static bool IsInsideBoardBoundary(this Vector2 self) + { + return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8); + } + } +} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs new file mode 100644 index 0000000..f75600e --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD +{ + public static class Notation + { + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private static readonly char A = 'A'; + + public static string ToBoardNotation(Vector2 vector) + { + return ToBoardNotation((int)vector.X, (int)vector.Y); + } + + public static string ToBoardNotation(int x, int y) + { + var file = (char)(x + A); + var rank = y + 1; + return $"{file}{rank}"; + } + public static Vector2 FromBoardNotation(string notation) + { + if (Regex.IsMatch(notation, BoardNotationRegex)) + { + var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase); + char file = match.Groups["file"].Value[0]; + int rank = int.Parse(match.Groups["rank"].Value); + return new Vector2(file - A, rank - 1); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); + } + } +} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs new file mode 100644 index 0000000..622d436 --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs @@ -0,0 +1,22 @@ +using System.Numerics; + +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing +{ + /// + /// Directions are relative to the perspective of Player 1. + /// Up points towards player 1. Down points towards player 2. + /// + public static class Direction + { + public static readonly Vector2 Up = new(0, 1); + public static readonly Vector2 Down = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 UpLeft = new(-1, 1); + public static readonly Vector2 UpRight = new(1, 1); + public static readonly Vector2 DownLeft = new(-1, -1); + public static readonly Vector2 DownRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); + } +} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs new file mode 100644 index 0000000..e9a7ff2 --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing +{ + public enum Distance + { + OneStep, + MultiStep + } +} \ No newline at end of file diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs new file mode 100644 index 0000000..2b8042a --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; + +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing +{ + [DebuggerDisplay("{Direction} - {Distance}")] + public class Path + { + public Vector2 Direction { get; } + public Distance Distance { get; } + public Path(Vector2 direction, Distance distance = Distance.OneStep) + { + Direction = direction; + Distance = distance; + } + + public Path Invert() => new(Vector2.Negate(Direction), Distance); + } + + public static class PathExtensions + { + public static Path GetNearestPath(this IEnumerable paths, Vector2 start, Vector2 end) + { + if (!paths.DefaultIfEmpty().Any()) + { + throw new ArgumentException("No paths to get nearest path from."); + } + + var shortestPath = paths.First(); + foreach (var path in paths.Skip(1)) + { + var distance = Vector2.Distance(start + path.Direction, end); + var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); + if (distance < shortestDistance) + { + shortestPath = path; + } + } + return shortestPath; + } + } +} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md b/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md new file mode 100644 index 0000000..51bfb0f --- /dev/null +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md @@ -0,0 +1,4 @@ +# Shogi.Domain + +### TODO: +* There are enough non-DDD classes around navigating the standard 9x9 Shogi board that probably a new value object or entity is merited. See classes within Pathing folder as well as Notation.cs. \ No newline at end of file diff --git a/Tests/UnitTests/NotationShould.cs b/Tests/UnitTests/NotationShould.cs index 456886b..0e71e82 100644 --- a/Tests/UnitTests/NotationShould.cs +++ b/Tests/UnitTests/NotationShould.cs @@ -1,8 +1,9 @@ using System.Numerics; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; namespace Shogi.Domain.UnitTests { - public class NotationShould + public class NotationShould { [Fact] public void ConvertFromNotationToVector() diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index d527f1b..b472c4a 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -1,225 +1,222 @@ -using Shogi.Domain.Pathing; -using Shogi.Domain.ValueObjects; +using Shogi.Domain.ValueObjects; +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using System.Numerics; -namespace Shogi.Domain.UnitTests +namespace Shogi.Domain.UnitTests; + +public class RookShould { - public class RookShould - { public class MoveSet { - private readonly Rook rook1; - private readonly Rook rook2; + private readonly Rook rook1; + private readonly Rook rook2; - public MoveSet() - { - this.rook1 = new Rook(WhichPlayer.Player1); - this.rook2 = new Rook(WhichPlayer.Player2); - } + public MoveSet() + { + this.rook1 = new Rook(WhichPlayer.Player1); + this.rook2 = new Rook(WhichPlayer.Player2); + } - [Fact] - public void Player1_HasCorrectMoveSet() - { - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - } + [Fact] + public void Player1_HasCorrectMoveSet() + { + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + } - [Fact] - public void Player1_Promoted_HasCorrectMoveSet() - { - // Arrange - rook1.Promote(); - rook1.IsPromoted.Should().BeTrue(); + [Fact] + public void Player1_Promoted_HasCorrectMoveSet() + { + // Arrange + rook1.Promote(); + rook1.IsPromoted.Should().BeTrue(); - // Assert - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); - } + // Assert + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + } - [Fact] - public void Player2_HasCorrectMoveSet() - { - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - } + [Fact] + public void Player2_HasCorrectMoveSet() + { + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + } - [Fact] - public void Player2_Promoted_HasCorrectMoveSet() - { - // Arrange - rook2.Promote(); - rook2.IsPromoted.Should().BeTrue(); + [Fact] + public void Player2_Promoted_HasCorrectMoveSet() + { + // Arrange + rook2.Promote(); + rook2.IsPromoted.Should().BeTrue(); - // Assert - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); - } + // Assert + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + } } private readonly Rook rookPlayer1; - private readonly Rook rookPlayer2; public RookShould() { - this.rookPlayer1 = new Rook(WhichPlayer.Player1); - this.rookPlayer2 = new Rook(WhichPlayer.Player2); + this.rookPlayer1 = new Rook(WhichPlayer.Player1); } [Fact] public void Promote() { - this.rookPlayer1.IsPromoted.Should().BeFalse(); - this.rookPlayer1.CanPromote.Should().BeTrue(); - this.rookPlayer1.Promote(); - this.rookPlayer1.IsPromoted.Should().BeTrue(); - this.rookPlayer1.CanPromote.Should().BeFalse(); + this.rookPlayer1.IsPromoted.Should().BeFalse(); + this.rookPlayer1.CanPromote.Should().BeTrue(); + this.rookPlayer1.Promote(); + this.rookPlayer1.IsPromoted.Should().BeTrue(); + this.rookPlayer1.CanPromote.Should().BeFalse(); } [Fact] public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); } [Fact] public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); } [Fact] public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); } [Fact] public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); } [Fact] public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); } [Fact] public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); } [Fact] public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); } [Fact] public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); } - } } diff --git a/Tests/UnitTests/ShogiBoardStateShould.cs b/Tests/UnitTests/ShogiBoardStateShould.cs index 9d0ad55..f3cfb83 100644 --- a/Tests/UnitTests/ShogiBoardStateShould.cs +++ b/Tests/UnitTests/ShogiBoardStateShould.cs @@ -1,4 +1,6 @@ -namespace Shogi.Domain.UnitTests; +using Shogi.Domain.ValueObjects; + +namespace Shogi.Domain.UnitTests; public class ShogiBoardStateShould {