This commit is contained in:
2024-10-11 11:10:38 -05:00
parent 81dd267290
commit f75553a0ad
14 changed files with 616 additions and 514 deletions

View File

@@ -0,0 +1,81 @@
namespace Shogi.Domain.Other;
/// <summary>
/// </summary>
/// <typeparam name="TPiece"></typeparam>
/// <param name="boardState">A 2D array of pieces, representing your board. Indexed as [x, y].</param>
public class BoardRules<TPiece>(TPiece?[,] boardState) where TPiece : IRulesLifecycle<TPiece>
{
private readonly Vector2 MaxIndex = new(boardState.GetLength(0) - 1, boardState.GetLength(1) - 1);
/// <summary>
/// Validates a move, invoking the <see cref="IRulesLifecycle{TPiece}.OnMoveValidation(MoveValidationContext{TPiece})"/> 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.
/// </summary>
/// <param name="from">The position of the piece being moved.</param>
/// <param name="to">The desired destination of the piece being moved.</param>
/// <param name="isPromotion">TODO</param>
/// <returns></returns>
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<TPiece>(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<TPiece>(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;
}
}

View File

@@ -0,0 +1,15 @@
namespace Shogi.Domain.Other;
public interface IRulesLifecycle<TPiece> where TPiece : IRulesLifecycle<TPiece>
{
/// <summary>
/// Invoked by <see cref="BoardRules{TPiece}.ValidateMove(Vector2, Vector2, bool)"/> 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.
///
/// </summary>
/// <param name="context">A context object with information for you to use to assess whether a move is valid for your implementing piece.</param>
/// <returns>A new <see cref="RulesLifecycleResult"/> object indicating whether or not the move is valid.</returns>
RulesLifecycleResult OnMoveValidation(MoveValidationContext<TPiece> context);
}

View File

@@ -0,0 +1,14 @@
namespace Shogi.Domain.Other;
public record MoveValidationContext<TPiece>(
Vector2 From,
Vector2 To,
bool IsPromotion,
TPiece?[,] BoardState) where TPiece : IRulesLifecycle<TPiece>
{
public TPiece? GetPieceByRelativePosition(Vector2 relativePosition)
{
var absolute = From + relativePosition;
return BoardState[(int)absolute.X, (int)absolute.Y];
}
}

View File

@@ -1,11 +0,0 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
namespace Shogi.Domain.Other;
public record PieceRulesRegistration<TPiece>(TPiece WhichPiece, ICollection<Path> MoveSet) where TPiece : Enum
{
}
public record PieceInPlay<TPiece>(TPiece WhichPiece, int OwningPlayerNumber) where TPiece : Enum
{
}

View File

@@ -1,61 +0,0 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Drawing;
namespace Shogi.Domain.Other;
public class Rules<TPiece> where TPiece : Enum
{
private Vector2 boardSize;
private TPiece theKing;
private Dictionary<TPiece, ICollection<Path>> piecePaths;
/// <summary>
/// Begin a new set of rules. If any rules already exist, this method will erase them.
/// </summary>
/// <param name="boardSize">The size of the game board in tiles. For examples, Chess is 8x8 and Shogi is 9x9.</param>
/// <param name="king">The piece that represents the King for each player or the piece that, when lost, results in losing the game.</param>
public Rules<TPiece> CreateNewRules(Vector2 boardSize, TPiece king)
{
this.boardSize = boardSize;
theKing = king;
piecePaths = [];
return this;
}
public Rules<TPiece> RegisterPieceWithRules(PieceRulesRegistration<TPiece> 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));
}
}
}

View File

@@ -0,0 +1,5 @@
namespace Shogi.Domain.Other;
public record RulesLifecycleResult(bool IsError, string ResultMessage = "")
{
}

View File

