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,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;
}
}

View File

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

View File

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

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}")]
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>

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