This commit is contained in:
2024-10-20 22:27:08 -05:00
parent f75553a0ad
commit 3593785421
27 changed files with 1020 additions and 1081 deletions

View File

@@ -10,6 +10,8 @@
/** /**
* Local setup instructions, in order: * Local setup instructions, in order:
* 1. To setup the Shogi database, use the publish menu option in visual studio with the Shogi.Database project. * 1. To setup the Shogi database, use the publish menu option in visual studio with the Shogi.Database project.
* 2. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef *
* 2. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update * 2. Setup Entity Framework because that's what the login system uses.
* 2.a. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef
* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update
*/ */

View File

@@ -1,81 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,14 +0,0 @@
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,5 +0,0 @@
namespace Shogi.Domain.Other;
public record RulesLifecycleResult(bool IsError, string ResultMessage = "")
{
}

View File

@@ -39,7 +39,7 @@ public class BoardState
} }
/// <summary> /// <summary>
/// Copy constructor. /// Copy constructor. Creates a deep copy.
/// </summary> /// </summary>
public BoardState(BoardState other) public BoardState(BoardState other)
{ {
@@ -115,6 +115,7 @@ public class BoardState
var fromPiece = this[fromNotation] var fromPiece = this[fromNotation]
?? throw new InvalidOperationException($"No piece exists at position {fromNotation}."); ?? throw new InvalidOperationException($"No piece exists at position {fromNotation}.");
if (isPromotionRequested && if (isPromotionRequested &&
(IsWithinPromotionZone(to) || IsWithinPromotionZone(from))) (IsWithinPromotionZone(to) || IsWithinPromotionZone(from)))
{ {

View File

@@ -5,17 +5,17 @@ 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 Path(Direction.Forward), new Path(Direction.Forward),
new Path(Direction.ForwardLeft), new Path(Direction.ForwardLeft),
new Path(Direction.ForwardRight), new Path(Direction.ForwardRight),
new Path(Direction.Left), new Path(Direction.Left),
new Path(Direction.Right), new Path(Direction.Right),
new Path(Direction.Backward) new Path(Direction.Backward)
}); ]);
private static readonly ReadOnlyCollection<Path> Player2Paths = public static readonly ReadOnlyCollection<Path> Player2Paths =
Player1Paths Player1Paths
.Select(p => p.Invert()) .Select(p => p.Invert())
.ToList() .ToList()

View File

@@ -0,0 +1,9 @@
namespace Shogi.Domain.ValueObjects;
[Flags]
internal enum InCheckResult
{
NobodyInCheck = 1, // This kinda doesn't make sense from a Flags perspective, but it works. =/
Player1InCheck = 2,
Player2InCheck = 4
}

View File

@@ -1,14 +1,6 @@
namespace Shogi.Domain.ValueObjects namespace Shogi.Domain.ValueObjects
{ {
public class MoveResult public record MoveResult(bool IsSuccess, string Reason = "")
{ {
public bool Success { get; } }
public string Reason { get; }
public MoveResult(bool isSuccess, string reason = "")
{
Success = isSuccess;
Reason = reason;
}
}
} }

View File

