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]
|
||||
@InitialBoardStateDocument [session].[JsonDocument],
|
||||
@Player1Name [user].[UserName]
|
||||
@Name [session].[SessionName],
|
||||
@Player1Name [user].[UserName],
|
||||
@InitialBoardStateDocument [session].[JsonDocument]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
CREATE TABLE [session].[Session]
|
||||
(
|
||||
Id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
||||
[Name] [session].[SessionName] UNIQUE,
|
||||
Player1Id BIGINT NOT NULL,
|
||||
Player2Id BIGINT 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 FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||
ON DELETE CASCADE
|
||||
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||
ON DELETE 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 UPDATE NO ACTION
|
||||
)
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
<Build Include="User\user.sql" />
|
||||
<Build Include="Session\Tables\Session.sql" />
|
||||
<Build Include="Session\Stored Procedures\CreateSession.sql" />
|
||||
<Build Include="Session\Stored Procedures\CreateBoardState.sql" />
|
||||
<Build Include="User\Tables\User.sql" />
|
||||
<Build Include="Session\Types\SessionName.sql" />
|
||||
<Build Include="User\Types\UserName.sql" />
|
||||
|
||||
@@ -1,240 +1,27 @@
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System.Text;
|
||||
|
||||
namespace Shogi.Domain;
|
||||
namespace Shogi.Domain.Aggregates;
|
||||
|
||||
/// <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
|
||||
public class Session
|
||||
{
|
||||
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)
|
||||
{
|
||||
Name = name;
|
||||
Player1 = player1;
|
||||
Player2 = player2;
|
||||
BoardState = initialState;
|
||||
rules = new StandardRules(BoardState);
|
||||
}
|
||||
public string Name { get; }
|
||||
public ShogiBoard Board { get; }
|
||||
public string Player1 { get; }
|
||||
public string? Player2 { get; private set; }
|
||||
|
||||
public BoardState BoardState { get; }
|
||||
public string Name { 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.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();
|
||||
}
|
||||
public void AddPlayer2(string player2Name)
|
||||
{
|
||||
if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player.");
|
||||
Player2 = player2Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,251 +1,253 @@
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System.Collections.ObjectModel;
|
||||
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>
|
||||
public static readonly BoardState StandardStarting = new();
|
||||
/// <summary>
|
||||
/// Board state before any moves have been made, using standard setup and rules.
|
||||
/// </summary>
|
||||
public static readonly BoardState StandardStarting = new(
|
||||
state: BuildStandardStartingBoardState(),
|
||||
player1Hand: new(),
|
||||
player2Hand: new(),
|
||||
whoseTurn: WhichPlayer.Player1,
|
||||
playerInCheck: null,
|
||||
previousMove: new Move());
|
||||
|
||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||
/// <summary>
|
||||
/// Key is position notation, such as "E4".
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Piece?> board;
|
||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||
/// <summary>
|
||||
/// Key is position notation, such as "E4".
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
board = state;
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy constructor.
|
||||
/// </summary>
|
||||
public BoardState(BoardState other)
|
||||
{
|
||||
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()
|
||||
{
|
||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||
InitializeBoardState();
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
public ReadOnlyDictionary<string, Piece?> State => new(board);
|
||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.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 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 List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.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; }
|
||||
public Piece? this[string notation]
|
||||
{
|
||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||
get => board[notation];
|
||||
set => board[notation] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy constructor.
|
||||
/// </summary>
|
||||
public BoardState(BoardState other) : this()
|
||||
{
|
||||
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[Vector2 vector]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(vector)];
|
||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||
}
|
||||
|
||||
public Piece? this[string notation]
|
||||
{
|
||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||
get => board[notation];
|
||||
set => board[notation] = value;
|
||||
}
|
||||
public Piece? this[int x, int y]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(x, y)];
|
||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||
}
|
||||
|
||||
public Piece? this[Vector2 vector]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(vector)];
|
||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||
/// </summary>
|
||||
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]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(x, y)];
|
||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||
}
|
||||
internal bool IsWithinPromotionZone(Vector2 position)
|
||||
{
|
||||
// TODO: Move this promotion zone logic into the StandardRules class.
|
||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||
}
|
||||
|
||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
||||
{
|
||||
PreviousMoveFrom = from;
|
||||
PreviousMoveTo = to;
|
||||
}
|
||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||
{
|
||||
return position.X <= 8 && position.X >= 0
|
||||
&& position.Y <= 8 && position.Y >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||
/// </summary>
|
||||
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
||||
{
|
||||
return !path.Any()
|
||||
|| path.SkipLast(1).Any(position => this[position] != null)
|
||||
|| this[path.Last()]?.Owner == WhoseTurn;
|
||||
}
|
||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||
.ToList();
|
||||
|
||||
internal bool IsWithinPromotionZone(Vector2 position)
|
||||
{
|
||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||
}
|
||||
internal void Capture(Vector2 to)
|
||||
{
|
||||
var piece = this[to];
|
||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||
|
||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||
{
|
||||
return position.X <= 8 && position.X >= 0
|
||||
&& position.Y <= 8 && position.Y >= 0;
|
||||
}
|
||||
piece.Capture(WhoseTurn);
|
||||
ActivePlayerHand.Add(piece);
|
||||
}
|
||||
|
||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||
.ToList();
|
||||
/// <summary>
|
||||
/// Does not include the start position.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var piece = this[to];
|
||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||
{
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (this[step] != null) return this[step];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
piece.Capture(WhoseTurn);
|
||||
ActivePlayerHand.Add(piece);
|
||||
}
|
||||
private static Dictionary<string, Piece?> BuildStandardStartingBoardState()
|
||||
{
|
||||
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>
|
||||
/// Does not include the start position.
|
||||
/// </summary>
|
||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||
{
|
||||
var next = start;
|
||||
while (IsWithinBoardBoundary(next + direction))
|
||||
{
|
||||
next += direction;
|
||||
yield return next;
|
||||
}
|
||||
}
|
||||
["A2"] = null,
|
||||
["B2"] = new Bishop(WhichPlayer.Player1),
|
||||
["C2"] = null,
|
||||
["D2"] = null,
|
||||
["E2"] = null,
|
||||
["F2"] = null,
|
||||
["G2"] = null,
|
||||
["H2"] = new Rook(WhichPlayer.Player1),
|
||||
["I2"] = null,
|
||||
|
||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||
{
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (this[step] != null) return this[step];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
["A3"] = new Pawn(WhichPlayer.Player1),
|
||||
["B3"] = new Pawn(WhichPlayer.Player1),
|
||||
["C3"] = new Pawn(WhichPlayer.Player1),
|
||||
["D3"] = new Pawn(WhichPlayer.Player1),
|
||||
["E3"] = new Pawn(WhichPlayer.Player1),
|
||||
["F3"] = new Pawn(WhichPlayer.Player1),
|
||||
["G3"] = new Pawn(WhichPlayer.Player1),
|
||||
["H3"] = new Pawn(WhichPlayer.Player1),
|
||||
["I3"] = new Pawn(WhichPlayer.Player1),
|
||||
|
||||
private void InitializeBoardState()
|
||||
{
|
||||
this["A1"] = new Lance(WhichPlayer.Player1);
|
||||
this["B1"] = new Knight(WhichPlayer.Player1);
|
||||
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["E1"] = new King(WhichPlayer.Player1);
|
||||
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["H1"] = new Knight(WhichPlayer.Player1);
|
||||
this["I1"] = new Lance(WhichPlayer.Player1);
|
||||
["A4"] = null,
|
||||
["B4"] = null,
|
||||
["C4"] = null,
|
||||
["D4"] = null,
|
||||
["E4"] = null,
|
||||
["F4"] = null,
|
||||
["G4"] = null,
|
||||
["H4"] = null,
|
||||
["I4"] = null,
|
||||
|
||||
this["A2"] = null;
|
||||
this["B2"] = new Bishop(WhichPlayer.Player1);
|
||||
this["C2"] = null;
|
||||
this["D2"] = null;
|
||||
this["E2"] = null;
|
||||
this["F2"] = null;
|
||||
this["G2"] = null;
|
||||
this["H2"] = new Rook(WhichPlayer.Player1);
|
||||
this["I2"] = null;
|
||||
["A5"] = null,
|
||||
["B5"] = null,
|
||||
["C5"] = null,
|
||||
["D5"] = null,
|
||||
["E5"] = null,
|
||||
["F5"] = null,
|
||||
["G5"] = null,
|
||||
["H5"] = null,
|
||||
["I5"] = null,
|
||||
|
||||
this["A3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["B3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["C3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["D3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["E3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["F3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["G3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["H3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["I3"] = new Pawn(WhichPlayer.Player1);
|
||||
["A6"] = null,
|
||||
["B6"] = null,
|
||||
["C6"] = null,
|
||||
["D6"] = null,
|
||||
["E6"] = null,
|
||||
["F6"] = null,
|
||||
["G6"] = null,
|
||||
["H6"] = null,
|
||||
["I6"] = null,
|
||||
|
||||
this["A4"] = null;
|
||||
this["B4"] = null;
|
||||
this["C4"] = null;
|
||||
this["D4"] = null;
|
||||
this["E4"] = null;
|
||||
this["F4"] = null;
|
||||
this["G4"] = null;
|
||||
this["H4"] = null;
|
||||
this["I4"] = null;
|
||||
["A7"] = new Pawn(WhichPlayer.Player2),
|
||||
["B7"] = new Pawn(WhichPlayer.Player2),
|
||||
["C7"] = new Pawn(WhichPlayer.Player2),
|
||||
["D7"] = new Pawn(WhichPlayer.Player2),
|
||||
["E7"] = new Pawn(WhichPlayer.Player2),
|
||||
["F7"] = new Pawn(WhichPlayer.Player2),
|
||||
["G7"] = new Pawn(WhichPlayer.Player2),
|
||||
["H7"] = new Pawn(WhichPlayer.Player2),
|
||||
["I7"] = new Pawn(WhichPlayer.Player2),
|
||||
|
||||
this["A5"] = null;
|
||||
this["B5"] = null;
|
||||
this["C5"] = null;
|
||||
this["D5"] = null;
|
||||
this["E5"] = null;
|
||||
this["F5"] = null;
|
||||
this["G5"] = null;
|
||||
this["H5"] = null;
|
||||
this["I5"] = null;
|
||||
["A8"] = null,
|
||||
["B8"] = new Rook(WhichPlayer.Player2),
|
||||
["C8"] = null,
|
||||
["D8"] = null,
|
||||
["E8"] = null,
|
||||
["F8"] = null,
|
||||
["G8"] = null,
|
||||
["H8"] = new Bishop(WhichPlayer.Player2),
|
||||
["I8"] = null,
|
||||
|
||||
this["A6"] = null;
|
||||
this["B6"] = null;
|
||||
this["C6"] = null;
|
||||
this["D6"] = null;
|
||||
this["E6"] = null;
|
||||
this["F6"] = null;
|
||||
this["G6"] = null;
|
||||
this["H6"] = null;
|
||||
this["I6"] = null;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
["A9"] = new Lance(WhichPlayer.Player2),
|
||||
["B9"] = new Knight(WhichPlayer.Player2),
|
||||
["C9"] = new SilverGeneral(WhichPlayer.Player2),
|
||||
["D9"] = new GoldGeneral(WhichPlayer.Player2),
|
||||
["E9"] = new King(WhichPlayer.Player2),
|
||||
["F9"] = new GoldGeneral(WhichPlayer.Player2),
|
||||
["G9"] = new SilverGeneral(WhichPlayer.Player2),
|
||||
["H9"] = new Knight(WhichPlayer.Player2),
|
||||
["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>;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
internal class StandardRules
|
||||
internal class StandardRules
|
||||
{
|
||||
private readonly BoardState boardState;
|
||||
|
||||
@@ -55,7 +54,7 @@ namespace Shogi.Domain
|
||||
boardState[to] = fromPiece;
|
||||
boardState[from] = null;
|
||||
|
||||
boardState.RememberAsMostRecentMove(from, to);
|
||||
boardState.PreviousMove = new Move(from, to);
|
||||
var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||
boardState.WhoseTurn = otherPlayer;
|
||||
|
||||
@@ -121,16 +120,16 @@ namespace Shogi.Domain
|
||||
/// </remarks>
|
||||
internal bool IsPlayerInCheckAfterMove()
|
||||
{
|
||||
var previousMovedPiece = boardState[boardState.PreviousMoveTo];
|
||||
if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMoveTo}.");
|
||||
var previousMovedPiece = boardState[boardState.PreviousMove.To];
|
||||
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 isCheck = false;
|
||||
|
||||
// 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 path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction));
|
||||
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From, Vector2.Normalize(direction));
|
||||
var threat = boardState.QueryFirstPieceInPath(path);
|
||||
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
||||
// If absolute slope is 45°, look for a bishop along the line.
|
||||
@@ -165,7 +164,7 @@ namespace Shogi.Domain
|
||||
return isCheck;
|
||||
}
|
||||
|
||||
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo);
|
||||
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To);
|
||||
|
||||
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}")]
|
||||
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)
|
||||
{
|
||||
return piece switch
|
||||
@@ -54,7 +50,7 @@ namespace Shogi.Domain.ValueObjects
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <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 ISessionRepository sessionRepository;
|
||||
private readonly IQueryRespository queryRespository;
|
||||
private readonly ILogger<SessionController> logger;
|
||||
|
||||
public SessionController(
|
||||
ISocketConnectionManager communicationManager,
|
||||
IModelMapper mapper,
|
||||
ISessionRepository sessionRepository,
|
||||
IQueryRespository queryRespository)
|
||||
IQueryRespository queryRespository,
|
||||
ILogger<SessionController> logger)
|
||||
{
|
||||
this.communicationManager = communicationManager;
|
||||
this.mapper = mapper;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.queryRespository = queryRespository;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -37,13 +40,17 @@ public class SessionController : ControllerBase
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
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
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
/// Useful with Dapper to read from database.
|
||||
/// </summary>
|
||||
public class SessionDto
|
||||
{
|
||||
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; }
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Player1 { get; set; }
|
||||
public string Player2 { get; set; }
|
||||
public BoardStateDto 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",
|
||||
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
|
||||
{
|
||||
Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata();
|
||||
Task<SessionMetadata?> ReadSession(string name);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using Dapper;
|
||||
using Shogi.Api.Repositories.Dto;
|
||||
using Shogi.Domain;
|
||||
using Shogi.Domain.Aggregates;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Text.Json;
|
||||
@@ -16,22 +18,53 @@ public class SessionRepository : ISessionRepository
|
||||
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);
|
||||
await connection.ExecuteAsync(
|
||||
"session.CreateSession",
|
||||
new
|
||||
{
|
||||
InitialBoardStateDocument = initialBoardState,
|
||||
Name = sessionName,
|
||||
InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto),
|
||||
Player1Name = player1,
|
||||
},
|
||||
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
|
||||
{
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
|
||||
board["A4"].Should().BeNull();
|
||||
@@ -33,7 +33,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void AllowValidMoves_AfterCheck()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Pawn
|
||||
shogi.Move("C3", "C4", false);
|
||||
@@ -58,7 +58,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
board["D5"].Should().BeNull();
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_MoveToCurrentPosition()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var expectedPiece = board["A3"];
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_MoveSet()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var expectedPiece = board["A1"];
|
||||
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||
@@ -121,7 +121,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_Ownership()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var expectedPiece = board["A7"];
|
||||
expectedPiece!.Owner.Should().Be(WhichPlayer.Player2);
|
||||
@@ -143,7 +143,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_MoveThroughAllies()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var lance = board["A1"];
|
||||
var pawn = board["A3"];
|
||||
@@ -166,7 +166,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_CaptureAlly()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var knight = board["B1"];
|
||||
var pawn = board["C3"];
|
||||
@@ -190,7 +190,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidMoves_Check()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Pawn
|
||||
shogi.Move("C3", "C4", false);
|
||||
@@ -219,7 +219,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void PreventInvalidDrops_MoveSet()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Pawn
|
||||
shogi.Move("C3", "C4", false);
|
||||
@@ -358,7 +358,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void Check()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Pawn
|
||||
shogi.Move("C3", "C4", false);
|
||||
@@ -376,7 +376,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void Promote()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Pawn
|
||||
shogi.Move("C3", "C4", false);
|
||||
@@ -401,7 +401,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void Capture()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
var p1Bishop = board["B2"];
|
||||
p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop);
|
||||
@@ -425,7 +425,7 @@ namespace Shogi.Domain.UnitTests
|
||||
public void CheckMate()
|
||||
{
|
||||
// Arrange
|
||||
var shogi = MockSession();
|
||||
var shogi = MockShogiBoard();
|
||||
var board = shogi.BoardState;
|
||||
// P1 Rook
|
||||
shogi.Move("H2", "E2", false);
|
||||
@@ -457,6 +457,6 @@ namespace Shogi.Domain.UnitTests
|
||||
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