yep
This commit is contained in:
81
Shogi.Domain/Other/BoardRules.cs
Normal file
81
Shogi.Domain/Other/BoardRules.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Shogi.Domain/Other/IRulesLifecycle.cs
Normal file
15
Shogi.Domain/Other/IRulesLifecycle.cs
Normal 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);
|
||||||
|
}
|
||||||
14
Shogi.Domain/Other/MoveValidationContext.cs
Normal file
14
Shogi.Domain/Other/MoveValidationContext.cs
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
Shogi.Domain/Other/RulesLifecycleResult.cs
Normal file
5
Shogi.Domain/Other/RulesLifecycleResult.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Shogi.Domain.Other;
|
||||||
|
|
||||||
|
public record RulesLifecycleResult(bool IsError, string ResultMessage = "")
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
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
|
private static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(6)
|
||||||
{
|
{
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(6)
|
new Path(Direction.Forward),
|
||||||
{
|
new Path(Direction.ForwardLeft),
|
||||||
new Path(Direction.Forward),
|
new Path(Direction.ForwardRight),
|
||||||
new Path(Direction.ForwardLeft),
|
new Path(Direction.Left),
|
||||||
new Path(Direction.ForwardRight),
|
new Path(Direction.Right),
|
||||||
new Path(Direction.Left),
|
new Path(Direction.Backward)
|
||||||
new Path(Direction.Right),
|
});
|
||||||
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()
|
||||||
.AsReadOnly();
|
.AsReadOnly();
|
||||||
|
|
||||||
public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
|
public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||||
: base(WhichPiece.GoldGeneral, owner, isPromoted)
|
: base(WhichPiece.GoldGeneral, owner, isPromoted)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
|
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
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
|
private static readonly ReadOnlyCollection<Path> Player1Paths = new(
|
||||||
{
|
[
|
||||||
internal static readonly ReadOnlyCollection<Path> KingPaths = new(new List<Path>(8)
|
new Path(Direction.Forward),
|
||||||
{
|
new Path(Direction.Left),
|
||||||
new Path(Direction.Forward),
|
new Path(Direction.Right),
|
||||||
new Path(Direction.Left),
|
new Path(Direction.Backward),
|
||||||
new Path(Direction.Right),
|
new Path(Direction.ForwardLeft),
|
||||||
new Path(Direction.Backward),
|
new Path(Direction.ForwardRight),
|
||||||
new Path(Direction.ForwardLeft),
|
new Path(Direction.BackwardLeft),
|
||||||
new Path(Direction.ForwardRight),
|
new Path(Direction.BackwardRight)
|
||||||
new Path(Direction.BackwardLeft),
|
]);
|
||||||
new Path(Direction.BackwardRight)
|
|
||||||
});
|
private static readonly ReadOnlyCollection<Path> Player2Paths = Player1Paths.Select(p => p.Invert()).ToList().AsReadOnly();
|
||||||
|
|
||||||
|
public King(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.King, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
|
||||||
|
|
||||||
public King(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.King, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<Path> MoveSet => KingPaths;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,126 @@
|
|||||||
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)
|
||||||
{
|
{
|
||||||
return piece switch
|
return piece switch
|
||||||
{
|
{
|
||||||
WhichPiece.King => new King(owner, isPromoted),
|
WhichPiece.King => new King(owner, isPromoted),
|
||||||
WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
|
WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
|
||||||
WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
|
WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
|
||||||
WhichPiece.Bishop => new Bishop(owner, isPromoted),
|
WhichPiece.Bishop => new Bishop(owner, isPromoted),
|
||||||
WhichPiece.Rook => new Rook(owner, isPromoted),
|
WhichPiece.Rook => new Rook(owner, isPromoted),
|
||||||
WhichPiece.Knight => new Knight(owner, isPromoted),
|
WhichPiece.Knight => new Knight(owner, isPromoted),
|
||||||
WhichPiece.Lance => new Lance(owner, isPromoted),
|
WhichPiece.Lance => new Lance(owner, isPromoted),
|
||||||
WhichPiece.Pawn => new Pawn(owner, isPromoted),
|
WhichPiece.Pawn => new Pawn(owner, isPromoted),
|
||||||
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
public abstract IEnumerable<Path> MoveSet { get; }
|
public abstract IEnumerable<Path> MoveSet { get; }
|
||||||
public WhichPiece WhichPiece { get; }
|
public WhichPiece WhichPiece { get; }
|
||||||
public WhichPlayer Owner { get; private set; }
|
public WhichPlayer Owner { get; private set; }
|
||||||
public bool IsPromoted { get; private set; }
|
public bool IsPromoted { get; private set; }
|
||||||
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
|
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
|
||||||
|
|
||||||
protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
||||||
{
|
{
|
||||||
WhichPiece = piece;
|
WhichPiece = piece;
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
IsPromoted = isPromoted;
|
IsPromoted = isPromoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanPromote => !IsPromoted
|
public bool CanPromote => !IsPromoted
|
||||||
&& WhichPiece != WhichPiece.King
|
&& WhichPiece != WhichPiece.King
|
||||||
&& WhichPiece != WhichPiece.GoldGeneral;
|
&& WhichPiece != WhichPiece.GoldGeneral;
|
||||||
|
|
||||||
public void Promote() => IsPromoted = CanPromote;
|
public void Promote() => IsPromoted = CanPromote;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prep the piece for capture by changing ownership and demoting.
|
/// Prep the piece for capture by changing ownership and demoting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Capture(WhichPlayer newOwner)
|
public void Capture(WhichPlayer newOwner)
|
||||||
{
|
{
|
||||||
Owner = newOwner;
|
Owner = newOwner;
|
||||||
IsPromoted = false;
|
IsPromoted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end.
|
/// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end.
|
||||||
/// Useful if you need to iterate a move-set.
|
/// Useful if you need to iterate a move-set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="start"></param>
|
/// <param name="start"></param>
|
||||||
/// <param name="end"></param>
|
/// <param name="end"></param>
|
||||||
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
||||||
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
||||||
{
|
{
|
||||||
var steps = new List<Vector2>(10);
|
var steps = new List<Vector2>(10);
|
||||||
|
|
||||||
var path = MoveSet.GetNearestPath(start, end);
|
var path = MoveSet.GetNearestPath(start, end);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position == end)
|
if (position == end)
|
||||||
{
|
{
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
var paths = this.MoveSet;
|
||||||
/// <returns>A list of positions the piece could move to.</returns>
|
|
||||||
public IEnumerable<Vector2> GetPossiblePositions(Vector2 currentPosition)
|
var matchingPaths = paths.Where(p => p.NormalizedDirection == Vector2.Normalize(ctx.To - ctx.From));
|
||||||
{
|
if (!matchingPaths.Any())
|
||||||
throw new NotImplementedException();
|
{
|
||||||
}
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -10,234 +11,257 @@ namespace Shogi.Domain.ValueObjects;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ShogiBoard
|
public sealed class ShogiBoard
|
||||||
{
|
{
|
||||||
private readonly StandardRules rules;
|
private readonly StandardRules rules;
|
||||||
|
|
||||||
public ShogiBoard(BoardState initialState)
|
public ShogiBoard(BoardState initialState)
|
||||||
{
|
{
|
||||||
BoardState = initialState;
|
BoardState = initialState;
|
||||||
rules = new StandardRules(BoardState);
|
rules = new StandardRules(BoardState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BoardState BoardState { get; }
|
public BoardState BoardState { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The strategy involves simulating a move on a throw-away board state that can be used to
|
/// 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.
|
/// 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)
|
||||||
{
|
{
|
||||||
var simulationState = new BoardState(BoardState);
|
// Validate the move
|
||||||
var simulation = new StandardRules(simulationState);
|
var rulesState = new Piece?[9, 9];
|
||||||
var moveResult = simulation.Move(from, to, isPromotion);
|
for (int x = 0; x < 9; x++)
|
||||||
if (!moveResult.Success)
|
for (int y = 0; y < 9; y++)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
rulesState[x, y] = this.BoardState[x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already in check, assert the move that resulted in check no longer results in check.
|
var rules = new BoardRules<Piece>(rulesState);
|
||||||
if (BoardState.InCheck == BoardState.WhoseTurn
|
var validationResult = rules.ValidateMove(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to), isPromotion);
|
||||||
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
|
if (validationResult.IsError)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Unable to move because you are still in check.");
|
throw new InvalidOperationException(validationResult.ResultMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simulation.DidPlayerPutThemselfInCheck())
|
// Move is valid, but is it legal?
|
||||||
{
|
// Check for correct player's turn.
|
||||||
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
if (BoardState.WhoseTurn != BoardState[from]!.Owner)
|
||||||
}
|
{
|
||||||
|
throw new InvalidOperationException("Not allowed to move the opponent's pieces.");
|
||||||
|
}
|
||||||
|
|
||||||
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
|
// Simulate the move on a throw -away state and look for "check" and "check-mate".
|
||||||
? WhichPlayer.Player2
|
var simulationState = new BoardState(BoardState);
|
||||||
: WhichPlayer.Player1;
|
var moveResult = simulationState.Move(from, to, isPromotion);
|
||||||
_ = rules.Move(from, to, isPromotion);
|
if (!moveResult.Success)
|
||||||
if (rules.IsOpponentInCheckAfterMove())
|
{
|
||||||
{
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
BoardState.InCheck = otherPlayer;
|
}
|
||||||
if (rules.IsOpponentInCheckMate())
|
|
||||||
{
|
|
||||||
BoardState.IsCheckmate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BoardState.InCheck = null;
|
|
||||||
}
|
|
||||||
BoardState.WhoseTurn = otherPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Move(WhichPiece pieceInHand, string to)
|
var simulation = new StandardRules(simulationState);
|
||||||
{
|
// If already in check, assert the move that resulted in check no longer results in check.
|
||||||
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
if (BoardState.InCheck == BoardState.WhoseTurn
|
||||||
if (index == -1)
|
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
|
throw new InvalidOperationException("Unable to move because you are still in check.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BoardState[to] != null)
|
if (simulation.DidPlayerPutThemselfInCheck())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var toVector = Notation.FromBoardNotation(to);
|
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
|
||||||
switch (pieceInHand)
|
? WhichPlayer.Player2
|
||||||
{
|
: WhichPlayer.Player1;
|
||||||
case WhichPiece.Knight:
|
_ = BoardState.Move(from, to, isPromotion); // "Rules" should not be doing any data changes. "State" should do that.
|
||||||
{
|
if (rules.IsOpponentInCheckAfterMove())
|
||||||
// Knight cannot be placed onto the farthest two ranks from the hand.
|
{
|
||||||
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6
|
BoardState.InCheck = otherPlayer;
|
||||||
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)
|
if (rules.IsOpponentInCheckMate())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
|
BoardState.IsCheckmate = true;
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
else
|
||||||
case WhichPiece.Lance:
|
{
|
||||||
case WhichPiece.Pawn:
|
BoardState.InCheck = null;
|
||||||
{
|
}
|
||||||
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
BoardState.WhoseTurn = otherPlayer;
|
||||||
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8
|
}
|
||||||
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempBoard = new BoardState(BoardState);
|
public void Move(WhichPiece pieceInHand, string to)
|
||||||
var simulation = new StandardRules(tempBoard);
|
{
|
||||||
var moveResult = simulation.Move(pieceInHand, to);
|
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
||||||
if (!moveResult.Success)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
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[to] != null)
|
||||||
if (BoardState.InCheck == BoardState.WhoseTurn
|
{
|
||||||
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
|
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
||||||
{
|
}
|
||||||
throw new InvalidOperationException("Unable to drop piece becauase you are still in check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simulation.DidPlayerPutThemselfInCheck())
|
var toVector = Notation.FromBoardNotation(to);
|
||||||
{
|
switch (pieceInHand)
|
||||||
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
{
|
||||||
}
|
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 tempBoard = new BoardState(BoardState);
|
||||||
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1
|
var simulation = new StandardRules(tempBoard);
|
||||||
? WhichPlayer.Player2
|
var moveResult = simulation.Move(pieceInHand, to);
|
||||||
: WhichPlayer.Player1;
|
if (!moveResult.Success)
|
||||||
_ = rules.Move(pieceInHand, to);
|
{
|
||||||
if (rules.IsOpponentInCheckAfterMove())
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
{
|
}
|
||||||
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 kingPosition = otherPlayer == WhichPlayer.Player1
|
// If already in check, assert the move that resulted in check no longer results in check.
|
||||||
? tempBoard.Player1KingPosition
|
if (BoardState.InCheck == BoardState.WhoseTurn
|
||||||
: tempBoard.Player2KingPosition;
|
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
|
||||||
BoardState.WhoseTurn = otherPlayer;
|
{
|
||||||
}
|
throw new InvalidOperationException("Unable to drop piece becauase you are still in check.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (simulation.DidPlayerPutThemselfInCheck())
|
||||||
/// Prints a ASCII representation of the board for debugging board state.
|
{
|
||||||
/// </summary>
|
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
||||||
/// <returns></returns>
|
}
|
||||||
public string ToStringStateAsAscii()
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append("Player 2");
|
|
||||||
builder.AppendLine();
|
|
||||||
for (var rank = 8; rank >= 0; rank--)
|
|
||||||
{
|
|
||||||
// Horizontal line
|
|
||||||
builder.Append(" - ");
|
|
||||||
for (var file = 0; file < 8; file++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
|
|
||||||
// Print Rank ruler.
|
// Update the non-simulation board.
|
||||||
builder.AppendLine();
|
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1
|
||||||
builder.Append($"{rank + 1} ");
|
? 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.
|
var kingPosition = otherPlayer == WhichPlayer.Player1
|
||||||
builder.Append(" |");
|
? tempBoard.Player1KingPosition
|
||||||
for (var x = 0; x < 9; x++)
|
: tempBoard.Player2KingPosition;
|
||||||
{
|
BoardState.WhoseTurn = otherPlayer;
|
||||||
var piece = BoardState[x, rank];
|
}
|
||||||
if (piece == null)
|
|
||||||
{
|
|
||||||
builder.Append(" ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.AppendFormat("{0}", ToAscii(piece));
|
|
||||||
}
|
|
||||||
builder.Append('|');
|
|
||||||
}
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal line
|
/// <summary>
|
||||||
builder.Append(" - ");
|
/// Prints a ASCII representation of the board for debugging board state.
|
||||||
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
/// </summary>
|
||||||
builder.Append("- -");
|
/// <returns></returns>
|
||||||
builder.AppendLine();
|
public string ToStringStateAsAscii()
|
||||||
builder.Append(" ");
|
{
|
||||||
builder.Append("Player 1");
|
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();
|
// Print Rank ruler.
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
// Print File ruler.
|
builder.Append($"{rank + 1} ");
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append(" A B C D E F G H I ");
|
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Horizontal line
|
||||||
///
|
builder.Append(" - ");
|
||||||
/// </summary>
|
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
||||||
/// <param name="piece"></param>
|
builder.Append("- -");
|
||||||
/// <returns>
|
builder.AppendLine();
|
||||||
/// A string with three characters.
|
builder.Append(" ");
|
||||||
/// The first character indicates promotion status.
|
builder.Append("Player 1");
|
||||||
/// The second character indicates piece.
|
|
||||||
/// The third character indicates ownership.
|
|
||||||
/// </returns>
|
|
||||||
private static string ToAscii(Piece piece)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
if (piece.IsPromoted) builder.Append('^');
|
|
||||||
else builder.Append(' ');
|
|
||||||
|
|
||||||
var name = piece.WhichPiece switch
|
builder.AppendLine();
|
||||||
{
|
builder.AppendLine();
|
||||||
WhichPiece.King => "K",
|
// Print File ruler.
|
||||||
WhichPiece.GoldGeneral => "G",
|
builder.Append(" ");
|
||||||
WhichPiece.SilverGeneral => "S",
|
builder.Append(" A B C D E F G H I ");
|
||||||
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('.');
|
return builder.ToString();
|
||||||
else builder.Append(' ');
|
}
|
||||||
|
|
||||||
return builder.ToString();
|
/// <summary>
|
||||||
}
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="piece"></param>
|
||||||
|
/// <returns>
|
||||||
|
/// A string with three characters.
|
||||||
|
/// The first character indicates promotion status.
|
||||||
|
/// The second character indicates piece.
|
||||||
|
/// The third character indicates ownership.
|
||||||
|
/// </returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
OneStep,
|
/// </summary>
|
||||||
MultiStep
|
OneStep,
|
||||||
}
|
/// <summary>
|
||||||
|
/// Signifies that a piece can move multiple tiles/positions in a single move.
|
||||||
|
/// </summary>
|
||||||
|
MultiStep
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user