Yep
This commit is contained in:
@@ -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
|
||||||
*/
|
*/
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace Shogi.Domain.Other;
|
|
||||||
|
|
||||||
public record RulesLifecycleResult(bool IsError, string ResultMessage = "")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -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)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
9
Shogi.Domain/ValueObjects/InCheckResult.cs
Normal file
9
Shogi.Domain/ValueObjects/InCheckResult.cs
Normal 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
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -111,19 +111,3 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css
Normal file
14
Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
97
Tests/UnitTests/Extensions.cs
Normal file
97
Tests/UnitTests/Extensions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Shogi.Domain.ValueObjects;
|
using Shogi.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Shogi.Domain.UnitTests;
|
namespace UnitTests;
|
||||||
|
|
||||||
public class ShogiBoardStateShould
|
public class ShogiBoardStateShould
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user