@@ -1,11 +1,10 @@
using Shogi.Domain.Other; using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
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 : IRulesLifecycle<Piece> public abstract record class Piece
{ {
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
{ {
@@ -65,7 +64,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.NormalizedDirection; position += path.Step;
steps.Add(position); steps.Add(position);
if (path.Distance == Distance.OneStep) break; if (path.Distance == Distance.OneStep) break;
@@ -76,51 +75,7 @@ namespace Shogi.Domain.ValueObjects
return steps; return steps;
} }
return Array.Empty<Vector2>(); return [];
} }
#region IRulesLifecycle
public RulesLifecycleResult OnMoveValidation(MoveValidationContext<Piece> 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<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,7 +1,4 @@
using System.Text; using Shogi.Domain.YetToBeAssimilatedIntoDDD;
using Shogi.Domain.Other;
using Shogi.Domain.YetToBeAssimilatedIntoDDD;
namespace Shogi.Domain.ValueObjects; namespace Shogi.Domain.ValueObjects;
/// <summary> /// <summary>
@@ -12,6 +9,7 @@ namespace Shogi.Domain.ValueObjects;
public sealed class ShogiBoard public sealed class ShogiBoard
{ {
private readonly StandardRules rules; private readonly StandardRules rules;
private static readonly Vector2 BoardSize = new Vector2(9, 9);
public ShogiBoard(BoardState initialState) public ShogiBoard(BoardState initialState)
{ {
@@ -29,239 +27,273 @@ 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 = false) public MoveResult Move(string from, string to, bool isPromotion = false)
{ {
// Validate the move // Validate the move
var rulesState = new Piece?[9, 9]; var moveResult = IsMoveValid(Notation.FromBoardNotation(from), Notation.FromBoardNotation(to));
for (int x = 0; x < 9; x++) if (!moveResult.IsSuccess)
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); return moveResult;
} }
// Move is valid, but is it legal? // Move is valid, but is it legal?
// Check for correct player's turn. // Check for correct player's turn.
if (BoardState.WhoseTurn != BoardState[from]!.Owner) if (BoardState.WhoseTurn != BoardState[from]!.Owner)
{ {
throw new InvalidOperationException("Not allowed to move the opponent's pieces."); return new MoveResult(false, "Not allowed to move the opponent's pieces.");
} }
// Simulate the move on a throw -away state and look for "check" and "check-mate". // Simulate the move on a throw-away state and look for "check" and "check-mate".
var simulationState = new BoardState(BoardState); var simState = new BoardState(BoardState);
var moveResult = simulationState.Move(from, to, isPromotion); moveResult = simState.Move(from, to, isPromotion);
if (!moveResult.Success) if (!moveResult.IsSuccess)
{ {
throw new InvalidOperationException(moveResult.Reason); return moveResult;
} }
var simulation = new StandardRules(simulationState); var kings = simState.State
// If already in check, assert the move that resulted in check no longer results in check. .Where(kvp => kvp.Value?.WhichPiece == WhichPiece.King)
if (BoardState.InCheck == BoardState.WhoseTurn .Cast<KeyValuePair<string, Piece>>()
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To)) .ToArray();
if (kings.Length != 2) throw new InvalidOperationException("Unexpected scenario: board does not have two kings in play.");
// Look for threats against the kings.
var inCheckResult = simState.State
.Where(kvp => kvp.Value != null)
.Cast<KeyValuePair<string, Piece>>()
.Aggregate(InCheckResult.NobodyInCheck, (inCheckResult, kvp) =>
{
var newInCheckResult = inCheckResult;
var threatPiece = kvp.Value;
var opposingKingPosition = Notation.FromBoardNotation(kings.Single(king => king.Value.Owner != threatPiece.Owner).Key);
var candidatePositions = threatPiece.GetPathFromStartToEnd(Notation.FromBoardNotation(kvp.Key), opposingKingPosition);
foreach (var position in candidatePositions)
{
// No piece at this position, so pathing is unobstructed. Continue pathing.
if (simState[position] == null) continue;
var pieceAtPosition = simState[position]!;
if (pieceAtPosition.WhichPiece == WhichPiece.King && pieceAtPosition.Owner != threatPiece.Owner)
{
newInCheckResult &= pieceAtPosition.Owner == WhichPlayer.Player1 ? InCheckResult.Player2InCheck : InCheckResult.Player1InCheck;
}
else
{
break;
}
}
return newInCheckResult;
});
var playerPutThemselfInCheck = BoardState.WhoseTurn == WhichPlayer.Player1
? inCheckResult.HasFlag(InCheckResult.Player1InCheck)
: inCheckResult.HasFlag(InCheckResult.Player2InCheck);
if (playerPutThemselfInCheck)
{ {
throw new InvalidOperationException("Unable to move because you are still in check."); return new MoveResult(false, "This move puts the moving player in check, which is illega.");
} }
if (simulation.DidPlayerPutThemselfInCheck()) // Move is legal; mutate the real state.
BoardState.Move(from, to, isPromotion);
var playerPutOpponentInCheck = BoardState.WhoseTurn == WhichPlayer.Player1
? inCheckResult.HasFlag(InCheckResult.Player2InCheck)
: inCheckResult.HasFlag(InCheckResult.Player1InCheck);
if (playerPutOpponentInCheck)
{ {
throw new InvalidOperationException("Illegal move. This move places you in check."); BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1
}
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1
? WhichPlayer.Player2 ? WhichPlayer.Player2
: WhichPlayer.Player1; : 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
{ // TODO: Look for check-mate.
BoardState.InCheck = null; return new MoveResult(true);
}
BoardState.WhoseTurn = otherPlayer;
//var simulation = new StandardRules(simState);
//// 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 (simulation.DidPlayerPutThemselfInCheck())
//{
// throw new InvalidOperationException("Illegal move. This move places you in check.");
//}
//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;
} }
public void Move(WhichPiece pieceInHand, string to) public void Move(WhichPiece pieceInHand, string to)
{ {
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); //var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
if (index == -1) //if (index == -1)
{ //{
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); // throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
} //}
if (BoardState[to] != null) //if (BoardState[to] != null)
{ //{
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); // throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
} //}
var toVector = Notation.FromBoardNotation(to); //var toVector = Notation.FromBoardNotation(to);
switch (pieceInHand) //switch (pieceInHand)
{ //{
case WhichPiece.Knight: // case WhichPiece.Knight:
{ // {
// Knight cannot be placed onto the farthest two ranks from the hand. // // Knight cannot be placed onto the farthest two ranks from the hand.
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6 // if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2) // || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)
{ // {
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); // throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
} // }
break; // break;
} // }
case WhichPiece.Lance: // case WhichPiece.Lance:
case WhichPiece.Pawn: // case WhichPiece.Pawn:
{ // {
// Lance and Pawn cannot be placed onto the farthest rank from the hand. // // Lance and Pawn cannot be placed onto the farthest rank from the hand.
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8 // if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0) // || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)
{ // {
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); // throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
} // }
break; // break;
} // }
} //}
var tempBoard = new BoardState(BoardState); //var tempBoard = new BoardState(BoardState);
var simulation = new StandardRules(tempBoard); //var simulation = new StandardRules(tempBoard);
var moveResult = simulation.Move(pieceInHand, to); //var moveResult = simulation.Move(pieceInHand, to);
if (!moveResult.Success) //if (!moveResult.Success)
{ //{
throw new InvalidOperationException(moveResult.Reason); // throw new InvalidOperationException(moveResult.Reason);
} //}
// 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))
{ //{
throw new InvalidOperationException("Unable to drop piece becauase you are still in check."); // throw new InvalidOperationException("Unable to drop piece becauase you are still in check.");
} //}
if (simulation.DidPlayerPutThemselfInCheck()) //if (simulation.DidPlayerPutThemselfInCheck())
{ //{
throw new InvalidOperationException("Illegal move. This move places you in check."); // throw new InvalidOperationException("Illegal move. This move places you in check.");
} //}
// Update the non-simulation board. //// Update the non-simulation board.
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 //var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1
? WhichPlayer.Player2 // ? WhichPlayer.Player2
: WhichPlayer.Player1; // : WhichPlayer.Player1;
_ = rules.Move(pieceInHand, to); //_ = rules.Move(pieceInHand, to);
if (rules.IsOpponentInCheckAfterMove()) //if (rules.IsOpponentInCheckAfterMove())
{ //{
BoardState.InCheck = otherPlayer; // BoardState.InCheck = otherPlayer;
// A pawn, placed from the hand, cannot be the cause of checkmate. // // A pawn, placed from the hand, cannot be the cause of checkmate.
if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) // if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn)
{ // {
BoardState.IsCheckmate = true; // BoardState.IsCheckmate = true;
} // }
} //}
var kingPosition = otherPlayer == WhichPlayer.Player1 //var kingPosition = otherPlayer == WhichPlayer.Player1
? tempBoard.Player1KingPosition // ? tempBoard.Player1KingPosition
: tempBoard.Player2KingPosition; // : tempBoard.Player2KingPosition;
BoardState.WhoseTurn = otherPlayer; //BoardState.WhoseTurn = otherPlayer;
} }
/// <summary> /// <summary>
/// Prints a ASCII representation of the board for debugging board state. /// 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 according to all Shogi rules.
/// It asserts that a proposed move is possible and worthy of further validation (check, check-mate, etc).
/// </summary> /// </summary>
/// <returns></returns> private MoveResult IsMoveValid(Vector2 from, Vector2 to)
public string ToStringStateAsAscii()
{ {
var builder = new StringBuilder(); if (IsWithinBounds(from) && IsWithinBounds(to))
builder.Append(" ");
builder.Append("Player 2");
builder.AppendLine();
for (var rank = 8; rank >= 0; rank--)
{ {
// Horizontal line if (BoardState[to]?.WhichPiece == WhichPiece.King)
builder.Append(" - ");
for (var file = 0; file < 8; file++) builder.Append("- - ");
builder.Append("- -");
// Print Rank ruler.
builder.AppendLine();
builder.Append($"{rank + 1} ");
// Print pieces.
builder.Append(" |");
for (var x = 0; x < 9; x++)
{ {
var piece = BoardState[x, rank]; return new MoveResult(false, "Kings may not be captured.");
if (piece == null)
{
builder.Append(" ");
}
else
{
builder.AppendFormat("{0}", ToAscii(piece));
}
builder.Append('|');
} }
builder.AppendLine();
var piece = BoardState[from];
if (piece == null)
{
return new MoveResult(false, $"There is no piece at position {from}.");
}
var matchingPaths = piece.MoveSet.Where(p => p.NormalizedStep == Vector2.Normalize(to - from));
if (!matchingPaths.Any())
{
return new MoveResult(false, "Piece cannot move like that.");
}
var multiStepPaths = matchingPaths.Where(path => path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep).ToArray();
foreach (var path in multiStepPaths)
{
// Assert that no pieces exist along the from -> to path.
var isPathObstructed = GetPositionsAlongPath(from, to, path)
.Any(pos => BoardState[pos] != null);
if (isPathObstructed)
{
return new MoveResult(false, "Piece cannot move through other pieces.");
}
}
var pieceAtTo = BoardState[to];
if (pieceAtTo?.Owner == piece.Owner)
{
return new MoveResult(false, "Cannot capture your own pieces.");
}
} }
return new MoveResult(true);
// Horizontal line
builder.Append(" - ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append(" ");
builder.Append("Player 1");
builder.AppendLine();
builder.AppendLine();
// Print File ruler.
builder.Append(" ");
builder.Append(" A B C D E F G H I ");
return builder.ToString();
} }
/// <summary> private static IEnumerable<Vector2> GetPositionsAlongPath(Vector2 from, Vector2 to, YetToBeAssimilatedIntoDDD.Pathing.Path path)
///
/// </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(); var next = from;
if (piece.IsPromoted) builder.Append('^'); while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9)
else builder.Append(' ');
var name = piece.WhichPiece switch
{ {
WhichPiece.King => "K", next += path.Step;
WhichPiece.GoldGeneral => "G", yield return next;
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('.'); private static bool IsWithinBounds(Vector2 position)
else builder.Append(' '); {
var isPositive = position - position == Vector2.Zero;
return builder.ToString(); return isPositive && position.X <= BoardSize.X && position.Y <= BoardSize.Y;
} }
} }