@@ -95,6 +95,92 @@ public class BoardState
set => this[Notation.ToBoardNotation(x, y)] = value; set => this[Notation.ToBoardNotation(x, y)] = value;
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="fromNotation">The position of the piece being moved expressed in board notation.</param>
/// <param name="toNotation">The target position expressed in board notation.</param>
/// <param name="isPromotionRequested">True if a promotion is expected as a result of this move.</param>
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
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.
/// </summary>
/// <param name="pieceInHand"></param>
/// <param name="to">The target position expressed in board notation.</param>
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
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);
}
/// <summary> /// <summary>
/// Returns true if the given path can be traversed without colliding into a piece. /// Returns true if the given path can be traversed without colliding into a piece.
/// </summary> /// </summary>
@@ -107,8 +193,6 @@ public class BoardState
internal bool IsWithinPromotionZone(Vector2 position) internal bool IsWithinPromotionZone(Vector2 position)
{ {
// TODO: Move this promotion zone logic into the StandardRules class. // TODO: Move this promotion zone logic into the StandardRules class.
return WhoseTurn == WhichPlayer.Player1 && position.Y > 5 return WhoseTurn == WhichPlayer.Player1 && position.Y > 5
|| WhoseTurn == WhichPlayer.Player2 && position.Y < 3; || WhoseTurn == WhichPlayer.Player2 && position.Y < 3;
@@ -127,9 +211,8 @@ public class BoardState
internal void Capture(Vector2 to) internal void Capture(Vector2 to)
{ {
var piece = this[to]; var piece = this[to]
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); ?? throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
piece.Capture(WhoseTurn); piece.Capture(WhoseTurn);
ActivePlayerHand.Add(piece); ActivePlayerHand.Add(piece);
} }

View File

@@ -1,11 +1,11 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Collections.ObjectModel; 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<Path> Player1Paths = new(new List<Path>(6) private static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(6)
{ {
new Path(Direction.Forward), new Path(Direction.Forward),
new Path(Direction.ForwardLeft), new Path(Direction.ForwardLeft),
@@ -15,7 +15,7 @@ namespace Shogi.Domain.ValueObjects
new Path(Direction.Backward) new Path(Direction.Backward)
}); });
public static readonly ReadOnlyCollection<Path> Player2Paths = private static readonly ReadOnlyCollection<Path> Player2Paths =
Player1Paths Player1Paths
.Select(p => p.Invert()) .Select(p => p.Invert())
.ToList() .ToList()
@@ -28,4 +28,3 @@ namespace Shogi.Domain.ValueObjects
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths; public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
} }
}

View File

@@ -1,12 +1,12 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Collections.ObjectModel; 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<Path> KingPaths = new(new List<Path>(8) private static readonly ReadOnlyCollection<Path> Player1Paths = new(
{ [
new Path(Direction.Forward), new Path(Direction.Forward),
new Path(Direction.Left), new Path(Direction.Left),
new Path(Direction.Right), new Path(Direction.Right),
@@ -15,13 +15,16 @@ namespace Shogi.Domain.ValueObjects
new Path(Direction.ForwardRight), new Path(Direction.ForwardRight),
new Path(Direction.BackwardLeft), new Path(Direction.BackwardLeft),
new Path(Direction.BackwardRight) new Path(Direction.BackwardRight)
}); ]);
private static readonly ReadOnlyCollection<Path> Player2Paths = Player1Paths.Select(p => p.Invert()).ToList().AsReadOnly();
public King(WhichPlayer owner, bool isPromoted = false) public King(WhichPlayer owner, bool isPromoted = false)
: base(WhichPiece.King, owner, isPromoted) : base(WhichPiece.King, owner, isPromoted)
{ {
} }
public override IEnumerable<Path> MoveSet => KingPaths; public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
}
} }

View File

