yep
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
CREATE PROCEDURE [dbo].[CreateBoardState]
|
|
||||||
@boardStateDocument NVARCHAR(max),
|
|
||||||
@sessionName NVARCHAR(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
SET NOCOUNT ON
|
|
||||||
|
|
||||||
INSERT INTO [session].[BoardState] (Document, SessionId)
|
|
||||||
SELECT
|
|
||||||
@boardStateDocument,
|
|
||||||
Id
|
|
||||||
FROM [session].[Session]
|
|
||||||
WHERE [Name] = @sessionName;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
CREATE PROCEDURE [session].[CreateSession]
|
CREATE PROCEDURE [session].[CreateSession]
|
||||||
@InitialBoardStateDocument [session].[JsonDocument],
|
@Name [session].[SessionName],
|
||||||
@Player1Name [user].[UserName]
|
@Player1Name [user].[UserName],
|
||||||
|
@InitialBoardStateDocument [session].[JsonDocument]
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
CREATE TABLE [session].[Session]
|
CREATE TABLE [session].[Session]
|
||||||
(
|
(
|
||||||
Id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
Id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
[Name] [session].[SessionName] UNIQUE,
|
||||||
Player1Id BIGINT NOT NULL,
|
Player1Id BIGINT NOT NULL,
|
||||||
Player2Id BIGINT NULL,
|
Player2Id BIGINT NULL,
|
||||||
BoardState [session].[JsonDocument] NOT NULL,
|
BoardState [session].[JsonDocument] NOT NULL,
|
||||||
[Name] AS JSON_VALUE(BoardState, '$.Name') UNIQUE,
|
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
||||||
|
|
||||||
CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1),
|
CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1),
|
||||||
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
ON UPDATE CASCADE,
|
ON UPDATE CASCADE,
|
||||||
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
||||||
ON DELETE NO ACTION
|
ON DELETE NO ACTION
|
||||||
ON UPDATE NO ACTION
|
ON UPDATE NO ACTION
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -72,7 +72,6 @@
|
|||||||
<Build Include="User\user.sql" />
|
<Build Include="User\user.sql" />
|
||||||
<Build Include="Session\Tables\Session.sql" />
|
<Build Include="Session\Tables\Session.sql" />
|
||||||
<Build Include="Session\Stored Procedures\CreateSession.sql" />
|
<Build Include="Session\Stored Procedures\CreateSession.sql" />
|
||||||
<Build Include="Session\Stored Procedures\CreateBoardState.sql" />
|
|
||||||
<Build Include="User\Tables\User.sql" />
|
<Build Include="User\Tables\User.sql" />
|
||||||
<Build Include="Session\Types\SessionName.sql" />
|
<Build Include="Session\Types\SessionName.sql" />
|
||||||
<Build Include="User\Types\UserName.sql" />
|
<Build Include="User\Types\UserName.sql" />
|
||||||
|
|||||||
@@ -1,240 +1,27 @@
|
|||||||
using Shogi.Domain.ValueObjects;
|
using Shogi.Domain.ValueObjects;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Shogi.Domain;
|
namespace Shogi.Domain.Aggregates;
|
||||||
|
|
||||||
/// <summary>
|
public class Session
|
||||||
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
|
||||||
/// The board is always from Player1's perspective.
|
|
||||||
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ShogiBoard
|
|
||||||
{
|
{
|
||||||
private readonly StandardRules rules;
|
public Session(
|
||||||
|
string name,
|
||||||
|
string player1Name,
|
||||||
|
ShogiBoard board)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Player1 = player1Name;
|
||||||
|
Board = board;
|
||||||
|
}
|
||||||
|
|
||||||
public ShogiBoard(string name, BoardState initialState, string player1, string? player2 = null)
|
public string Name { get; }
|
||||||
{
|
public ShogiBoard Board { get; }
|
||||||
Name = name;
|
public string Player1 { get; }
|
||||||
Player1 = player1;
|
public string? Player2 { get; private set; }
|
||||||
Player2 = player2;
|
|
||||||
BoardState = initialState;
|
|
||||||
rules = new StandardRules(BoardState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BoardState BoardState { get; }
|
public void AddPlayer2(string player2Name)
|
||||||
public string Name { get; }
|
{
|
||||||
|
if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player.");
|
||||||
/// <summary>
|
Player2 = player2Name;
|
||||||
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The strategy involves simulating a move on a throw-away board state that can be used to
|
|
||||||
/// validate legal vs illegal moves without having to worry about reverting board state.
|
|
||||||
/// </remarks>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public void Move(string from, string to, bool isPromotion)
|
|
||||||
{
|
|
||||||
var simulationState = new BoardState(BoardState);
|
|
||||||
var simulation = new StandardRules(simulationState);
|
|
||||||
var moveResult = simulation.Move(from, to, isPromotion);
|
|
||||||
if (!moveResult.Success)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If already in check, assert the move that resulted in check no longer results in check.
|
|
||||||
if (BoardState.InCheck == BoardState.WhoseTurn
|
|
||||||
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMoveTo))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to move because you are still in check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
|
||||||
if (simulation.IsPlayerInCheckAfterMove())
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = rules.Move(from, to, isPromotion);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
|
||||||
if (index == -1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BoardState[to] != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var toVector = Notation.FromBoardNotation(to);
|
|
||||||
switch (pieceInHand)
|
|
||||||
{
|
|
||||||
case WhichPiece.Knight:
|
|
||||||
{
|
|
||||||
// Knight cannot be placed onto the farthest two ranks from the hand.
|
|
||||||
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6
|
|
||||||
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WhichPiece.Lance:
|
|
||||||
case WhichPiece.Pawn:
|
|
||||||
{
|
|
||||||
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
|
||||||
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8
|
|
||||||
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempBoard = new BoardState(BoardState);
|
|
||||||
var simulation = new StandardRules(tempBoard);
|
|
||||||
var moveResult = simulation.Move(pieceInHand, to);
|
|
||||||
if (!moveResult.Success)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
|
||||||
if (BoardState.InCheck == BoardState.WhoseTurn)
|
|
||||||
{
|
|
||||||
//if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn))
|
|
||||||
//{
|
|
||||||
// throw new InvalidOperationException("Illegal move. You're still in check!");
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition;
|
|
||||||
//if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer))
|
|
||||||
//{
|
|
||||||
|
|
||||||
//}
|
|
||||||
|
|
||||||
//rules.Move(from, to, isPromotion);
|
|
||||||
//if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer))
|
|
||||||
//{
|
|
||||||
// board.InCheck = otherPlayer;
|
|
||||||
// board.IsCheckmate = rules.EvaluateCheckmate();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
//{
|
|
||||||
// board.InCheck = null;
|
|
||||||
//}
|
|
||||||
BoardState.WhoseTurn = otherPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints a ASCII representation of the board for debugging board state.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string ToStringStateAsAscii()
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append("Player 2(.)");
|
|
||||||
builder.AppendLine();
|
|
||||||
for (var rank = 8; rank >= 0; rank--)
|
|
||||||
{
|
|
||||||
// Horizontal line
|
|
||||||
builder.Append(" - ");
|
|
||||||
for (var file = 0; file < 8; file++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
|
|
||||||
// Print Rank ruler.
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="piece"></param>
|
|
||||||
/// <returns>
|
|
||||||
/// A string with three characters.
|
|
||||||
/// The first character indicates promotion status.
|
|
||||||
/// The second character indicates piece.
|
|
||||||
/// The third character indicates ownership.
|
|
||||||
/// </returns>
|
|
||||||
private static string ToAscii(Piece piece)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
if (piece.IsPromoted) builder.Append('^');
|
|
||||||
else builder.Append(' ');
|
|
||||||
|
|
||||||
var name = piece.WhichPiece switch
|
|
||||||
{
|
|
||||||
WhichPiece.King => "K",
|
|
||||||
WhichPiece.GoldGeneral => "G",
|
|
||||||
WhichPiece.SilverGeneral => "S",
|
|
||||||
WhichPiece.Bishop => "B",
|
|
||||||
WhichPiece.Rook => "R",
|
|
||||||
WhichPiece.Knight => "k",
|
|
||||||
WhichPiece.Lance => "L",
|
|
||||||
WhichPiece.Pawn => "P",
|
|
||||||
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."),
|
|
||||||
};
|
|
||||||
builder.Append(name);
|
|
||||||
|
|
||||||
if (piece.Owner == WhichPlayer.Player2) builder.Append('.');
|
|
||||||
else builder.Append(' ');
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,251 +1,253 @@
|
|||||||
using Shogi.Domain.ValueObjects;
|
using Shogi.Domain.ValueObjects;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain;
|
||||||
|
|
||||||
|
public class BoardState
|
||||||
{
|
{
|
||||||
public class BoardState
|
/// <summary>
|
||||||
{
|
/// Board state before any moves have been made, using standard setup and rules.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Board state before any moves have been made, using standard setup and rules.
|
public static readonly BoardState StandardStarting = new(
|
||||||
/// </summary>
|
state: BuildStandardStartingBoardState(),
|
||||||
public static readonly BoardState StandardStarting = new();
|
player1Hand: new(),
|
||||||
|
player2Hand: new(),
|
||||||
|
whoseTurn: WhichPlayer.Player1,
|
||||||
|
playerInCheck: null,
|
||||||
|
previousMove: new Move());
|
||||||
|
|
||||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key is position notation, such as "E4".
|
/// Key is position notation, such as "E4".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<string, Piece?> board;
|
private readonly Dictionary<string, Piece?> board;
|
||||||
|
|
||||||
|
public BoardState(
|
||||||
|
Dictionary<string, Piece?> state,
|
||||||
|
List<Piece> player1Hand,
|
||||||
|
List<Piece> player2Hand,
|
||||||
|
WhichPlayer whoseTurn,
|
||||||
|
WhichPlayer? playerInCheck,
|
||||||
|
Move previousMove)
|
||||||
|
{
|
||||||
|
board = state;
|
||||||
|
Player1Hand = player1Hand;
|
||||||
|
Player2Hand = player2Hand;
|
||||||
|
PreviousMove = previousMove;
|
||||||
|
WhoseTurn = whoseTurn;
|
||||||
|
InCheck = playerInCheck;
|
||||||
|
}
|
||||||
|
|
||||||
public BoardState(Dictionary<string, Piece?> state)
|
/// <summary>
|
||||||
{
|
/// Copy constructor.
|
||||||
board = state;
|
/// </summary>
|
||||||
Player1Hand = new List<Piece>();
|
public BoardState(BoardState other)
|
||||||
Player2Hand = new List<Piece>();
|
{
|
||||||
PreviousMoveTo = Vector2.Zero;
|
board = new(81);
|
||||||
}
|
foreach (var kvp in other.board)
|
||||||
|
{
|
||||||
|
var piece = kvp.Value;
|
||||||
|
board[kvp.Key] = piece == null ? null : Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
||||||
|
}
|
||||||
|
WhoseTurn = other.WhoseTurn;
|
||||||
|
InCheck = other.InCheck;
|
||||||
|
IsCheckmate = other.IsCheckmate;
|
||||||
|
PreviousMove = other.PreviousMove;
|
||||||
|
Player1Hand = new(other.Player1Hand);
|
||||||
|
Player2Hand = new(other.Player2Hand);
|
||||||
|
}
|
||||||
|
|
||||||
public BoardState()
|
public ReadOnlyDictionary<string, Piece?> State => new(board);
|
||||||
{
|
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
InitializeBoardState();
|
{
|
||||||
Player1Hand = new List<Piece>();
|
var piece = kvp.Value;
|
||||||
Player2Hand = new List<Piece>();
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
||||||
PreviousMoveTo = Vector2.Zero;
|
}).Key);
|
||||||
}
|
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
|
{
|
||||||
|
var piece = kvp.Value;
|
||||||
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
||||||
|
}).Key);
|
||||||
|
public List<Piece> Player1Hand { get; }
|
||||||
|
public List<Piece> Player2Hand { get; }
|
||||||
|
public Move PreviousMove { get; set; }
|
||||||
|
public WhichPlayer WhoseTurn { get; set; }
|
||||||
|
public WhichPlayer? InCheck { get; set; }
|
||||||
|
public bool IsCheckmate { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, Piece?> State => board;
|
public Piece? this[string notation]
|
||||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
{
|
||||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||||
{
|
get => board[notation];
|
||||||
var piece = kvp.Value;
|
set => board[notation] = value;
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
}
|
||||||
}).Key);
|
|
||||||
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
|
||||||
{
|
|
||||||
var piece = kvp.Value;
|
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
|
||||||
}).Key);
|
|
||||||
public List<Piece> Player1Hand { get; }
|
|
||||||
public List<Piece> Player2Hand { get; }
|
|
||||||
public Vector2 PreviousMoveFrom { get; private set; }
|
|
||||||
public Vector2 PreviousMoveTo { get; private set; }
|
|
||||||
public WhichPlayer WhoseTurn { get; set; }
|
|
||||||
public WhichPlayer? InCheck { get; set; }
|
|
||||||
public bool IsCheckmate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
public Piece? this[Vector2 vector]
|
||||||
/// Copy constructor.
|
{
|
||||||
/// </summary>
|
get => this[Notation.ToBoardNotation(vector)];
|
||||||
public BoardState(BoardState other) : this()
|
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||||
{
|
}
|
||||||
foreach (var kvp in other.board)
|
|
||||||
{
|
|
||||||
// Replace copy constructor with static factory method in Piece.cs
|
|
||||||
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
|
||||||
}
|
|
||||||
WhoseTurn = other.WhoseTurn;
|
|
||||||
InCheck = other.InCheck;
|
|
||||||
IsCheckmate = other.IsCheckmate;
|
|
||||||
PreviousMoveTo = other.PreviousMoveTo;
|
|
||||||
Player1Hand.AddRange(other.Player1Hand);
|
|
||||||
Player2Hand.AddRange(other.Player2Hand);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Piece? this[string notation]
|
public Piece? this[int x, int y]
|
||||||
{
|
{
|
||||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
get => this[Notation.ToBoardNotation(x, y)];
|
||||||
get => board[notation];
|
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||||
set => board[notation] = value;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Piece? this[Vector2 vector]
|
/// <summary>
|
||||||
{
|
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||||
get => this[Notation.ToBoardNotation(vector)];
|
/// </summary>
|
||||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
||||||
}
|
{
|
||||||
|
return !path.Any()
|
||||||
|
|| path.SkipLast(1).Any(position => this[position] != null)
|
||||||
|
|| this[path.Last()]?.Owner == WhoseTurn;
|
||||||
|
}
|
||||||
|
|
||||||
public Piece? this[int x, int y]
|
internal bool IsWithinPromotionZone(Vector2 position)
|
||||||
{
|
{
|
||||||
get => this[Notation.ToBoardNotation(x, y)];
|
// TODO: Move this promotion zone logic into the StandardRules class.
|
||||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||||
}
|
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||||
|
}
|
||||||
|
|
||||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||||
{
|
{
|
||||||
PreviousMoveFrom = from;
|
return position.X <= 8 && position.X >= 0
|
||||||
PreviousMoveTo = to;
|
&& position.Y <= 8 && position.Y >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||||
/// </summary>
|
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||||
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
.ToList();
|
||||||
{
|
|
||||||
return !path.Any()
|
|
||||||
|| path.SkipLast(1).Any(position => this[position] != null)
|
|
||||||
|| this[path.Last()]?.Owner == WhoseTurn;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool IsWithinPromotionZone(Vector2 position)
|
internal void Capture(Vector2 to)
|
||||||
{
|
{
|
||||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
var piece = this[to];
|
||||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
piece.Capture(WhoseTurn);
|
||||||
{
|
ActivePlayerHand.Add(piece);
|
||||||
return position.X <= 8 && position.X >= 0
|
}
|
||||||
&& position.Y <= 8 && position.Y >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
/// <summary>
|
||||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
/// Does not include the start position.
|
||||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
/// </summary>
|
||||||
.ToList();
|
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||||
|
{
|
||||||
|
var next = start;
|
||||||
|
while (IsWithinBoardBoundary(next + direction))
|
||||||
|
{
|
||||||
|
next += direction;
|
||||||
|
yield return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal void Capture(Vector2 to)
|
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||||
{
|
{
|
||||||
var piece = this[to];
|
foreach (var step in path)
|
||||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
{
|
||||||
|
if (this[step] != null) return this[step];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
piece.Capture(WhoseTurn);
|
private static Dictionary<string, Piece?> BuildStandardStartingBoardState()
|
||||||
ActivePlayerHand.Add(piece);
|
{
|
||||||
}
|
return new Dictionary<string, Piece?>(81)
|
||||||
|
{
|
||||||
|
["A1"] = new Lance(WhichPlayer.Player1),
|
||||||
|
["B1"] = new Knight(WhichPlayer.Player1),
|
||||||
|
["C1"] = new SilverGeneral(WhichPlayer.Player1),
|
||||||
|
["D1"] = new GoldGeneral(WhichPlayer.Player1),
|
||||||
|
["E1"] = new King(WhichPlayer.Player1),
|
||||||
|
["F1"] = new GoldGeneral(WhichPlayer.Player1),
|
||||||
|
["G1"] = new SilverGeneral(WhichPlayer.Player1),
|
||||||
|
["H1"] = new Knight(WhichPlayer.Player1),
|
||||||
|
["I1"] = new Lance(WhichPlayer.Player1),
|
||||||
|
|
||||||
/// <summary>
|
["A2"] = null,
|
||||||
/// Does not include the start position.
|
["B2"] = new Bishop(WhichPlayer.Player1),
|
||||||
/// </summary>
|
["C2"] = null,
|
||||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
["D2"] = null,
|
||||||
{
|
["E2"] = null,
|
||||||
var next = start;
|
["F2"] = null,
|
||||||
while (IsWithinBoardBoundary(next + direction))
|
["G2"] = null,
|
||||||
{
|
["H2"] = new Rook(WhichPlayer.Player1),
|
||||||
next += direction;
|
["I2"] = null,
|
||||||
yield return next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
["A3"] = new Pawn(WhichPlayer.Player1),
|
||||||
{
|
["B3"] = new Pawn(WhichPlayer.Player1),
|
||||||
foreach (var step in path)
|
["C3"] = new Pawn(WhichPlayer.Player1),
|
||||||
{
|
["D3"] = new Pawn(WhichPlayer.Player1),
|
||||||
if (this[step] != null) return this[step];
|
["E3"] = new Pawn(WhichPlayer.Player1),
|
||||||
}
|
["F3"] = new Pawn(WhichPlayer.Player1),
|
||||||
return null;
|
["G3"] = new Pawn(WhichPlayer.Player1),
|
||||||
}
|
["H3"] = new Pawn(WhichPlayer.Player1),
|
||||||
|
["I3"] = new Pawn(WhichPlayer.Player1),
|
||||||
|
|
||||||
private void InitializeBoardState()
|
["A4"] = null,
|
||||||
{
|
["B4"] = null,
|
||||||
this["A1"] = new Lance(WhichPlayer.Player1);
|
["C4"] = null,
|
||||||
this["B1"] = new Knight(WhichPlayer.Player1);
|
["D4"] = null,
|
||||||
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
["E4"] = null,
|
||||||
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
["F4"] = null,
|
||||||
this["E1"] = new King(WhichPlayer.Player1);
|
["G4"] = null,
|
||||||
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
["H4"] = null,
|
||||||
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
["I4"] = null,
|
||||||
this["H1"] = new Knight(WhichPlayer.Player1);
|
|
||||||
this["I1"] = new Lance(WhichPlayer.Player1);
|
|
||||||
|
|
||||||
this["A2"] = null;
|
["A5"] = null,
|
||||||
this["B2"] = new Bishop(WhichPlayer.Player1);
|
["B5"] = null,
|
||||||
this["C2"] = null;
|
["C5"] = null,
|
||||||
this["D2"] = null;
|
["D5"] = null,
|
||||||
this["E2"] = null;
|
["E5"] = null,
|
||||||
this["F2"] = null;
|
["F5"] = null,
|
||||||
this["G2"] = null;
|
["G5"] = null,
|
||||||
this["H2"] = new Rook(WhichPlayer.Player1);
|
["H5"] = null,
|
||||||
this["I2"] = null;
|
["I5"] = null,
|
||||||
|
|
||||||
this["A3"] = new Pawn(WhichPlayer.Player1);
|
["A6"] = null,
|
||||||
this["B3"] = new Pawn(WhichPlayer.Player1);
|
["B6"] = null,
|
||||||
this["C3"] = new Pawn(WhichPlayer.Player1);
|
["C6"] = null,
|
||||||
this["D3"] = new Pawn(WhichPlayer.Player1);
|
["D6"] = null,
|
||||||
this["E3"] = new Pawn(WhichPlayer.Player1);
|
["E6"] = null,
|
||||||
this["F3"] = new Pawn(WhichPlayer.Player1);
|
["F6"] = null,
|
||||||
this["G3"] = new Pawn(WhichPlayer.Player1);
|
["G6"] = null,
|
||||||
this["H3"] = new Pawn(WhichPlayer.Player1);
|
["H6"] = null,
|
||||||
this["I3"] = new Pawn(WhichPlayer.Player1);
|
["I6"] = null,
|
||||||
|
|
||||||
this["A4"] = null;
|
["A7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["B4"] = null;
|
["B7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["C4"] = null;
|
["C7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["D4"] = null;
|
["D7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["E4"] = null;
|
["E7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["F4"] = null;
|
["F7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["G4"] = null;
|
["G7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["H4"] = null;
|
["H7"] = new Pawn(WhichPlayer.Player2),
|
||||||
this["I4"] = null;
|
["I7"] = new Pawn(WhichPlayer.Player2),
|
||||||
|
|
||||||
this["A5"] = null;
|
["A8"] = null,
|
||||||
this["B5"] = null;
|
["B8"] = new Rook(WhichPlayer.Player2),
|
||||||
this["C5"] = null;
|
["C8"] = null,
|
||||||
this["D5"] = null;
|
["D8"] = null,
|
||||||
this["E5"] = null;
|
["E8"] = null,
|
||||||
this["F5"] = null;
|
["F8"] = null,
|
||||||
this["G5"] = null;
|
["G8"] = null,
|
||||||
this["H5"] = null;
|
["H8"] = new Bishop(WhichPlayer.Player2),
|
||||||
this["I5"] = null;
|
["I8"] = null,
|
||||||
|
|
||||||
this["A6"] = null;
|
["A9"] = new Lance(WhichPlayer.Player2),
|
||||||
this["B6"] = null;
|
["B9"] = new Knight(WhichPlayer.Player2),
|
||||||
this["C6"] = null;
|
["C9"] = new SilverGeneral(WhichPlayer.Player2),
|
||||||
this["D6"] = null;
|
["D9"] = new GoldGeneral(WhichPlayer.Player2),
|
||||||
this["E6"] = null;
|
["E9"] = new King(WhichPlayer.Player2),
|
||||||
this["F6"] = null;
|
["F9"] = new GoldGeneral(WhichPlayer.Player2),
|
||||||
this["G6"] = null;
|
["G9"] = new SilverGeneral(WhichPlayer.Player2),
|
||||||
this["H6"] = null;
|
["H9"] = new Knight(WhichPlayer.Player2),
|
||||||
this["I6"] = null;
|
["I9"] = new Lance(WhichPlayer.Player2)
|
||||||
|
};
|
||||||
this["A7"] = new Pawn(WhichPlayer.Player2);
|
}
|
||||||
this["B7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["C7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["D7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["E7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["F7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["G7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["H7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
this["I7"] = new Pawn(WhichPlayer.Player2);
|
|
||||||
|
|
||||||
this["A8"] = null;
|
|
||||||
this["B8"] = new Rook(WhichPlayer.Player2);
|
|
||||||
this["C8"] = null;
|
|
||||||
this["D8"] = null;
|
|
||||||
this["E8"] = null;
|
|
||||||
this["F8"] = null;
|
|
||||||
this["G8"] = null;
|
|
||||||
this["H8"] = new Bishop(WhichPlayer.Player2);
|
|
||||||
this["I8"] = null;
|
|
||||||
|
|
||||||
this["A9"] = new Lance(WhichPlayer.Player2);
|
|
||||||
this["B9"] = new Knight(WhichPlayer.Player2);
|
|
||||||
this["C9"] = new SilverGeneral(WhichPlayer.Player2);
|
|
||||||
this["D9"] = new GoldGeneral(WhichPlayer.Player2);
|
|
||||||
this["E9"] = new King(WhichPlayer.Player2);
|
|
||||||
this["F9"] = new GoldGeneral(WhichPlayer.Player2);
|
|
||||||
this["G9"] = new SilverGeneral(WhichPlayer.Player2);
|
|
||||||
this["H9"] = new Knight(WhichPlayer.Player2);
|
|
||||||
this["I9"] = new Lance(WhichPlayer.Player2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using Shogi.Domain.Pathing;
|
using Shogi.Domain.ValueObjects;
|
||||||
using Shogi.Domain.ValueObjects;
|
|
||||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain
|
||||||
{
|
{
|
||||||
internal class StandardRules
|
internal class StandardRules
|
||||||
{
|
{
|
||||||
private readonly BoardState boardState;
|
private readonly BoardState boardState;
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ namespace Shogi.Domain
|
|||||||
boardState[to] = fromPiece;
|
boardState[to] = fromPiece;
|
||||||
boardState[from] = null;
|
boardState[from] = null;
|
||||||
|
|
||||||
boardState.RememberAsMostRecentMove(from, to);
|
boardState.PreviousMove = new Move(from, to);
|
||||||
var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||||
boardState.WhoseTurn = otherPlayer;
|
boardState.WhoseTurn = otherPlayer;
|
||||||
|
|
||||||
@@ -121,16 +120,16 @@ namespace Shogi.Domain
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal bool IsPlayerInCheckAfterMove()
|
internal bool IsPlayerInCheckAfterMove()
|
||||||
{
|
{
|
||||||
var previousMovedPiece = boardState[boardState.PreviousMoveTo];
|
var previousMovedPiece = boardState[boardState.PreviousMove.To];
|
||||||
if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMoveTo}.");
|
if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}.");
|
||||||
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition;
|
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition;
|
||||||
|
|
||||||
var isCheck = false;
|
var isCheck = false;
|
||||||
|
|
||||||
// Get line equation from king through the now-unoccupied location.
|
// Get line equation from king through the now-unoccupied location.
|
||||||
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom);
|
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From);
|
||||||
var slope = Math.Abs(direction.Y / direction.X);
|
var slope = Math.Abs(direction.Y / direction.X);
|
||||||
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction));
|
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From, Vector2.Normalize(direction));
|
||||||
var threat = boardState.QueryFirstPieceInPath(path);
|
var threat = boardState.QueryFirstPieceInPath(path);
|
||||||
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
||||||
// If absolute slope is 45°, look for a bishop along the line.
|
// If absolute slope is 45°, look for a bishop along the line.
|
||||||
@@ -165,7 +164,7 @@ namespace Shogi.Domain
|
|||||||
return isCheck;
|
return isCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo);
|
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To);
|
||||||
|
|
||||||
internal bool IsOpposingKingThreatenedByPosition(Vector2 position)
|
internal bool IsOpposingKingThreatenedByPosition(Vector2 position)
|
||||||
{
|
{
|
||||||
|
|||||||
8
Shogi.Domain/ValueObjects/Move.cs
Normal file
8
Shogi.Domain/ValueObjects/Move.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Shogi.Domain.ValueObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single piece being moved by a player from <paramref name="From"/> to <paramref name="To"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record struct Move(Vector2 From, Vector2 To)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -6,10 +6,6 @@ namespace Shogi.Domain.ValueObjects
|
|||||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
||||||
public abstract record class Piece
|
public abstract record class Piece
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Creates a clone of an existing piece.
|
|
||||||
/// </summary>
|
|
||||||
public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
|
||||||
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
||||||
{
|
{
|
||||||
return piece switch
|
return piece switch
|
||||||
@@ -54,7 +50,7 @@ namespace Shogi.Domain.ValueObjects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Respecting the move-set of the Piece, collect all positions from start to end.
|
/// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end.
|
||||||
/// Useful if you need to iterate a move-set.
|
/// Useful if you need to iterate a move-set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="start"></param>
|
/// <param name="start"></param>
|
||||||
|
|||||||
235
Shogi.Domain/ValueObjects/ShogiBoard.cs
Normal file
235
Shogi.Domain/ValueObjects/ShogiBoard.cs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
||||||
|
/// The board is always from Player1's perspective.
|
||||||
|
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShogiBoard
|
||||||
|
{
|
||||||
|
private readonly StandardRules rules;
|
||||||
|
|
||||||
|
public ShogiBoard(BoardState initialState)
|
||||||
|
{
|
||||||
|
BoardState = initialState;
|
||||||
|
rules = new StandardRules(BoardState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardState BoardState { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The strategy involves simulating a move on a throw-away board state that can be used to
|
||||||
|
/// validate legal vs illegal moves without having to worry about reverting board state.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public void Move(string from, string to, bool isPromotion)
|
||||||
|
{
|
||||||
|
var simulationState = new BoardState(BoardState);
|
||||||
|
var simulation = new StandardRules(simulationState);
|
||||||
|
var moveResult = simulation.Move(from, to, isPromotion);
|
||||||
|
if (!moveResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already in check, assert the move that resulted in check no longer results in check.
|
||||||
|
if (BoardState.InCheck == BoardState.WhoseTurn
|
||||||
|
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMove.To))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unable to move because you are still in check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||||
|
if (simulation.IsPlayerInCheckAfterMove())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = rules.Move(from, to, isPromotion);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BoardState[to] != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var toVector = Notation.FromBoardNotation(to);
|
||||||
|
switch (pieceInHand)
|
||||||
|
{
|
||||||
|
case WhichPiece.Knight:
|
||||||
|
{
|
||||||
|
// Knight cannot be placed onto the farthest two ranks from the hand.
|
||||||
|
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6
|
||||||
|
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WhichPiece.Lance:
|
||||||
|
case WhichPiece.Pawn:
|
||||||
|
{
|
||||||
|
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
||||||
|
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8
|
||||||
|
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempBoard = new BoardState(BoardState);
|
||||||
|
var simulation = new StandardRules(tempBoard);
|
||||||
|
var moveResult = simulation.Move(pieceInHand, to);
|
||||||
|
if (!moveResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||||
|
if (BoardState.InCheck == BoardState.WhoseTurn)
|
||||||
|
{
|
||||||
|
//if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn))
|
||||||
|
//{
|
||||||
|
// throw new InvalidOperationException("Illegal move. You're still in check!");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition;
|
||||||
|
//if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer))
|
||||||
|
//{
|
||||||
|
|
||||||
|
//}
|
||||||
|
|
||||||
|
//rules.Move(from, to, isPromotion);
|
||||||
|
//if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer))
|
||||||
|
//{
|
||||||
|
// board.InCheck = otherPlayer;
|
||||||
|
// board.IsCheckmate = rules.EvaluateCheckmate();
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// board.InCheck = null;
|
||||||
|
//}
|
||||||
|
BoardState.WhoseTurn = otherPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prints a ASCII representation of the board for debugging board state.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ToStringStateAsAscii()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append(" ");
|
||||||
|
builder.Append("Player 2(.)");
|
||||||
|
builder.AppendLine();
|
||||||
|
for (var rank = 8; rank >= 0; rank--)
|
||||||
|
{
|
||||||
|
// Horizontal line
|
||||||
|
builder.Append(" - ");
|
||||||
|
for (var file = 0; file < 8; file++) builder.Append("- - ");
|
||||||
|
builder.Append("- -");
|
||||||
|
|
||||||
|
// Print Rank ruler.
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="piece"></param>
|
||||||
|
/// <returns>
|
||||||
|
/// A string with three characters.
|
||||||
|
/// The first character indicates promotion status.
|
||||||
|
/// The second character indicates piece.
|
||||||
|
/// The third character indicates ownership.
|
||||||
|
/// </returns>
|
||||||
|
private static string ToAscii(Piece piece)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
if (piece.IsPromoted) builder.Append('^');
|
||||||
|
else builder.Append(' ');
|
||||||
|
|
||||||
|
var name = piece.WhichPiece switch
|
||||||
|
{
|
||||||
|
WhichPiece.King => "K",
|
||||||
|
WhichPiece.GoldGeneral => "G",
|
||||||
|
WhichPiece.SilverGeneral => "S",
|
||||||
|
WhichPiece.Bishop => "B",
|
||||||
|
WhichPiece.Rook => "R",
|
||||||
|
WhichPiece.Knight => "k",
|
||||||
|
WhichPiece.Lance => "L",
|
||||||
|
WhichPiece.Pawn => "P",
|
||||||
|
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."),
|
||||||
|
};
|
||||||
|
builder.Append(name);
|
||||||
|
|
||||||
|
if (piece.Owner == WhichPlayer.Player2) builder.Append('.');
|
||||||
|
else builder.Append(' ');
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,17 +19,20 @@ public class SessionController : ControllerBase
|
|||||||
private readonly IModelMapper mapper;
|
private readonly IModelMapper mapper;
|
||||||
private readonly ISessionRepository sessionRepository;
|
private readonly ISessionRepository sessionRepository;
|
||||||
private readonly IQueryRespository queryRespository;
|
private readonly IQueryRespository queryRespository;
|
||||||
|
private readonly ILogger<SessionController> logger;
|
||||||
|
|
||||||
public SessionController(
|
public SessionController(
|
||||||
ISocketConnectionManager communicationManager,
|
ISocketConnectionManager communicationManager,
|
||||||
IModelMapper mapper,
|
IModelMapper mapper,
|
||||||
ISessionRepository sessionRepository,
|
ISessionRepository sessionRepository,
|
||||||
IQueryRespository queryRespository)
|
IQueryRespository queryRespository,
|
||||||
|
ILogger<SessionController> logger)
|
||||||
{
|
{
|
||||||
this.communicationManager = communicationManager;
|
this.communicationManager = communicationManager;
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.sessionRepository = sessionRepository;
|
this.sessionRepository = sessionRepository;
|
||||||
this.queryRespository = queryRespository;
|
this.queryRespository = queryRespository;
|
||||||
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -37,13 +40,17 @@ public class SessionController : ControllerBase
|
|||||||
{
|
{
|
||||||
var userId = User.GetShogiUserId();
|
var userId = User.GetShogiUserId();
|
||||||
if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized();
|
if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized();
|
||||||
var session = new Domain.ShogiBoard(request.Name, Domain.BoardState.StandardStarting, userId);
|
var session = new Domain.Aggregates.Session(
|
||||||
|
request.Name,
|
||||||
|
userId,
|
||||||
|
new Domain.ValueObjects.ShogiBoard(Domain.BoardState.StandardStarting));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await sessionRepository.CreateSession(session);
|
await sessionRepository.CreateShogiBoard(board, request.Name, userId);
|
||||||
}
|
}
|
||||||
catch (SqlException)
|
catch (SqlException e)
|
||||||
{
|
{
|
||||||
|
logger.LogError(exception: e, message: "Uh oh");
|
||||||
return this.Conflict();
|
return this.Conflict();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
Shogi.Sockets/Repositories/Dto/BoardStateDto.cs
Normal file
22
Shogi.Sockets/Repositories/Dto/BoardStateDto.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Shogi.Domain;
|
||||||
|
using Shogi.Domain.ValueObjects;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Repositories.Dto;
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public class BoardStateDto
|
||||||
|
{
|
||||||
|
public ReadOnlyDictionary<string, Piece?> State { get; set; }
|
||||||
|
|
||||||
|
public List<Piece> Player1Hand { get; set; }
|
||||||
|
|
||||||
|
public List<Piece> Player2Hand { get; set; }
|
||||||
|
|
||||||
|
public Move PreviousMove { get; }
|
||||||
|
|
||||||
|
public WhichPlayer WhoseTurn { get; set; }
|
||||||
|
public WhichPlayer? InCheck { get; set; }
|
||||||
|
public bool IsCheckmate { get; set; }
|
||||||
|
}
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
namespace Shogi.Api.Repositories.Dto
|
namespace Shogi.Api.Repositories.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Useful with Dapper to read from database.
|
||||||
|
/// </summary>
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public class SessionDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string Name { get; set; }
|
||||||
/// Useful with Dapper to read from database.
|
public string Player1 { get; set; }
|
||||||
/// </summary>
|
public string Player2 { get; set; }
|
||||||
public class SessionDto
|
public BoardStateDto BoardState { get; set; }
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Player1 { get; set; }
|
|
||||||
public string Player2 { get; set; }
|
|
||||||
public bool GameOver { get; set; }
|
|
||||||
public string BoardState { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
@@ -21,19 +21,9 @@ public class QueryRepository : IQueryRespository
|
|||||||
"session.ReadAllSessionsMetadata",
|
"session.ReadAllSessionsMetadata",
|
||||||
commandType: System.Data.CommandType.StoredProcedure);
|
commandType: System.Data.CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SessionMetadata?> ReadSession(string name)
|
|
||||||
{
|
|
||||||
using var connection = new SqlConnection(connectionString);
|
|
||||||
var results = await connection.QueryAsync<SessionDto>(
|
|
||||||
"session.ReadSession",
|
|
||||||
commandType: System.Data.CommandType.StoredProcedure);
|
|
||||||
return results.SingleOrDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IQueryRespository
|
public interface IQueryRespository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata();
|
Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata();
|
||||||
Task<SessionMetadata?> ReadSession(string name);
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Shogi.Api.Repositories.Dto;
|
using Shogi.Api.Repositories.Dto;
|
||||||
using Shogi.Domain;
|
using Shogi.Domain;
|
||||||
|
using Shogi.Domain.Aggregates;
|
||||||
|
using Shogi.Domain.ValueObjects;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.SqlClient;
|
using System.Data.SqlClient;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -16,22 +18,53 @@ public class SessionRepository : ISessionRepository
|
|||||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateSession(ShogiBoard session, string player1)
|
public async Task CreateSession(Session session)
|
||||||
{
|
{
|
||||||
var initialBoardState = JsonSerializer.Serialize(session.BoardState);
|
var boardStateDto = new BoardStateDto
|
||||||
|
{
|
||||||
|
InCheck = session.BoardState.InCheck,
|
||||||
|
IsCheckmate = session.BoardState.IsCheckmate,
|
||||||
|
Player1Hand = session.BoardState.Player1Hand,
|
||||||
|
Player2Hand = session.BoardState.Player2Hand,
|
||||||
|
State = session.BoardState.State,
|
||||||
|
WhoseTurn = session.BoardState.WhoseTurn,
|
||||||
|
};
|
||||||
|
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"session.CreateSession",
|
"session.CreateSession",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
InitialBoardStateDocument = initialBoardState,
|
Name = sessionName,
|
||||||
|
InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto),
|
||||||
Player1Name = player1,
|
Player1Name = player1,
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ShogiBoard?> ReadShogiBoard(string name)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(connectionString);
|
||||||
|
var results = await connection.QueryAsync<SessionDto>(
|
||||||
|
"session.ReadSession",
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
var dto = results.SingleOrDefault();
|
||||||
|
if (dto == null) return null;
|
||||||
|
|
||||||
|
var boardState = new BoardState(
|
||||||
|
state: new(dto.BoardState.State),
|
||||||
|
player1Hand: dto.BoardState.Player1Hand,
|
||||||
|
player2Hand: dto.BoardState.Player2Hand,
|
||||||
|
whoseTurn: dto.BoardState.WhoseTurn,
|
||||||
|
playerInCheck: dto.BoardState.InCheck,
|
||||||
|
previousMove: dto.BoardState.PreviousMove);
|
||||||
|
var session = new ShogiBoard(boardState);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ISessionRepository
|
public interface ISessionRepository
|
||||||
{
|
{
|
||||||
Task CreateSession(ShogiBoard session, string player1);
|
Task CreateSession(Session session);
|
||||||
|
Task<ShogiBoard?> ReadShogiBoard(string name);
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void MoveAPieceToAnEmptyPosition()
|
public void MoveAPieceToAnEmptyPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
var shogi = MockShogiBoard();
|
||||||
var board = shogi.BoardState;
|
var board = shogi.BoardState;
|
||||||
|
|
||||||
board["A4"].Should().BeNull();
|
board["A4"].Should().BeNull();
|
||||||
@@ -33,7 +33,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void AllowValidMoves_AfterCheck()
|
public void AllowValidMoves_AfterCheck()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -58,7 +58,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
var shogi = MockShogiBoard();
|
||||||
var board = shogi.BoardState;
|
var board = shogi.BoardState;
|
||||||
board["D5"].Should().BeNull();
|
board["D5"].Should().BeNull();
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveToCurrentPosition()
|
public void PreventInvalidMoves_MoveToCurrentPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
var shogi = MockShogiBoard();
|
||||||
var board = shogi.BoardState;
|
var board = shogi.BoardState;
|
||||||
var expectedPiece = board["A3"];
|
var expectedPiece = board["A3"];
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveSet()
|
public void PreventInvalidMoves_MoveSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -121,7 +121,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_Ownership()
|
public void PreventInvalidMoves_Ownership()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -143,7 +143,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveThroughAllies()
|
public void PreventInvalidMoves_MoveThroughAllies()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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"];
|
||||||
@@ -166,7 +166,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_CaptureAlly()
|
public void PreventInvalidMoves_CaptureAlly()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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"];
|
||||||
@@ -190,7 +190,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_Check()
|
public void PreventInvalidMoves_Check()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -219,7 +219,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidDrops_MoveSet()
|
public void PreventInvalidDrops_MoveSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -358,7 +358,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Check()
|
public void Check()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -376,7 +376,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Promote()
|
public void Promote()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -401,7 +401,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Capture()
|
public void Capture()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -425,7 +425,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void CheckMate()
|
public void CheckMate()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var shogi = MockSession();
|
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);
|
||||||
@@ -457,6 +457,6 @@ namespace Shogi.Domain.UnitTests
|
|||||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
board.InCheck.Should().Be(WhichPlayer.Player2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ShogiBoard MockSession() => new ShogiBoard("Test Session", BoardState.StandardStarting, "Test P1", "Test P2");
|
private static ShogiBoard MockShogiBoard() => new ShogiBoard("Test Session", BoardState.StandardStarting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user