This commit is contained in:
2022-11-09 08:56:54 -06:00
parent 2241ab23fe
commit 3257b420e9
17 changed files with 601 additions and 538 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1,11 +1,11 @@
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)

View File

@@ -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" />

View File

@@ -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,
public ShogiBoard(string name, BoardState initialState, string player1, string? player2 = null) string player1Name,
ShogiBoard board)
{ {
Name = name; Name = name;
Player1 = player1; Player1 = player1Name;
Player2 = player2; Board = board;
BoardState = initialState;
rules = new StandardRules(BoardState);
} }
public BoardState BoardState { get; }
public string Name { get; } public string Name { get; }
public ShogiBoard Board { get; }
public string Player1 { get; }
public string? Player2 { get; private set; }
/// <summary> public void AddPlayer2(string 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); if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player.");
var simulation = new StandardRules(simulationState); Player2 = player2Name;
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();
} }
} }

View File

@@ -1,14 +1,21 @@
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> /// <summary>
/// Board state before any moves have been made, using standard setup and rules. /// Board state before any moves have been made, using standard setup and rules.
/// </summary> /// </summary>
public static readonly BoardState StandardStarting = new(); 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); public delegate void ForEachDelegate(Piece element, Vector2 position);
/// <summary> /// <summary>
@@ -16,25 +23,42 @@ namespace Shogi.Domain
/// </summary> /// </summary>
private readonly Dictionary<string, Piece?> board; private readonly Dictionary<string, Piece?> board;
public BoardState(
public BoardState(Dictionary<string, Piece?> state) Dictionary<string, Piece?> state,
List<Piece> player1Hand,
List<Piece> player2Hand,
WhichPlayer whoseTurn,
WhichPlayer? playerInCheck,
Move previousMove)
{ {
board = state; board = state;
Player1Hand = new List<Piece>(); Player1Hand = player1Hand;
Player2Hand = new List<Piece>(); Player2Hand = player2Hand;
PreviousMoveTo = Vector2.Zero; PreviousMove = previousMove;
WhoseTurn = whoseTurn;
InCheck = playerInCheck;
} }
public BoardState() /// <summary>
/// Copy constructor.
/// </summary>
public BoardState(BoardState other)
{ {
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase); board = new(81);
InitializeBoardState(); foreach (var kvp in other.board)
Player1Hand = new List<Piece>(); {
Player2Hand = new List<Piece>(); var piece = kvp.Value;
PreviousMoveTo = Vector2.Zero; 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 Dictionary<string, Piece?> State => board; public ReadOnlyDictionary<string, Piece?> State => new(board);
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
{ {
@@ -48,30 +72,11 @@ namespace Shogi.Domain
}).Key); }).Key);
public List<Piece> Player1Hand { get; } public List<Piece> Player1Hand { get; }
public List<Piece> Player2Hand { get; } public List<Piece> Player2Hand { get; }
public Vector2 PreviousMoveFrom { get; private set; } public Move PreviousMove { get; set; }
public Vector2 PreviousMoveTo { get; private set; }
public WhichPlayer WhoseTurn { get; set; } public WhichPlayer WhoseTurn { get; set; }
public WhichPlayer? InCheck { get; set; } public WhichPlayer? InCheck { get; set; }
public bool IsCheckmate { get; set; } public bool IsCheckmate { get; set; }
/// <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[string notation] public Piece? this[string notation]
{ {
// TODO: Validate "notation" here and throw an exception if invalid. // TODO: Validate "notation" here and throw an exception if invalid.
@@ -91,12 +96,6 @@ namespace Shogi.Domain
set => this[Notation.ToBoardNotation(x, y)] = value; set => this[Notation.ToBoardNotation(x, y)] = value;
} }
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
{
PreviousMoveFrom = from;
PreviousMoveTo = to;
}
/// <summary> /// <summary>
/// Returns true if the given path can be traversed without colliding into a piece. /// Returns true if the given path can be traversed without colliding into a piece.
/// </summary> /// </summary>
@@ -109,6 +108,7 @@ namespace Shogi.Domain
internal bool IsWithinPromotionZone(Vector2 position) internal bool IsWithinPromotionZone(Vector2 position)
{ {
// TODO: Move this promotion zone logic into the StandardRules class.
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
} }
@@ -155,97 +155,99 @@ namespace Shogi.Domain
return null; return null;
} }
private void InitializeBoardState() private static Dictionary<string, Piece?> BuildStandardStartingBoardState()
{ {
this["A1"] = new Lance(WhichPlayer.Player1); return new Dictionary<string, Piece?>(81)
this["B1"] = new Knight(WhichPlayer.Player1); {
this["C1"] = new SilverGeneral(WhichPlayer.Player1); ["A1"] = new Lance(WhichPlayer.Player1),
this["D1"] = new GoldGeneral(WhichPlayer.Player1); ["B1"] = new Knight(WhichPlayer.Player1),
this["E1"] = new King(WhichPlayer.Player1); ["C1"] = new SilverGeneral(WhichPlayer.Player1),
this["F1"] = new GoldGeneral(WhichPlayer.Player1); ["D1"] = new GoldGeneral(WhichPlayer.Player1),
this["G1"] = new SilverGeneral(WhichPlayer.Player1); ["E1"] = new King(WhichPlayer.Player1),
this["H1"] = new Knight(WhichPlayer.Player1); ["F1"] = new GoldGeneral(WhichPlayer.Player1),
this["I1"] = new Lance(WhichPlayer.Player1); ["G1"] = new SilverGeneral(WhichPlayer.Player1),
["H1"] = new Knight(WhichPlayer.Player1),
["I1"] = new Lance(WhichPlayer.Player1),
this["A2"] = null; ["A2"] = null,
this["B2"] = new Bishop(WhichPlayer.Player1); ["B2"] = new Bishop(WhichPlayer.Player1),
this["C2"] = null; ["C2"] = null,
this["D2"] = null; ["D2"] = null,
this["E2"] = null; ["E2"] = null,
this["F2"] = null; ["F2"] = null,
this["G2"] = null; ["G2"] = null,
this["H2"] = new Rook(WhichPlayer.Player1); ["H2"] = new Rook(WhichPlayer.Player1),
this["I2"] = null; ["I2"] = null,
this["A3"] = new Pawn(WhichPlayer.Player1); ["A3"] = new Pawn(WhichPlayer.Player1),
this["B3"] = new Pawn(WhichPlayer.Player1); ["B3"] = new Pawn(WhichPlayer.Player1),
this["C3"] = new Pawn(WhichPlayer.Player1); ["C3"] = new Pawn(WhichPlayer.Player1),
this["D3"] = new Pawn(WhichPlayer.Player1); ["D3"] = new Pawn(WhichPlayer.Player1),
this["E3"] = new Pawn(WhichPlayer.Player1); ["E3"] = new Pawn(WhichPlayer.Player1),
this["F3"] = new Pawn(WhichPlayer.Player1); ["F3"] = new Pawn(WhichPlayer.Player1),
this["G3"] = new Pawn(WhichPlayer.Player1); ["G3"] = new Pawn(WhichPlayer.Player1),
this["H3"] = new Pawn(WhichPlayer.Player1); ["H3"] = new Pawn(WhichPlayer.Player1),
this["I3"] = new Pawn(WhichPlayer.Player1); ["I3"] = new Pawn(WhichPlayer.Player1),
this["A4"] = null; ["A4"] = null,
this["B4"] = null; ["B4"] = null,
this["C4"] = null; ["C4"] = null,
this["D4"] = null; ["D4"] = null,
this["E4"] = null; ["E4"] = null,
this["F4"] = null; ["F4"] = null,
this["G4"] = null; ["G4"] = null,
this["H4"] = null; ["H4"] = null,
this["I4"] = null; ["I4"] = null,
this["A5"] = null; ["A5"] = null,
this["B5"] = null; ["B5"] = null,
this["C5"] = null; ["C5"] = null,
this["D5"] = null; ["D5"] = null,
this["E5"] = null; ["E5"] = null,
this["F5"] = null; ["F5"] = null,
this["G5"] = null; ["G5"] = null,
this["H5"] = null; ["H5"] = null,
this["I5"] = null; ["I5"] = null,
this["A6"] = null; ["A6"] = null,
this["B6"] = null; ["B6"] = null,
this["C6"] = null; ["C6"] = null,
this["D6"] = null; ["D6"] = null,
this["E6"] = null; ["E6"] = null,
this["F6"] = null; ["F6"] = null,
this["G6"] = null; ["G6"] = null,
this["H6"] = null; ["H6"] = null,
this["I6"] = null; ["I6"] = null,
this["A7"] = new Pawn(WhichPlayer.Player2); ["A7"] = new Pawn(WhichPlayer.Player2),
this["B7"] = new Pawn(WhichPlayer.Player2); ["B7"] = new Pawn(WhichPlayer.Player2),
this["C7"] = new Pawn(WhichPlayer.Player2); ["C7"] = new Pawn(WhichPlayer.Player2),
this["D7"] = new Pawn(WhichPlayer.Player2); ["D7"] = new Pawn(WhichPlayer.Player2),
this["E7"] = new Pawn(WhichPlayer.Player2); ["E7"] = new Pawn(WhichPlayer.Player2),
this["F7"] = new Pawn(WhichPlayer.Player2); ["F7"] = new Pawn(WhichPlayer.Player2),
this["G7"] = new Pawn(WhichPlayer.Player2); ["G7"] = new Pawn(WhichPlayer.Player2),
this["H7"] = new Pawn(WhichPlayer.Player2); ["H7"] = new Pawn(WhichPlayer.Player2),
this["I7"] = new Pawn(WhichPlayer.Player2); ["I7"] = new Pawn(WhichPlayer.Player2),
this["A8"] = null; ["A8"] = null,
this["B8"] = new Rook(WhichPlayer.Player2); ["B8"] = new Rook(WhichPlayer.Player2),
this["C8"] = null; ["C8"] = null,
this["D8"] = null; ["D8"] = null,
this["E8"] = null; ["E8"] = null,
this["F8"] = null; ["F8"] = null,
this["G8"] = null; ["G8"] = null,
this["H8"] = new Bishop(WhichPlayer.Player2); ["H8"] = new Bishop(WhichPlayer.Player2),
this["I8"] = null; ["I8"] = null,
this["A9"] = new Lance(WhichPlayer.Player2); ["A9"] = new Lance(WhichPlayer.Player2),
this["B9"] = new Knight(WhichPlayer.Player2); ["B9"] = new Knight(WhichPlayer.Player2),
this["C9"] = new SilverGeneral(WhichPlayer.Player2); ["C9"] = new SilverGeneral(WhichPlayer.Player2),
this["D9"] = new GoldGeneral(WhichPlayer.Player2); ["D9"] = new GoldGeneral(WhichPlayer.Player2),
this["E9"] = new King(WhichPlayer.Player2); ["E9"] = new King(WhichPlayer.Player2),
this["F9"] = new GoldGeneral(WhichPlayer.Player2); ["F9"] = new GoldGeneral(WhichPlayer.Player2),
this["G9"] = new SilverGeneral(WhichPlayer.Player2); ["G9"] = new SilverGeneral(WhichPlayer.Player2),
this["H9"] = new Knight(WhichPlayer.Player2); ["H9"] = new Knight(WhichPlayer.Player2),
this["I9"] = new Lance(WhichPlayer.Player2); ["I9"] = new Lance(WhichPlayer.Player2)
} };
} }
} }

View File

@@ -1,5 +1,4 @@
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
@@ -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)
{ {

View 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)
{
}

View File

@@ -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>

View 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();
}
}

View File

@@ -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();
} }

View 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.

View File

@@ -1,14 +1,14 @@
namespace Shogi.Api.Repositories.Dto namespace Shogi.Api.Repositories.Dto;
{
/// <summary> /// <summary>
/// Useful with Dapper to read from database. /// Useful with Dapper to read from database.
/// </summary> /// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public class SessionDto public class SessionDto
{ {
public string Name { get; set; } public string Name { get; set; }
public string Player1 { get; set; } public string Player1 { get; set; }
public string Player2 { get; set; } public string Player2 { get; set; }
public bool GameOver { get; set; } public BoardStateDto BoardState { 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.

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);
} }
} }