@@ -1,10 +1,11 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using Shogi.Domain.Other;
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Diagnostics; using System.Diagnostics;
namespace Shogi.Domain.ValueObjects namespace Shogi.Domain.ValueObjects
{ {
[DebuggerDisplay("{WhichPiece} {Owner}")] [DebuggerDisplay("{WhichPiece} {Owner}")]
public abstract record class Piece public abstract record class Piece : IRulesLifecycle<Piece>
{ {
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
{ {
@@ -64,7 +65,7 @@ namespace Shogi.Domain.ValueObjects
var position = start; var position = start;
while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
{ {
position += path.Direction; position += path.NormalizedDirection;
steps.Add(position); steps.Add(position);
if (path.Distance == Distance.OneStep) break; if (path.Distance == Distance.OneStep) break;
@@ -78,14 +79,48 @@ namespace Shogi.Domain.ValueObjects
return Array.Empty<Vector2>(); return Array.Empty<Vector2>();
} }
/// <summary> #region IRulesLifecycle
/// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece. public RulesLifecycleResult OnMoveValidation(MoveValidationContext<Piece> ctx)
/// </summary>
/// <param name="currentPosition"></param>
/// <returns>A list of positions the piece could move to.</returns>
public IEnumerable<Vector2> GetPossiblePositions(Vector2 currentPosition)
{ {
throw new NotImplementedException(); 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<Vector2> 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
}
}

View File

@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using Shogi.Domain.Other;
using Shogi.Domain.YetToBeAssimilatedIntoDDD; using Shogi.Domain.YetToBeAssimilatedIntoDDD;
namespace Shogi.Domain.ValueObjects; namespace Shogi.Domain.ValueObjects;
@@ -28,16 +29,39 @@ public sealed class ShogiBoard
/// validate legal vs illegal moves without having to worry about reverting board state. /// validate legal vs illegal moves without having to worry about reverting board state.
/// </remarks> /// </remarks>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public void Move(string from, string to, bool isPromotion) 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];
}
var rules = new BoardRules<Piece>(rulesState);
var validationResult = rules.ValidateMove(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to), isPromotion);
if (validationResult.IsError)
{
throw new InvalidOperationException(validationResult.ResultMessage);
}
// 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.");
}
// Simulate the move on a throw -away state and look for "check" and "check-mate".
var simulationState = new BoardState(BoardState); var simulationState = new BoardState(BoardState);
var simulation = new StandardRules(simulationState); var moveResult = simulationState.Move(from, to, isPromotion);
var moveResult = simulation.Move(from, to, isPromotion);
if (!moveResult.Success) if (!moveResult.Success)
{ {
throw new InvalidOperationException(moveResult.Reason); throw new InvalidOperationException(moveResult.Reason);
} }
var simulation = new StandardRules(simulationState);
// If already in check, assert the move that resulted in check no longer results in check. // If already in check, assert the move that resulted in check no longer results in check.
if (BoardState.InCheck == BoardState.WhoseTurn if (BoardState.InCheck == BoardState.WhoseTurn
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
@@ -53,7 +77,7 @@ public sealed class ShogiBoard
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
? WhichPlayer.Player2 ? WhichPlayer.Player2
: WhichPlayer.Player1; : WhichPlayer.Player1;
_ = rules.Move(from, to, isPromotion); _ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that.
if (rules.IsOpponentInCheckAfterMove()) if (rules.IsOpponentInCheckAfterMove())
{ {
BoardState.InCheck = otherPlayer; BoardState.InCheck = otherPlayer;

View File

@@ -12,105 +12,6 @@ namespace Shogi.Domain.ValueObjects
boardState = board; boardState = board;
} }
/// <summary>
/// Move a piece from a board tile to another board tile ignorant of check or check-mate.
/// </summary>
/// <param name="fromNotation">The position of the piece being moved expressed in board notation.</param>
/// <param name="toNotation">The target position expressed in board notation.</param>
/// <param name="isPromotionRequested">True if a promotion is expected as a result of this move.</param>
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
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.
/// </summary>
/// <param name="pieceInHand"></param>
/// <param name="to">The target position expressed in board notation.</param>
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
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);
}
/// <summary> /// <summary>
/// Determines if the last move put the player who moved in check. /// Determines if the last move put the player who moved in check.
/// </summary> /// </summary>
@@ -238,14 +139,16 @@ namespace Shogi.Domain.ValueObjects
return true; return true;
} }
private IList<Vector2> GetPossiblePositionsForKing(WhichPlayer whichPlayer) private List<Vector2> GetPossiblePositionsForKing(WhichPlayer whichPlayer)
{ {
var kingPosition = whichPlayer == WhichPlayer.Player1 var kingPosition = whichPlayer == WhichPlayer.Player1
? boardState.Player1KingPosition ? boardState.Player1KingPosition
: boardState.Player2KingPosition; : boardState.Player2KingPosition;
return King.KingPaths var paths = boardState[kingPosition]!.MoveSet;
.Select(path => path.Direction + kingPosition) 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(newPosition => newPosition.IsInsideBoardBoundary())
// Where tile at position is empty, meaning the king could move there. // Where tile at position is empty, meaning the king could move there.
.Where(newPosition => boardState[newPosition] == null) .Where(newPosition => boardState[newPosition] == null)

View File

@@ -1,8 +1,13 @@
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
{
public enum Distance public enum Distance
{ {
/// <summary>
/// Signifies that a piece can move one tile/position per move.
/// </summary>
OneStep, OneStep,
/// <summary>
/// Signifies that a piece can move multiple tiles/positions in a single move.
/// </summary>
MultiStep MultiStep
} }
}

View File

@@ -3,10 +3,17 @@
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
[DebuggerDisplay("{Direction} - {Distance}")] [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 public static class PathExtensions
@@ -21,8 +28,8 @@ public static class PathExtensions
var shortestPath = paths.First(); var shortestPath = paths.First();
foreach (var path in paths.Skip(1)) foreach (var path in paths.Skip(1))
{ {
var distance = Vector2.Distance(start + path.Direction, end); var distance = Vector2.Distance(start + path.NormalizedDirection, end); //Normalizing the direction probably broke this.
var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); var shortestDistance = Vector2.Distance(start + shortestPath.NormalizedDirection, end); // And this.
if (distance < shortestDistance) if (distance < shortestDistance)
{ {
shortestPath = path; shortestPath = path;