View File

@@ -147,7 +147,7 @@ namespace Shogi.Domain.ValueObjects
var paths = boardState[kingPosition]!.MoveSet; var paths = boardState[kingPosition]!.MoveSet;
return paths return paths
.Select(path => path.NormalizedDirection + kingPosition) .Select(path => path.Step + kingPosition)
// Because the king could be on the edge of the board, where some of its paths do not make sense. // 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.

View File

@@ -5,15 +5,15 @@ public enum WhichPiece
King, King,
GoldGeneral, GoldGeneral,
SilverGeneral, SilverGeneral,
PromotedSilverGeneral, //PromotedSilverGeneral,
Bishop, Bishop,
PromotedBishop, //PromotedBishop,
Rook, Rook,
PromotedRook, //PromotedRook,
Knight, Knight,
PromotedKnight, //PromotedKnight,
Lance, Lance,
PromotedLance, //PromotedLance,
Pawn Pawn,
PromotedPawn //PromotedPawn,
} }

View File

@@ -1,19 +1,32 @@
using System.Diagnostics; using System.Diagnostics;
using static Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing.Path;
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
[DebuggerDisplay("{Direction} - {Distance}")] [DebuggerDisplay("{Step} - {Distance}")]
public record Path public record Path
{ {
public Vector2 NormalizedDirection { get; } public Vector2 Step { get; }
public Vector2 NormalizedStep => Vector2.Normalize(Step);
public Distance Distance { get; } public Distance Distance { get; }
public Path(Vector2 direction, Distance distance = Distance.OneStep) /// <summary>
///
/// </summary>
/// <param name="step">The smallest distance that can occur during a move.</param>
/// <param name="distance"></param>
public Path(Vector2 step, Distance distance = Distance.OneStep)
{ {
NormalizedDirection = Vector2.Normalize(direction); Step = step;
this.Distance = distance; this.Distance = distance;
} }
public Path Invert() => new(Vector2.Negate(NormalizedDirection), Distance); public Path Invert() => new(Vector2.Negate(Step), Distance);
//public enum PathingResult
//{
// Obstructed,
// CompletedWithoutObstruction
//}
} }
public static class PathExtensions public static class PathExtensions
@@ -28,8 +41,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.NormalizedDirection, end); //Normalizing the direction probably broke this. var distance = Vector2.Distance(start + path.Step, end);
var shortestDistance = Vector2.Distance(start + shortestPath.NormalizedDirection, end); // And this. var shortestDistance = Vector2.Distance(start + shortestPath.Step, end);
if (distance < shortestDistance) if (distance < shortestDistance)
{ {
shortestPath = path; shortestPath = path;

View File

@@ -5,7 +5,6 @@
@implements IDisposable @implements IDisposable
@inject ShogiApi ShogiApi @inject ShogiApi ShogiApi
@inject PromotePrompt PromotePrompt
@inject GameHubNode hubNode @inject GameHubNode hubNode
@inject NavigationManager navigator @inject NavigationManager navigator

View File

@@ -1,6 +1,5 @@
@using Shogi.Contracts.Types; @using Shogi.Contracts.Types;
@using System.Text.Json; @using System.Text.Json;
@inject PromotePrompt PromotePrompt;
<article class="game-board"> <article class="game-board">
@if (IsSpectating) @if (IsSpectating)
@@ -53,16 +52,6 @@
<span>H</span> <span>H</span>
<span>I</span> <span>I</span>
</div> </div>
<!-- Promote prompt -->
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
<p>Do you wish to promote?</p>
<div>
<button type="button">Yes</button>
<button type="button">No</button>
<button type="button">Cancel</button>
</div>
</div>
</section> </section>
<!-- Side board --> <!-- Side board -->
@@ -121,13 +110,6 @@
</article> </article>
@code { @code {
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
/// <summary> /// <summary>

View File

@@ -110,20 +110,4 @@
grid-template-rows: 3rem; grid-template-rows: 3rem;
place-items: center start; place-items: center start;
padding: 0.5rem; padding: 0.5rem;
} }
.promote-prompt {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 2px solid #444;
padding: 1rem;
box-shadow: 1px 1px 1px #444;
text-align: center;
}
.promote-prompt[data-visible="true"] {
display: block;
}

View File

@@ -2,16 +2,31 @@
@using Shogi.Contracts.Types; @using Shogi.Contracts.Types;
@using System.Text.RegularExpressions; @using System.Text.RegularExpressions;
@using System.Net; @using System.Net;
@inject PromotePrompt PromotePrompt;
@inject ShogiApi ShogiApi; @inject ShogiApi ShogiApi;
<GameBoardPresentation Session="Session" <div style="position: relative;">
Perspective="Perspective" <GameBoardPresentation Session="Session"
OnClickHand="OnClickHand" Perspective="Perspective"
OnClickTile="OnClickTile" OnClickHand="OnClickHand"
SelectedPosition="@selectedBoardPosition" OnClickTile="OnClickTile"
SelectedPieceFromHand="@selectedPieceFromHand" SelectedPosition="@selectedBoardPosition"
IsMyTurn="IsMyTurn" /> SelectedPieceFromHand="@selectedPieceFromHand"
IsMyTurn="IsMyTurn" />
@if (showPromotePrompt)
{
<!-- Promote prompt -->
<!-- TODO: Add a background div which prevents mouse inputs to the board while this decision is being made. -->
<section class="promote-prompt">
<p>Do you wish to promote?</p>
<div>
<button type="button" @onclick="() => OnClickPromotionChoice(true)">Yes</button>
<button type="button" @onclick="() => OnClickPromotionChoice(false)">No</button>
<button type="button" @onclick="() => showPromotePrompt = false">Cancel</button>
</div>
</section>
}
</div>
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
@@ -21,6 +36,8 @@
private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective; private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective;
private string? selectedBoardPosition; private string? selectedBoardPosition;
private WhichPiece? selectedPieceFromHand; private WhichPiece? selectedPieceFromHand;
private bool showPromotePrompt;
private string? moveTo;
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
@@ -75,7 +92,7 @@
{ {
// Placing a piece from the hand to an empty space. // Placing a piece from the hand to an empty space.
var success = await ShogiApi.Move( var success = await ShogiApi.Move(
Session.SessionId.ToString(), Session.SessionId,
new MovePieceCommand(selectedPieceFromHand.Value, position)); new MovePieceCommand(selectedPieceFromHand.Value, position));
if (!success) if (!success)
{ {
@@ -88,18 +105,24 @@
if (selectedBoardPosition != null) if (selectedBoardPosition != null)
{ {
Console.WriteLine("pieceAtPosition is null? {0}", pieceAtPosition == null);
if (pieceAtPosition == null || pieceAtPosition?.Owner != Perspective) if (pieceAtPosition == null || pieceAtPosition?.Owner != Perspective)
{ {
// Moving to an empty space or capturing an opponent's piece. // Moving to an empty space or capturing an opponent's piece.
if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedBoardPosition)) if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedBoardPosition))
{ {
PromotePrompt.Show( Console.WriteLine("Prompt!");
Session.SessionId.ToString(), moveTo = position;
new MovePieceCommand(selectedBoardPosition, position, false)); showPromotePrompt = true;
} }
else else
{ {
var success = await ShogiApi.Move(Session.SessionId.ToString(), new MovePieceCommand(selectedBoardPosition, position, false));
Console.WriteLine("OnClick to move to {0}", position);
var success = await ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedBoardPosition, position, false));
Console.WriteLine("Success? {0}", success);
if (!success) if (!success)
{ {
selectedBoardPosition = null; selectedBoardPosition = null;
@@ -125,4 +148,14 @@
StateHasChanged(); StateHasChanged();
} }
private Task OnClickPromotionChoice(bool shouldPromote)
{
if (selectedBoardPosition == null && selectedPieceFromHand.HasValue && moveTo != null)
{
return ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedPieceFromHand.Value, moveTo));
}
throw new InvalidOperationException("Unexpected scenario during OnClickPromotionChoice.");
}
} }

View File

@@ -0,0 +1,14 @@
.promote-prompt {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 2px solid #444;
padding: 1rem;
box-shadow: 1px 1px 1px #444;
text-align: center;
z-index: 101;
background-color: #444;
border: 1px solid black;
}

View File

@@ -1,58 +0,0 @@
using Shogi.Contracts.Api;
using Shogi.UI.Shared;
namespace Shogi.UI.Pages.Play;
public class PromotePrompt
{
private readonly ShogiApi shogiApi;
private string? sessionName;
private MovePieceCommand? command;
public PromotePrompt(ShogiApi shogiApi)
{
this.shogiApi = shogiApi;
this.IsVisible = false;
this.OnClickCancel = this.Hide;
}
public bool IsVisible { get; private set; }
public Action OnClickCancel;
public Func<Task>? OnClickNo;
public Func<Task>? OnClickYes;
public void Show(string sessionName, MovePieceCommand command)
{
this.sessionName = sessionName;
this.command = command;
this.IsVisible = true;
this.OnClickNo = this.Move;
this.OnClickYes = this.MoveAndPromote;
}
public void Hide()
{
this.IsVisible = false;
this.OnClickNo = null;
this.OnClickYes = null;
}
private Task Move()
{
if (this.command != null && this.sessionName != null)
{
this.command.IsPromotion = false;
return this.shogiApi.Move(this.sessionName, this.command);
}
return Task.CompletedTask;
}
private Task MoveAndPromote()
{
if (this.command != null && this.sessionName != null)
{
this.command.IsPromotion = true;
return this.shogiApi.Move(this.sessionName, this.command);
}
return Task.CompletedTask;
}
}

View File

@@ -39,8 +39,7 @@ static void ConfigureDependencies(IServiceCollection services, IConfiguration co
services services
.AddTransient<CookieCredentialsMessageHandler>() .AddTransient<CookieCredentialsMessageHandler>()
.AddTransient<ILocalStorage, LocalStorage>() .AddTransient<ILocalStorage, LocalStorage>();
.AddSingleton<PromotePrompt>();
// Identity // Identity
services services

View File

@@ -52,7 +52,7 @@ public class ShogiApi(HttpClient httpClient)
/// <summary> /// <summary>
/// Returns false if the move was not accepted by the server. /// Returns false if the move was not accepted by the server.
/// </summary> /// </summary>
public async Task<bool> Move(string sessionName, MovePieceCommand command) public async Task<bool> Move(Guid sessionName, MovePieceCommand command)
{ {
var response = await httpClient.PatchAsync(Relative($"Sessions/{sessionName}/Move"), JsonContent.Create(command)); var response = await httpClient.PatchAsync(Relative($"Sessions/{sessionName}/Move"), JsonContent.Create(command));
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;

View File

@@ -0,0 +1,97 @@
using Shogi.Domain.ValueObjects;
using System;
using System.Text;
namespace UnitTests;
public static class Extensions
{
/// <summary>
/// Prints a ASCII representation of the board for debugging board state.
/// </summary>
/// <returns></returns>
public static string ToStringStateAsAscii(this ShogiBoard board)
{
var boardState = board.BoardState;
var builder = new StringBuilder();
builder.Append(" ");
builder.Append("Player 2");
builder.AppendLine();
for (var rank = 8; rank >= 0; rank--)
{
// Horizontal line
builder.Append(" - ");
for (var file = 0; file < 8; file++) builder.Append("- - ");
builder.Append("- -");
// Print Rank ruler.
builder.AppendLine();
builder.Append($"{rank + 1} ");
// Print pieces.
builder.Append(" |");
for (var x = 0; x < 9; x++)
{
var piece = boardState[x, rank];
if (piece == null)
{
builder.Append(" ");
}
else
{
builder.AppendFormat("{0}", ToAscii(piece));
}
builder.Append('|');
}
builder.AppendLine();
}
// Horizontal line
builder.Append(" - ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append(" ");
builder.Append("Player 1");
builder.AppendLine();
builder.AppendLine();
// Print File ruler.
builder.Append(" ");
builder.Append(" A B C D E F G H I ");
return builder.ToString();
}
/// <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();
}
}

View File

@@ -1,9 +1,9 @@
using System.Numerics; using System.Numerics;
using Shogi.Domain.YetToBeAssimilatedIntoDDD; using Shogi.Domain.YetToBeAssimilatedIntoDDD;
namespace Shogi.Domain.UnitTests namespace UnitTests
{ {
public class NotationShould public class NotationShould
{ {
[Fact] [Fact]
public void ConvertFromNotationToVector() public void ConvertFromNotationToVector()

View File

@@ -2,221 +2,221 @@
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
using System.Numerics; using System.Numerics;
namespace Shogi.Domain.UnitTests; namespace UnitTests;
public class RookShould public class RookShould
{ {
public class MoveSet public class MoveSet
{ {
private readonly Rook rook1; private readonly Rook rook1;
private readonly Rook rook2; private readonly Rook rook2;
public MoveSet() public MoveSet()
{ {
this.rook1 = new Rook(WhichPlayer.Player1); rook1 = new Rook(WhichPlayer.Player1);
this.rook2 = new Rook(WhichPlayer.Player2); rook2 = new Rook(WhichPlayer.Player2);
} }
[Fact] [Fact]
public void Player1_HasCorrectMoveSet() public void Player1_HasCorrectMoveSet()
{ {
var moveSet = rook1.MoveSet; var moveSet = rook1.MoveSet;
moveSet.Should().HaveCount(4); moveSet.Should().HaveCount(4);
moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep));
} }
[Fact] [Fact]
public void Player1_Promoted_HasCorrectMoveSet() public void Player1_Promoted_HasCorrectMoveSet()
{ {
// Arrange // Arrange
rook1.Promote(); rook1.Promote();
rook1.IsPromoted.Should().BeTrue(); rook1.IsPromoted.Should().BeTrue();
// Assert // Assert
var moveSet = rook1.MoveSet; var moveSet = rook1.MoveSet;
moveSet.Should().HaveCount(8); moveSet.Should().HaveCount(8);
moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep));
} }
[Fact] [Fact]
public void Player2_HasCorrectMoveSet() public void Player2_HasCorrectMoveSet()
{ {
var moveSet = rook2.MoveSet; var moveSet = rook2.MoveSet;
moveSet.Should().HaveCount(4); moveSet.Should().HaveCount(4);
moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep));
} }
[Fact] [Fact]
public void Player2_Promoted_HasCorrectMoveSet() public void Player2_Promoted_HasCorrectMoveSet()
{ {
// Arrange // Arrange
rook2.Promote(); rook2.Promote();
rook2.IsPromoted.Should().BeTrue(); rook2.IsPromoted.Should().BeTrue();
// Assert // Assert
var moveSet = rook2.MoveSet; var moveSet = rook2.MoveSet;
moveSet.Should().HaveCount(8); moveSet.Should().HaveCount(8);
moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep));
moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep));
} }
} }
private readonly Rook rookPlayer1; private readonly Rook rookPlayer1;
public RookShould() public RookShould()
{ {
this.rookPlayer1 = new Rook(WhichPlayer.Player1); rookPlayer1 = new Rook(WhichPlayer.Player1);
} }
[Fact] [Fact]
public void Promote() public void Promote()
{ {
this.rookPlayer1.IsPromoted.Should().BeFalse(); rookPlayer1.IsPromoted.Should().BeFalse();
this.rookPlayer1.CanPromote.Should().BeTrue(); rookPlayer1.CanPromote.Should().BeTrue();
this.rookPlayer1.Promote(); rookPlayer1.Promote();
this.rookPlayer1.IsPromoted.Should().BeTrue(); rookPlayer1.IsPromoted.Should().BeTrue();
this.rookPlayer1.CanPromote.Should().BeFalse(); rookPlayer1.CanPromote.Should().BeFalse();
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(0, 5); Vector2 end = new(0, 5);
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeFalse(); rookPlayer1.IsPromoted.Should().BeFalse();
steps.Should().HaveCount(5); steps.Should().HaveCount(5);
steps.Should().Contain(new Vector2(0, 1)); steps.Should().Contain(new Vector2(0, 1));
steps.Should().Contain(new Vector2(0, 2)); steps.Should().Contain(new Vector2(0, 2));
steps.Should().Contain(new Vector2(0, 3)); steps.Should().Contain(new Vector2(0, 3));
steps.Should().Contain(new Vector2(0, 4)); steps.Should().Contain(new Vector2(0, 4));
steps.Should().Contain(new Vector2(0, 5)); steps.Should().Contain(new Vector2(0, 5));
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(1, 1); Vector2 end = new(1, 1);
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeFalse(); rookPlayer1.IsPromoted.Should().BeFalse();
steps.Should().BeEmpty(); steps.Should().BeEmpty();
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() public void GetStepsFromStartToEnd_Player1Promoted_LateralMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(0, 5); Vector2 end = new(0, 5);
rookPlayer1.Promote(); rookPlayer1.Promote();
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeTrue(); rookPlayer1.IsPromoted.Should().BeTrue();
steps.Should().HaveCount(5); steps.Should().HaveCount(5);
steps.Should().Contain(new Vector2(0, 1)); steps.Should().Contain(new Vector2(0, 1));
steps.Should().Contain(new Vector2(0, 2)); steps.Should().Contain(new Vector2(0, 2));
steps.Should().Contain(new Vector2(0, 3)); steps.Should().Contain(new Vector2(0, 3));
steps.Should().Contain(new Vector2(0, 4)); steps.Should().Contain(new Vector2(0, 4));
steps.Should().Contain(new Vector2(0, 5)); steps.Should().Contain(new Vector2(0, 5));
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(1, 1); Vector2 end = new(1, 1);
rookPlayer1.Promote(); rookPlayer1.Promote();
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeTrue(); rookPlayer1.IsPromoted.Should().BeTrue();
steps.Should().HaveCount(1); steps.Should().HaveCount(1);
steps.Should().Contain(new Vector2(1, 1)); steps.Should().Contain(new Vector2(1, 1));
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(0, 5); Vector2 end = new(0, 5);
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeFalse(); rookPlayer1.IsPromoted.Should().BeFalse();
steps.Should().HaveCount(5); steps.Should().HaveCount(5);
steps.Should().Contain(new Vector2(0, 1)); steps.Should().Contain(new Vector2(0, 1));
steps.Should().Contain(new Vector2(0, 2)); steps.Should().Contain(new Vector2(0, 2));
steps.Should().Contain(new Vector2(0, 3)); steps.Should().Contain(new Vector2(0, 3));
steps.Should().Contain(new Vector2(0, 4)); steps.Should().Contain(new Vector2(0, 4));
steps.Should().Contain(new Vector2(0, 5)); steps.Should().Contain(new Vector2(0, 5));
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(1, 1); Vector2 end = new(1, 1);
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeFalse(); rookPlayer1.IsPromoted.Should().BeFalse();
steps.Should().BeEmpty(); steps.Should().BeEmpty();
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() public void GetStepsFromStartToEnd_Player2Promoted_LateralMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(0, 5); Vector2 end = new(0, 5);
rookPlayer1.Promote(); rookPlayer1.Promote();
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeTrue(); rookPlayer1.IsPromoted.Should().BeTrue();
steps.Should().HaveCount(5); steps.Should().HaveCount(5);
steps.Should().Contain(new Vector2(0, 1)); steps.Should().Contain(new Vector2(0, 1));
steps.Should().Contain(new Vector2(0, 2)); steps.Should().Contain(new Vector2(0, 2));
steps.Should().Contain(new Vector2(0, 3)); steps.Should().Contain(new Vector2(0, 3));
steps.Should().Contain(new Vector2(0, 4)); steps.Should().Contain(new Vector2(0, 4));
steps.Should().Contain(new Vector2(0, 5)); steps.Should().Contain(new Vector2(0, 5));
} }
[Fact] [Fact]
public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove()
{ {
Vector2 start = new(0, 0); Vector2 start = new(0, 0);
Vector2 end = new(1, 1); Vector2 end = new(1, 1);
rookPlayer1.Promote(); rookPlayer1.Promote();
var steps = rookPlayer1.GetPathFromStartToEnd(start, end); var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
rookPlayer1.IsPromoted.Should().BeTrue(); rookPlayer1.IsPromoted.Should().BeTrue();
steps.Should().HaveCount(1); steps.Should().HaveCount(1);
steps.Should().Contain(new Vector2(1, 1)); steps.Should().Contain(new Vector2(1, 1));
} }
} }

