diff --git a/Shogi.Domain/Other/BoardRules.cs b/Shogi.Domain/Other/BoardRules.cs
new file mode 100644
index 0000000..8639730
--- /dev/null
+++ b/Shogi.Domain/Other/BoardRules.cs
@@ -0,0 +1,81 @@
+
+namespace Shogi.Domain.Other;
+
+///
+///
+///
+/// A 2D array of pieces, representing your board. Indexed as [x, y].
+public class BoardRules(TPiece?[,] boardState) where TPiece : IRulesLifecycle
+{
+ private readonly Vector2 MaxIndex = new(boardState.GetLength(0) - 1, boardState.GetLength(1) - 1);
+
+ ///
+ /// Validates a move, invoking the callback which you should implement.
+ /// A move is considered valid if it could be made legally, ignoring check or check-mate rules.
+ /// Check and check-mate verifying happens in a different lifecycle callback.
+ ///
+ /// The position of the piece being moved.
+ /// The desired destination of the piece being moved.
+ /// TODO
+ ///
+ public RulesLifecycleResult ValidateMove(Vector2 from, Vector2 to, bool isPromotion)
+ {
+ if (IsWithinBounds(from) && IsWithinBounds(to))
+ {
+ var piece = boardState[(int)from.X, (int)from.Y];
+ if (piece == null)
+ {
+ return new RulesLifecycleResult(IsError: true, $"There is no piece at position {from}.");
+ }
+
+ return piece.OnMoveValidation(new MoveValidationContext(from, to, isPromotion, boardState));
+ }
+
+ return new RulesLifecycleResult(IsError: true, "test message");
+ }
+
+
+ //foreach (var piece in boardState)
+ //{
+ // if (piece != null)
+ // {
+ // var result = piece.OnMoveValidation(new MoveValidationContext(from, to, isPromotion, boardState));
+ // if (result.IsError)
+ // {
+ // return result;
+ // }
+ // }
+ //}
+
+ //public int ValidateMove(Vector2 start, Vector2 end)
+ //{
+ // if (board.GetLength(0) != boardSize.X || board.GetLength(1) != boardSize.Y)
+ // {
+ // throw new ArgumentException($"2D array dimensions must match boardSize given during {nameof(CreateNewRules)} method.", nameof(board));
+ // }
+ // if (start - start != Vector2.Zero)
+ // {
+ // throw new ArgumentException("Negative values not allowed.", nameof(start));
+ // }
+ // if (end - end != Vector2.Zero)
+ // {
+ // throw new ArgumentException("Negative values not allowed.", nameof(end));
+ // }
+ // if (start.X >= boardSize.X || start.Y >= boardSize.Y)
+ // {
+ // throw new ArgumentException("Start position must be within the given boardSize.", nameof(start));
+ // }
+ // if (end.X >= boardSize.X || end.Y >= boardSize.Y)
+ // {
+ // throw new ArgumentException("End position must be within the given boardSize.", nameof(end));
+ // }
+
+ // return 0;
+ //}
+
+ private bool IsWithinBounds(Vector2 position)
+ {
+ var isPositive = position - position == Vector2.Zero;
+ return isPositive && position.X <= MaxIndex.X && position.Y <= MaxIndex.Y;
+ }
+}
\ No newline at end of file
diff --git a/Shogi.Domain/Other/IRulesLifecycle.cs b/Shogi.Domain/Other/IRulesLifecycle.cs
new file mode 100644
index 0000000..4255bcd
--- /dev/null
+++ b/Shogi.Domain/Other/IRulesLifecycle.cs
@@ -0,0 +1,15 @@
+namespace Shogi.Domain.Other;
+
+public interface IRulesLifecycle where TPiece : IRulesLifecycle
+{
+ ///
+ /// Invoked by during the MoveValidation life cycle event.
+ /// If a move begins or ends outside the board space coordinates, this function is not called.
+ /// The purpose is to ensure a proposed board move is valid with regard to the moved piece's rules.
+ /// This event does not worry about check or check-mate, or if a move is legal.
+ ///
+ ///
+ /// A context object with information for you to use to assess whether a move is valid for your implementing piece.
+ /// A new object indicating whether or not the move is valid.
+ RulesLifecycleResult OnMoveValidation(MoveValidationContext context);
+}
diff --git a/Shogi.Domain/Other/MoveValidationContext.cs b/Shogi.Domain/Other/MoveValidationContext.cs
new file mode 100644
index 0000000..32b4206
--- /dev/null
+++ b/Shogi.Domain/Other/MoveValidationContext.cs
@@ -0,0 +1,14 @@
+namespace Shogi.Domain.Other;
+
+public record MoveValidationContext(
+ Vector2 From,
+ Vector2 To,
+ bool IsPromotion,
+ TPiece?[,] BoardState) where TPiece : IRulesLifecycle
+{
+ public TPiece? GetPieceByRelativePosition(Vector2 relativePosition)
+ {
+ var absolute = From + relativePosition;
+ return BoardState[(int)absolute.X, (int)absolute.Y];
+ }
+}
diff --git a/Shogi.Domain/Other/PieceRulesRegistration.cs b/Shogi.Domain/Other/PieceRulesRegistration.cs
deleted file mode 100644
index 9661745..0000000
--- a/Shogi.Domain/Other/PieceRulesRegistration.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
-
-namespace Shogi.Domain.Other;
-
-public record PieceRulesRegistration(TPiece WhichPiece, ICollection MoveSet) where TPiece : Enum
-{
-}
-
-public record PieceInPlay(TPiece WhichPiece, int OwningPlayerNumber) where TPiece : Enum
-{
-}
diff --git a/Shogi.Domain/Other/Rules.cs b/Shogi.Domain/Other/Rules.cs
deleted file mode 100644
index d2b3c89..0000000
--- a/Shogi.Domain/Other/Rules.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
-using System.Drawing;
-
-namespace Shogi.Domain.Other;
-
-public class Rules where TPiece : Enum
-{
- private Vector2 boardSize;
- private TPiece theKing;
- private Dictionary> piecePaths;
-
- ///
- /// Begin a new set of rules. If any rules already exist, this method will erase them.
- ///
- /// The size of the game board in tiles. For examples, Chess is 8x8 and Shogi is 9x9.
- /// The piece that represents the King for each player or the piece that, when lost, results in losing the game.
- public Rules CreateNewRules(Vector2 boardSize, TPiece king)
- {
- this.boardSize = boardSize;
- theKing = king;
- piecePaths = [];
- return this;
- }
-
- public Rules RegisterPieceWithRules(PieceRulesRegistration pieceToRegister)
- {
- if (piecePaths.ContainsKey(pieceToRegister.WhichPiece))
- {
- throw new ArgumentException("This type of piece has already been registered.", nameof(pieceToRegister));
- }
-
- piecePaths.Add(pieceToRegister.WhichPiece, pieceToRegister.MoveSet);
- return this;
- }
-
- public int ValidateMove(Vector2 start, Vector2 end, TPiece[][] board)
- {
- if (board.GetLength(0) != boardSize.X || board.GetLength(1) != boardSize.Y)
- {
- throw new ArgumentException($"2D array dimensions must match boardSize given during {nameof(CreateNewRules)} method.", nameof(board));
- }
- if (start - start != Vector2.Zero)
- {
- throw new ArgumentException("Negative values not allowed.", nameof(start));
- }
- if (end - end != Vector2.Zero)
- {
- throw new ArgumentException("Negative values not allowed.", nameof(end));
- }
- if (start.X >= boardSize.X || start.Y >= boardSize.Y)
- {
- throw new ArgumentException("Start position must be within the given boardSize.", nameof(start));
- }
- if (end.X >= boardSize.X || end.Y >= boardSize.Y)
- {
- throw new ArgumentException("End position must be within the given boardSize.", nameof(end));
- }
-
-
- }
-}
\ No newline at end of file
diff --git a/Shogi.Domain/Other/RulesLifecycleResult.cs b/Shogi.Domain/Other/RulesLifecycleResult.cs
new file mode 100644
index 0000000..5dd255c
--- /dev/null
+++ b/Shogi.Domain/Other/RulesLifecycleResult.cs
@@ -0,0 +1,5 @@
+namespace Shogi.Domain.Other;
+
+public record RulesLifecycleResult(bool IsError, string ResultMessage = "")
+{
+}
diff --git a/Shogi.Domain/ValueObjects/BoardState.cs b/Shogi.Domain/ValueObjects/BoardState.cs
index a8c3dbb..5581ff5 100644
--- a/Shogi.Domain/ValueObjects/BoardState.cs
+++ b/Shogi.Domain/ValueObjects/BoardState.cs
@@ -95,6 +95,92 @@ public class BoardState
set => this[Notation.ToBoardNotation(x, y)] = value;
}
+ ///
+ /// Move a piece from a board tile to another board tile ignorant of check or check-mate.
+ /// If a piece is captured during the move, state will change to reflect that.
+ /// If a piece should promote during the move, state will change to reflect that.
+ ///
+ /// The position of the piece being moved expressed in board notation.
+ /// The target position expressed in board notation.
+ /// True if a promotion is expected as a result of this move.
+ /// A describing the success or failure of the move.
+ internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false)
+ {
+ var from = Notation.FromBoardNotation(fromNotation);
+ var to = Notation.FromBoardNotation(toNotation);
+ if (this[toNotation] != null)
+ {
+ Capture(to);
+ }
+
+ var fromPiece = this[fromNotation]
+ ?? throw new InvalidOperationException($"No piece exists at position {fromNotation}.");
+ if (isPromotionRequested &&
+ (IsWithinPromotionZone(to) || IsWithinPromotionZone(from)))
+ {
+ fromPiece.Promote();
+ }
+
+ this[toNotation] = fromPiece;
+ this[fromNotation] = null;
+
+ PreviousMove = new Move(from, to);
+ var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
+ WhoseTurn = otherPlayer;
+
+ return new MoveResult(true);
+ }
+
+ /// Move a piece from the hand to the board ignorant of check or check-mate.
+ ///
+ ///
+ /// The target position expressed in board notation.
+ /// A describing the success or failure of the simulation.
+ internal MoveResult Move(WhichPiece pieceInHand, string toNotation)
+ {
+ var index = ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
+ if (index == -1)
+ {
+ return new MoveResult(false, $"{pieceInHand} does not exist in the hand.");
+ }
+ if (this[toNotation] != null)
+ {
+ return new MoveResult(false, "Illegal move - attempting to capture while playing a piece from the hand.");
+ }
+
+ var to = Notation.FromBoardNotation(toNotation);
+ switch (pieceInHand)
+ {
+ case WhichPiece.Knight:
+ {
+ // Knight cannot be placed onto the farthest two ranks from the hand.
+ if (WhoseTurn == WhichPlayer.Player1 && to.Y > 6
+ || WhoseTurn == WhichPlayer.Player2 && to.Y < 2)
+ {
+ return new MoveResult(false, "Knight has no valid moves after placed.");
+ }
+ break;
+ }
+ case WhichPiece.Lance:
+ case WhichPiece.Pawn:
+ {
+ // Lance and Pawn cannot be placed onto the farthest rank from the hand.
+ if (WhoseTurn == WhichPlayer.Player1 && to.Y == 8
+ || WhoseTurn == WhichPlayer.Player2 && to.Y == 0)
+ {
+ return new MoveResult(false, $"{pieceInHand} has no valid moves after placed.");
+ }
+ break;
+ }
+ }
+
+ // Mutate the board.
+ this[toNotation] = ActivePlayerHand[index];
+ ActivePlayerHand.RemoveAt(index);
+ PreviousMove = new Move(pieceInHand, to);
+ return new MoveResult(true);
+ }
+
///
/// Returns true if the given path can be traversed without colliding into a piece.
///
@@ -107,8 +193,6 @@ public class BoardState
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;
@@ -127,9 +211,8 @@ public class BoardState
internal void Capture(Vector2 to)
{
- var piece = this[to];
- if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
-
+ var piece = this[to]
+ ?? throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
piece.Capture(WhoseTurn);
ActivePlayerHand.Add(piece);
}
diff --git a/Shogi.Domain/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/GoldGeneral.cs
index f54aa16..b1d0c26 100644
--- a/Shogi.Domain/ValueObjects/GoldGeneral.cs
+++ b/Shogi.Domain/ValueObjects/GoldGeneral.cs
@@ -1,31 +1,30 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Collections.ObjectModel;
-namespace Shogi.Domain.ValueObjects
+namespace Shogi.Domain.ValueObjects;
+
+internal record class GoldGeneral : Piece
{
- internal record class GoldGeneral : Piece
- {
- public static readonly ReadOnlyCollection Player1Paths = new(new List(6)
- {
- new Path(Direction.Forward),
- new Path(Direction.ForwardLeft),
- new Path(Direction.ForwardRight),
- new Path(Direction.Left),
- new Path(Direction.Right),
- new Path(Direction.Backward)
- });
+ private static readonly ReadOnlyCollection Player1Paths = new(new List(6)
+ {
+ new Path(Direction.Forward),
+ new Path(Direction.ForwardLeft),
+ new Path(Direction.ForwardRight),
+ new Path(Direction.Left),
+ new Path(Direction.Right),
+ new Path(Direction.Backward)
+ });
- public static readonly ReadOnlyCollection Player2Paths =
- Player1Paths
- .Select(p => p.Invert())
- .ToList()
- .AsReadOnly();
+ private static readonly ReadOnlyCollection Player2Paths =
+ Player1Paths
+ .Select(p => p.Invert())
+ .ToList()
+ .AsReadOnly();
- public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
- : base(WhichPiece.GoldGeneral, owner, isPromoted)
- {
- }
+ public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
+ : base(WhichPiece.GoldGeneral, owner, isPromoted)
+ {
+ }
- public override IEnumerable MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
- }
+ public override IEnumerable MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
}
diff --git a/Shogi.Domain/ValueObjects/King.cs b/Shogi.Domain/ValueObjects/King.cs
index 5238e29..d7cf66c 100644
--- a/Shogi.Domain/ValueObjects/King.cs
+++ b/Shogi.Domain/ValueObjects/King.cs
@@ -1,27 +1,30 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Collections.ObjectModel;
-namespace Shogi.Domain.ValueObjects
+namespace Shogi.Domain.ValueObjects;
+
+internal record class King : Piece
{
- internal record class King : Piece
- {
- internal static readonly ReadOnlyCollection KingPaths = new(new List(8)
- {
- new Path(Direction.Forward),
- new Path(Direction.Left),
- new Path(Direction.Right),
- new Path(Direction.Backward),
- new Path(Direction.ForwardLeft),
- new Path(Direction.ForwardRight),
- new Path(Direction.BackwardLeft),
- new Path(Direction.BackwardRight)
- });
+ private static readonly ReadOnlyCollection Player1Paths = new(
+ [
+ new Path(Direction.Forward),
+ new Path(Direction.Left),
+ new Path(Direction.Right),
+ new Path(Direction.Backward),
+ new Path(Direction.ForwardLeft),
+ new Path(Direction.ForwardRight),
+ new Path(Direction.BackwardLeft),
+ new Path(Direction.BackwardRight)
+ ]);
+
+ private static readonly ReadOnlyCollection Player2Paths = Player1Paths.Select(p => p.Invert()).ToList().AsReadOnly();
+
+ public King(WhichPlayer owner, bool isPromoted = false)
+ : base(WhichPiece.King, owner, isPromoted)
+ {
+ }
+
+ public override IEnumerable MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
- public King(WhichPlayer owner, bool isPromoted = false)
- : base(WhichPiece.King, owner, isPromoted)
- {
- }
- public override IEnumerable MoveSet => KingPaths;
- }
}
diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Piece.cs
index e620d6a..40b534d 100644
--- a/Shogi.Domain/ValueObjects/Piece.cs
+++ b/Shogi.Domain/ValueObjects/Piece.cs
@@ -1,91 +1,126 @@
-using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
+using Shogi.Domain.Other;
+using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Diagnostics;
namespace Shogi.Domain.ValueObjects
{
- [DebuggerDisplay("{WhichPiece} {Owner}")]
- public abstract record class Piece
- {
- public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
- {
- return piece switch
- {
- WhichPiece.King => new King(owner, isPromoted),
- WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
- WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
- WhichPiece.Bishop => new Bishop(owner, isPromoted),
- WhichPiece.Rook => new Rook(owner, isPromoted),
- WhichPiece.Knight => new Knight(owner, isPromoted),
- WhichPiece.Lance => new Lance(owner, isPromoted),
- WhichPiece.Pawn => new Pawn(owner, isPromoted),
- _ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
- };
- }
- public abstract IEnumerable MoveSet { get; }
- public WhichPiece WhichPiece { get; }
- public WhichPlayer Owner { get; private set; }
- public bool IsPromoted { get; private set; }
- public bool IsUpsideDown => Owner == WhichPlayer.Player2;
+ [DebuggerDisplay("{WhichPiece} {Owner}")]
+ public abstract record class Piece : IRulesLifecycle
+ {
+ public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
+ {
+ return piece switch
+ {
+ WhichPiece.King => new King(owner, isPromoted),
+ WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
+ WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
+ WhichPiece.Bishop => new Bishop(owner, isPromoted),
+ WhichPiece.Rook => new Rook(owner, isPromoted),
+ WhichPiece.Knight => new Knight(owner, isPromoted),
+ WhichPiece.Lance => new Lance(owner, isPromoted),
+ WhichPiece.Pawn => new Pawn(owner, isPromoted),
+ _ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
+ };
+ }
+ public abstract IEnumerable MoveSet { get; }
+ public WhichPiece WhichPiece { get; }
+ public WhichPlayer Owner { get; private set; }
+ public bool IsPromoted { get; private set; }
+ public bool IsUpsideDown => Owner == WhichPlayer.Player2;
- protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
- {
- WhichPiece = piece;
- Owner = owner;
- IsPromoted = isPromoted;
- }
+ protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
+ {
+ WhichPiece = piece;
+ Owner = owner;
+ IsPromoted = isPromoted;
+ }
- public bool CanPromote => !IsPromoted
- && WhichPiece != WhichPiece.King
- && WhichPiece != WhichPiece.GoldGeneral;
+ public bool CanPromote => !IsPromoted
+ && WhichPiece != WhichPiece.King
+ && WhichPiece != WhichPiece.GoldGeneral;
- public void Promote() => IsPromoted = CanPromote;
+ public void Promote() => IsPromoted = CanPromote;
- ///
- /// Prep the piece for capture by changing ownership and demoting.
- ///
- public void Capture(WhichPlayer newOwner)
- {
- Owner = newOwner;
- IsPromoted = false;
- }
+ ///
+ /// Prep the piece for capture by changing ownership and demoting.
+ ///
+ public void Capture(WhichPlayer newOwner)
+ {
+ Owner = newOwner;
+ IsPromoted = false;
+ }
- ///
- /// 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.
- ///
- ///
- ///
- /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.
- public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end)
- {
- var steps = new List(10);
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.
+ public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end)
+ {
+ var steps = new List(10);
- var path = MoveSet.GetNearestPath(start, end);
- var position = start;
- while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
- {
- position += path.Direction;
- steps.Add(position);
+ var path = MoveSet.GetNearestPath(start, end);
+ var position = start;
+ while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
+ {
+ position += path.NormalizedDirection;
+ steps.Add(position);
- if (path.Distance == Distance.OneStep) break;
- }
+ if (path.Distance == Distance.OneStep) break;
+ }
- if (position == end)
- {
- return steps;
- }
+ if (position == end)
+ {
+ return steps;
+ }
- return Array.Empty();
- }
+ return Array.Empty();
+ }
- ///
- /// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece.
- ///
- ///
- /// A list of positions the piece could move to.
- public IEnumerable GetPossiblePositions(Vector2 currentPosition)
- {
- throw new NotImplementedException();
- }
- }
+ #region IRulesLifecycle
+ public RulesLifecycleResult OnMoveValidation(MoveValidationContext ctx)
+ {
+ var paths = this.MoveSet;
+
+ var matchingPaths = paths.Where(p => p.NormalizedDirection == Vector2.Normalize(ctx.To - ctx.From));
+ if (!matchingPaths.Any())
+ {
+ return new RulesLifecycleResult(IsError: true, "Piece cannot move like that.");
+ }
+
+ var multiStepPaths = matchingPaths.Where(p => p.Distance == Distance.MultiStep).ToArray();
+ foreach (var path in multiStepPaths)
+ {
+ // Assert that no pieces exist along the from -> to path.
+ var isPathObstructed = GetPositionsAlongPath(ctx.From, ctx.To, path)
+ .Any(pos => ctx.BoardState[(int)pos.X, (int)pos.Y] != null);
+ if (isPathObstructed)
+ {
+ return new RulesLifecycleResult(IsError: true, "Piece cannot move through other pieces.");
+ }
+
+ }
+
+ var pieceAtTo = ctx.BoardState[(int)ctx.To.X, (int)ctx.To.Y];
+ if (pieceAtTo?.Owner == this.Owner)
+ {
+ return new RulesLifecycleResult(IsError: true, "Cannot capture your own pieces.");
+ }
+
+ return new RulesLifecycleResult(IsError: false);
+
+ static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, Path path)
+ {
+ var next = from;
+ while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9)
+ {
+ next += path.NormalizedDirection;
+ yield return next;
+ }
+ }
+ }
+ #endregion
+ }
}
diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs
index fbc793c..7328dff 100644
--- a/Shogi.Domain/ValueObjects/ShogiBoard.cs
+++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs
@@ -1,4 +1,5 @@
using System.Text;
+using Shogi.Domain.Other;
using Shogi.Domain.YetToBeAssimilatedIntoDDD;
namespace Shogi.Domain.ValueObjects;
@@ -10,234 +11,257 @@ namespace Shogi.Domain.ValueObjects;
///
public sealed class ShogiBoard
{
- private readonly StandardRules rules;
+ private readonly StandardRules rules;
- public ShogiBoard(BoardState initialState)
- {
- BoardState = initialState;
- rules = new StandardRules(BoardState);
- }
+ public ShogiBoard(BoardState initialState)
+ {
+ BoardState = initialState;
+ rules = new StandardRules(BoardState);
+ }
- public BoardState BoardState { get; }
+ 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);
- }
+ ///
+ /// 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 = false)
+ {
+ // Validate the move
+ var rulesState = new Piece?[9, 9];
+ for (int x = 0; x < 9; x++)
+ for (int y = 0; y < 9; y++)
+ {
+ rulesState[x, y] = this.BoardState[x, y];
+ }
- // 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 rules = new BoardRules(rulesState);
+ var validationResult = rules.ValidateMove(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to), isPromotion);
+ if (validationResult.IsError)
+ {
+ throw new InvalidOperationException(validationResult.ResultMessage);
+ }
- if (simulation.DidPlayerPutThemselfInCheck())
- {
- throw new InvalidOperationException("Illegal move. This move places you in check.");
- }
+ // Move is valid, but is it legal?
+ // Check for correct player's turn.
+ if (BoardState.WhoseTurn != BoardState[from]!.Owner)
+ {
+ throw new InvalidOperationException("Not allowed to move the opponent's pieces.");
+ }
- var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
- ? WhichPlayer.Player2
- : WhichPlayer.Player1;
- _ = rules.Move(from, to, isPromotion);
- if (rules.IsOpponentInCheckAfterMove())
- {
- BoardState.InCheck = otherPlayer;
- if (rules.IsOpponentInCheckMate())
- {
- BoardState.IsCheckmate = true;
- }
- }
- else
- {
- BoardState.InCheck = null;
- }
- BoardState.WhoseTurn = otherPlayer;
- }
+ // Simulate the move on a throw -away state and look for "check" and "check-mate".
+ var simulationState = new BoardState(BoardState);
+ var moveResult = simulationState.Move(from, to, isPromotion);
+ if (!moveResult.Success)
+ {
+ throw new InvalidOperationException(moveResult.Reason);
+ }
- 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.");
- }
+ var simulation = new StandardRules(simulationState);
+ // 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.");
+ }
- if (BoardState[to] != null)
- {
- throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
- }
+ if (simulation.DidPlayerPutThemselfInCheck())
+ {
+ throw new InvalidOperationException("Illegal move. This move places you in check.");
+ }
- 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 otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
+ ? WhichPlayer.Player2
+ : WhichPlayer.Player1;
+ _ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that.
+ if (rules.IsOpponentInCheckAfterMove())
+ {
+ BoardState.InCheck = otherPlayer;
+ if (rules.IsOpponentInCheckMate())
+ {
+ BoardState.IsCheckmate = true;
+ }
+ }
+ else
+ {
+ BoardState.InCheck = null;
+ }
+ BoardState.WhoseTurn = otherPlayer;
+ }
- var tempBoard = new BoardState(BoardState);
- var simulation = new StandardRules(tempBoard);
- var moveResult = simulation.Move(pieceInHand, to);
- if (!moveResult.Success)
- {
- throw new InvalidOperationException(moveResult.Reason);
- }
+ 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 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 drop piece becauase you are still in check.");
- }
+ if (BoardState[to] != null)
+ {
+ throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
+ }
- if (simulation.DidPlayerPutThemselfInCheck())
- {
- throw new InvalidOperationException("Illegal move. This move places you in check.");
- }
+ 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;
+ }
+ }
- // Update the non-simulation board.
- var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1
- ? WhichPlayer.Player2
- : WhichPlayer.Player1;
- _ = rules.Move(pieceInHand, to);
- if (rules.IsOpponentInCheckAfterMove())
- {
- BoardState.InCheck = otherPlayer;
- // A pawn, placed from the hand, cannot be the cause of checkmate.
- if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn)
- {
- BoardState.IsCheckmate = true;
- }
- }
+ 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 kingPosition = otherPlayer == WhichPlayer.Player1
- ? tempBoard.Player1KingPosition
- : tempBoard.Player2KingPosition;
- BoardState.WhoseTurn = otherPlayer;
- }
+ // 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 drop piece becauase you are still in check.");
+ }
- ///
- /// 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("- -");
+ if (simulation.DidPlayerPutThemselfInCheck())
+ {
+ throw new InvalidOperationException("Illegal move. This move places you in check.");
+ }
- // Print Rank ruler.
- builder.AppendLine();
- builder.Append($"{rank + 1} ");
+ // Update the non-simulation board.
+ var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1
+ ? WhichPlayer.Player2
+ : WhichPlayer.Player1;
+ _ = rules.Move(pieceInHand, to);
+ if (rules.IsOpponentInCheckAfterMove())
+ {
+ BoardState.InCheck = otherPlayer;
+ // A pawn, placed from the hand, cannot be the cause of checkmate.
+ if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn)
+ {
+ BoardState.IsCheckmate = true;
+ }
+ }
- // 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();
- }
+ var kingPosition = otherPlayer == WhichPlayer.Player1
+ ? tempBoard.Player1KingPosition
+ : tempBoard.Player2KingPosition;
+ BoardState.WhoseTurn = otherPlayer;
+ }
- // Horizontal line
- builder.Append(" - ");
- for (var x = 0; x < 8; x++) builder.Append("- - ");
- builder.Append("- -");
- builder.AppendLine();
- builder.Append(" ");
- builder.Append("Player 1");
+ ///
+ /// 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("- -");
- builder.AppendLine();
- builder.AppendLine();
- // Print File ruler.
- builder.Append(" ");
- builder.Append(" A B C D E F G H I ");
+ // Print Rank ruler.
+ builder.AppendLine();
+ builder.Append($"{rank + 1} ");
- return builder.ToString();
- }
+ // 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();
+ }
- ///
- ///
- ///
- ///
- ///
- /// 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(' ');
+ // Horizontal line
+ builder.Append(" - ");
+ for (var x = 0; x < 8; x++) builder.Append("- - ");
+ builder.Append("- -");
+ builder.AppendLine();
+ builder.Append(" ");
+ builder.Append("Player 1");
- 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);
+ builder.AppendLine();
+ builder.AppendLine();
+ // Print File ruler.
+ builder.Append(" ");
+ builder.Append(" A B C D E F G H I ");
- if (piece.Owner == WhichPlayer.Player2) builder.Append('.');
- else builder.Append(' ');
+ return builder.ToString();
+ }
- 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.Domain/ValueObjects/StandardRules.cs b/Shogi.Domain/ValueObjects/StandardRules.cs
index 5cd8349..40712e3 100644
--- a/Shogi.Domain/ValueObjects/StandardRules.cs
+++ b/Shogi.Domain/ValueObjects/StandardRules.cs
@@ -12,105 +12,6 @@ namespace Shogi.Domain.ValueObjects
boardState = board;
}
- ///
- /// Move a piece from a board tile to another board tile ignorant of check or check-mate.
- ///
- /// The position of the piece being moved expressed in board notation.
- /// The target position expressed in board notation.
- /// True if a promotion is expected as a result of this move.
- /// A describing the success or failure of the move.
- internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false)
- {
- var from = Notation.FromBoardNotation(fromNotation);
- var to = Notation.FromBoardNotation(toNotation);
- var fromPiece = boardState[from];
- if (fromPiece == null)
- {
- return new MoveResult(false, $"Tile [{fromNotation}] is empty. There is no piece to move.");
- }
-
- if (fromPiece.Owner != boardState.WhoseTurn)
- {
- return new MoveResult(false, "Not allowed to move the opponents piece");
- }
-
- var path = fromPiece.GetPathFromStartToEnd(from, to);
-
- if (boardState.IsPathBlocked(path))
- {
- return new MoveResult(false, "Another piece obstructs the desired move.");
- }
-
- if (boardState[to] != null)
- {
- boardState.Capture(to);
- }
-
- if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from)))
- {
- fromPiece.Promote();
- }
-
- boardState[to] = fromPiece;
- boardState[from] = null;
-
- boardState.PreviousMove = new Move(from, to);
- var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
- boardState.WhoseTurn = otherPlayer;
-
- return new MoveResult(true);
- }
-
- /// Move a piece from the hand to the board ignorant if check or check-mate.
- ///
- ///
- /// The target position expressed in board notation.
- /// A describing the success or failure of the simulation.
- internal MoveResult Move(WhichPiece pieceInHand, string toNotation)
- {
- var to = Notation.FromBoardNotation(toNotation);
- var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
- if (index == -1)
- {
- return new MoveResult(false, $"{pieceInHand} does not exist in the hand.");
- }
- if (boardState[to] != null)
- {
- return new MoveResult(false, "Illegal move - attempting to capture while playing a piece from the hand.");
- }
-
- switch (pieceInHand)
- {
- 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)
- {
- return new MoveResult(false, "Knight has no valid moves after placed.");
- }
- 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 && to.Y == 8
- || boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)
- {
- return new MoveResult(false, $"{pieceInHand} has no valid moves after placed.");
- }
- break;
- }
- }
-
- // Mutate the board.
- boardState[to] = boardState.ActivePlayerHand[index];
- boardState.ActivePlayerHand.RemoveAt(index);
- boardState.PreviousMove = new Move(pieceInHand, to);
- return new MoveResult(true);
- }
-
///
/// Determines if the last move put the player who moved in check.
///
@@ -238,14 +139,16 @@ namespace Shogi.Domain.ValueObjects
return true;
}
- private IList GetPossiblePositionsForKing(WhichPlayer whichPlayer)
+ private List GetPossiblePositionsForKing(WhichPlayer whichPlayer)
{
var kingPosition = whichPlayer == WhichPlayer.Player1
? boardState.Player1KingPosition
: boardState.Player2KingPosition;
- return King.KingPaths
- .Select(path => path.Direction + kingPosition)
+ var paths = boardState[kingPosition]!.MoveSet;
+ return paths
+ .Select(path => path.NormalizedDirection + kingPosition)
+ // Because the king could be on the edge of the board, where some of its paths do not make sense.
.Where(newPosition => newPosition.IsInsideBoardBoundary())
// Where tile at position is empty, meaning the king could move there.
.Where(newPosition => boardState[newPosition] == null)
diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs
index e9a7ff2..915f3af 100644
--- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs
+++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs
@@ -1,8 +1,13 @@
-namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing
+namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
+
+public enum Distance
{
- public enum Distance
- {
- OneStep,
- MultiStep
- }
+ ///
+ /// Signifies that a piece can move one tile/position per move.
+ ///
+ OneStep,
+ ///
+ /// Signifies that a piece can move multiple tiles/positions in a single move.
+ ///
+ MultiStep
}
\ No newline at end of file
diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs
index 3fba1a6..765c1f7 100644
--- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs
+++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs
@@ -3,10 +3,17 @@
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
[DebuggerDisplay("{Direction} - {Distance}")]
-public record Path(Vector2 Direction, Distance Distance = Distance.OneStep)
+public record Path
{
+ public Vector2 NormalizedDirection { get; }
+ public Distance Distance { get; }
- public Path Invert() => new(Vector2.Negate(Direction), Distance);
+ public Path(Vector2 direction, Distance distance = Distance.OneStep)
+ {
+ NormalizedDirection = Vector2.Normalize(direction);
+ this.Distance = distance;
+ }
+ public Path Invert() => new(Vector2.Negate(NormalizedDirection), Distance);
}
public static class PathExtensions
@@ -21,8 +28,8 @@ public static class PathExtensions
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);
+ var distance = Vector2.Distance(start + path.NormalizedDirection, end); //Normalizing the direction probably broke this.
+ var shortestDistance = Vector2.Distance(start + shortestPath.NormalizedDirection, end); // And this.
if (distance < shortestDistance)
{
shortestPath = path;