View File

@@ -1,6 +1,6 @@
using Shogi.Domain.ValueObjects; using Shogi.Domain.ValueObjects;
namespace Shogi.Domain.UnitTests; namespace UnitTests;
public class ShogiBoardStateShould public class ShogiBoardStateShould
{ {

View File

@@ -2,453 +2,453 @@
using System; using System;
using System.Linq; using System.Linq;
namespace Shogi.Domain.UnitTests namespace UnitTests
{ {
public class ShogiShould public class ShogiShould
{ {
private readonly ITestOutputHelper console; private readonly ITestOutputHelper console;
public ShogiShould(ITestOutputHelper console) public ShogiShould(ITestOutputHelper console)
{ {
this.console = console; this.console = console;
} }
[Fact] [Fact]
public void MoveAPieceToAnEmptyPosition() public void MoveAPieceToAnEmptyPosition()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
board["A4"].Should().BeNull(); board["A4"].Should().BeNull();
var expectedPiece = board["A3"]; var expectedPiece = board["A3"];
expectedPiece.Should().NotBeNull(); expectedPiece.Should().NotBeNull();
// Act // Act
shogi.Move("A3", "A4", false); shogi.Move("A3", "A4", false);
// Assert // Assert
board["A3"].Should().BeNull(); board["A3"].Should().BeNull();
board["A4"].Should().Be(expectedPiece); board["A4"].Should().Be(expectedPiece);
} }
[Fact] [Fact]
public void AllowValidMoves_AfterCheck() public void AllowValidMoves_AfterCheck()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// P1 Bishop puts P2 in check // P1 Bishop puts P2 in check
shogi.Move("B2", "G7", false); shogi.Move("B2", "G7", false);
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
// Act - P2 is able to un-check theirself. // Act - P2 is able to un-check theirself.
/// P2 King moves out of check /// P2 King moves out of check
shogi.Move("E9", "E8", false); shogi.Move("E9", "E8", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
board.InCheck.Should().BeNull(); board.InCheck.Should().BeNull();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_MoveFromEmptyPosition() public void PreventInvalidMoves_MoveFromEmptyPosition()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
board["D5"].Should().BeNull(); board["D5"].Should().BeNull();
// Act // Act
var act = () => shogi.Move("D5", "D6", false); var act = () => shogi.Move("D5", "D6", false);
// Assert // Assert
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["D5"].Should().BeNull(); board["D5"].Should().BeNull();
board["D6"].Should().BeNull(); board["D6"].Should().BeNull();
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
} }
[Fact] [Fact]
public void PreventInvalidMoves_MoveToCurrentPosition() public void PreventInvalidMoves_MoveToCurrentPosition()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var expectedPiece = board["A3"]; var expectedPiece = board["A3"];
// Act - P1 "moves" pawn to the position it already exists at. // Act - P1 "moves" pawn to the position it already exists at.
var act = () => shogi.Move("A3", "A3", false); var act = () => shogi.Move("A3", "A3", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["A3"].Should().Be(expectedPiece); board["A3"].Should().Be(expectedPiece);
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_MoveSet() public void PreventInvalidMoves_MoveSet()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var expectedPiece = board["A1"]; var expectedPiece = board["A1"];
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
// Act - Move Lance illegally // Act - Move Lance illegally
var act = () => shogi.Move("A1", "D5", false); var act = () => shogi.Move("A1", "D5", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["A1"].Should().Be(expectedPiece); board["A1"].Should().Be(expectedPiece);
board["A5"].Should().BeNull(); board["A5"].Should().BeNull();
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_Ownership() public void PreventInvalidMoves_Ownership()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var expectedPiece = board["A7"]; var expectedPiece = board["A7"];
expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); expectedPiece!.Owner.Should().Be(WhichPlayer.Player2);
board.WhoseTurn.Should().Be(WhichPlayer.Player1); board.WhoseTurn.Should().Be(WhichPlayer.Player1);
// Act - Move Player2 Pawn when it is Player1 turn. // Act - Move Player2 Pawn when it is Player1 turn.
var act = () => shogi.Move("A7", "A6", false); var act = () => shogi.Move("A7", "A6", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["A7"].Should().Be(expectedPiece); board["A7"].Should().Be(expectedPiece);
board["A6"].Should().BeNull(); board["A6"].Should().BeNull();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_MoveThroughAllies() public void PreventInvalidMoves_MoveThroughAllies()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var lance = board["A1"]; var lance = board["A1"];
var pawn = board["A3"]; var pawn = board["A3"];
lance!.Owner.Should().Be(pawn!.Owner); lance!.Owner.Should().Be(pawn!.Owner);
// Act - Move P1 Lance through P1 Pawn. // Act - Move P1 Lance through P1 Pawn.
var act = () => shogi.Move("A1", "A5", false); var act = () => shogi.Move("A1", "A5", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["A1"].Should().Be(lance); board["A1"].Should().Be(lance);
board["A3"].Should().Be(pawn); board["A3"].Should().Be(pawn);
board["A5"].Should().BeNull(); board["A5"].Should().BeNull();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_CaptureAlly() public void PreventInvalidMoves_CaptureAlly()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var knight = board["B1"]; var knight = board["B1"];
var pawn = board["C3"]; var pawn = board["C3"];
knight!.Owner.Should().Be(pawn!.Owner); knight!.Owner.Should().Be(pawn!.Owner);
// Act - P1 Knight tries to capture P1 Pawn. // Act - P1 Knight tries to capture P1 Pawn.
var act = () => shogi.Move("B1", "C3", false); var act = () => shogi.Move("B1", "C3", false);
// Arrange // Arrange
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["B1"].Should().Be(knight); board["B1"].Should().Be(knight);
board["C3"].Should().Be(pawn); board["C3"].Should().Be(pawn);
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
} }
} }
[Fact] [Fact]
public void PreventInvalidMoves_Check() public void PreventInvalidMoves_Check()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// P1 Bishop puts P2 in check // P1 Bishop puts P2 in check
shogi.Move("B2", "G7", false); shogi.Move("B2", "G7", false);
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
var lance = board["I9"]; var lance = board["I9"];
// Act - P2 moves Lance while in check. // Act - P2 moves Lance while in check.
var act = () => shogi.Move("I9", "I8", false); var act = () => shogi.Move("I9", "I8", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
board["I9"].Should().Be(lance); board["I9"].Should().Be(lance);
board["I8"].Should().BeNull(); board["I8"].Should().BeNull();
} }
} }
[Fact] [Fact]
public void PreventInvalidDrops_MoveSet() public void PreventInvalidDrops_MoveSet()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("I7", "I6", false); shogi.Move("I7", "I6", false);
// P1 Bishop takes P2 Pawn. // P1 Bishop takes P2 Pawn.
shogi.Move("B2", "G7", false); shogi.Move("B2", "G7", false);
// P2 Gold, block check from P1 Bishop. // P2 Gold, block check from P1 Bishop.
shogi.Move("F9", "F8", false); shogi.Move("F9", "F8", false);
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
shogi.Move("G7", "H8", true); shogi.Move("G7", "H8", true);
// P2 Pawn again // P2 Pawn again
shogi.Move("I6", "I5", false); shogi.Move("I6", "I5", false);
// P1 Bishop takes P2 Knight // P1 Bishop takes P2 Knight
shogi.Move("H8", "H9", false); shogi.Move("H8", "H9", false);
// P2 Pawn again // P2 Pawn again
shogi.Move("I5", "I4", false); shogi.Move("I5", "I4", false);
// P1 Bishop takes P2 Lance // P1 Bishop takes P2 Lance
shogi.Move("H9", "I9", false); shogi.Move("H9", "I9", false);
// P2 Pawn captures P1 Pawn // P2 Pawn captures P1 Pawn
shogi.Move("I4", "I3", false); shogi.Move("I4", "I3", false);
board.Player1Hand.Count.Should().Be(4); board.Player1Hand.Count.Should().Be(4);
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
board.WhoseTurn.Should().Be(WhichPlayer.Player1); board.WhoseTurn.Should().Be(WhichPlayer.Player1);
// Act | Assert - Illegally placing Knight from the hand in farthest rank. // Act | Assert - Illegally placing Knight from the hand in farthest rank.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
var act = () => shogi.Move(WhichPiece.Knight, "H9"); var act = () => shogi.Move(WhichPiece.Knight, "H9");
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally placing Knight from the hand in second farthest row. // Act | Assert - Illegally placing Knight from the hand in second farthest row.
board["H8"].Should().BeNull(); board["H8"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Knight, "H8"); act = () => shogi.Move(WhichPiece.Knight, "H8");
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["H8"].Should().BeNull(); board["H8"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally place Lance from the hand. // Act | Assert - Illegally place Lance from the hand.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Knight, "H9"); act = () => shogi.Move(WhichPiece.Knight, "H9");
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act | Assert - Illegally place Pawn from the hand. // Act | Assert - Illegally place Pawn from the hand.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Pawn, "H9"); act = () => shogi.Move(WhichPiece.Pawn, "H9");
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
// // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn.
// // TODO // // TODO
} }
[Fact] [Fact]
public void PreventInvalidDrop_Check() public void PreventInvalidDrop_Check()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// P1 Pawn, arbitrary move. // P1 Pawn, arbitrary move.
shogi.Move("A3", "A4", false); shogi.Move("A3", "A4", false);
// P2 Bishop takes P1 Bishop // P2 Bishop takes P1 Bishop
shogi.Move("H8", "B2", false); shogi.Move("H8", "B2", false);
// P1 Silver takes P2 Bishop // P1 Silver takes P2 Bishop
shogi.Move("C1", "B2", false); shogi.Move("C1", "B2", false);
// P2 Pawn, arbtrary move // P2 Pawn, arbtrary move
shogi.Move("A7", "A6", false); shogi.Move("A7", "A6", false);
// P1 drop Bishop, place P2 in check // P1 drop Bishop, place P2 in check
shogi.Move(WhichPiece.Bishop, "G7"); shogi.Move(WhichPiece.Bishop, "G7");
shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2);
shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.BoardState["E5"].Should().BeNull(); shogi.BoardState["E5"].Should().BeNull();
// Act - P2 places a Bishop while in check. // Act - P2 places a Bishop while in check.
var act = () => shogi.Move(WhichPiece.Bishop, "E5"); var act = () => shogi.Move(WhichPiece.Bishop, "E5");
// Assert // Assert
using var scope = new AssertionScope(); using var scope = new AssertionScope();
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
shogi.BoardState["E5"].Should().BeNull(); shogi.BoardState["E5"].Should().BeNull();
shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2);
shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
} }
[Fact] [Fact]
public void PreventInvalidDrop_Capture() public void PreventInvalidDrop_Capture()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// P1 Bishop capture P2 Bishop // P1 Bishop capture P2 Bishop
shogi.Move("B2", "H8", false); shogi.Move("B2", "H8", false);
// P2 Pawn // P2 Pawn
shogi.Move("G6", "G5", false); shogi.Move("G6", "G5", false);
shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.BoardState["I9"].Should().NotBeNull(); shogi.BoardState["I9"].Should().NotBeNull();
shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance);
shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2);
// Act - P1 tries to place a piece where an opponent's piece resides. // Act - P1 tries to place a piece where an opponent's piece resides.
var act = () => shogi.Move(WhichPiece.Bishop, "I9"); var act = () => shogi.Move(WhichPiece.Bishop, "I9");
// Assert // Assert
using var scope = new AssertionScope(); using var scope = new AssertionScope();
act.Should().Throw<InvalidOperationException>(); act.Should().Throw<InvalidOperationException>();
shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.BoardState["I9"].Should().NotBeNull(); shogi.BoardState["I9"].Should().NotBeNull();
shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance);
shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2);
} }
[Fact] [Fact]
public void Check() public void Check()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// Act - P1 Bishop, check // Act - P1 Bishop, check
shogi.Move("B2", "G7", false); shogi.Move("B2", "G7", false);
// Assert // Assert
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
} }
[Fact] [Fact]
public void Promote() public void Promote()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Pawn // P1 Pawn
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
// P2 Pawn // P2 Pawn
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// Act - P1 moves across promote threshold. // Act - P1 moves across promote threshold.
shogi.Move("B2", "G7", true); shogi.Move("B2", "G7", true);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
board["B2"].Should().BeNull(); board["B2"].Should().BeNull();
board["G7"].Should().NotBeNull(); board["G7"].Should().NotBeNull();
board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop);
board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); board["G7"]!.Owner.Should().Be(WhichPlayer.Player1);
board["G7"]!.IsPromoted.Should().BeTrue(); board["G7"]!.IsPromoted.Should().BeTrue();
} }
} }
[Fact] [Fact]
public void Capture() public void Capture()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
var p1Bishop = board["B2"]; var p1Bishop = board["B2"];
p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop);
shogi.Move("C3", "C4", false); shogi.Move("C3", "C4", false);
shogi.Move("G7", "G6", false); shogi.Move("G7", "G6", false);
// Act - P1 Bishop captures P2 Bishop // Act - P1 Bishop captures P2 Bishop
shogi.Move("B2", "H8", false); shogi.Move("B2", "H8", false);
// Assert // Assert
board["B2"].Should().BeNull(); board["B2"].Should().BeNull();
board["H8"].Should().Be(p1Bishop); board["H8"].Should().Be(p1Bishop);
board board
.Player1Hand .Player1Hand
.Should() .Should()
.ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1);
} }
[Fact] [Fact]
public void CheckMate() public void CheckMate()
{ {
// Arrange // Arrange
var shogi = MockShogiBoard(); var shogi = MockShogiBoard();
var board = shogi.BoardState; var board = shogi.BoardState;
// P1 Rook // P1 Rook
shogi.Move("H2", "E2", false); shogi.Move("H2", "E2", false);
// P2 Gold // P2 Gold
shogi.Move("F9", "G8", false); shogi.Move("F9", "G8", false);
// P1 Pawn // P1 Pawn
shogi.Move("E3", "E4", false); shogi.Move("E3", "E4", false);
// P2 other Gold // P2 other Gold
shogi.Move("D9", "C8", false); shogi.Move("D9", "C8", false);
// P1 same Pawn // P1 same Pawn
shogi.Move("E4", "E5", false); shogi.Move("E4", "E5", false);
// P2 Pawn // P2 Pawn
shogi.Move("E7", "E6", false); shogi.Move("E7", "E6", false);
// P1 Pawn takes P2 Pawn // P1 Pawn takes P2 Pawn
shogi.Move("E5", "E6", false); shogi.Move("E5", "E6", false);
// P2 King // P2 King
shogi.Move("E9", "E8", false); shogi.Move("E9", "E8", false);
// P1 Pawn promotes; threatens P2 King // P1 Pawn promotes; threatens P2 King
shogi.Move("E6", "E7", true); shogi.Move("E6", "E7", true);
// P2 King retreat // P2 King retreat
shogi.Move("E8", "E9", false); shogi.Move("E8", "E9", false);
// Act - P1 Pawn wins by checkmate. // Act - P1 Pawn wins by checkmate.
shogi.Move("E7", "E8", false); shogi.Move("E7", "E8", false);
// Assert - checkmate // Assert - checkmate
console.WriteLine(shogi.ToStringStateAsAscii()); console.WriteLine(shogi.ToStringStateAsAscii());
console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString()))); console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString())));
board.IsCheckmate.Should().BeTrue(); board.IsCheckmate.Should().BeTrue();
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
} }
private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting);
} }
} }