checkpoint

This commit is contained in:
2022-06-12 12:37:32 -05:00
parent 2dcc6ca417
commit 4ca0b63564
43 changed files with 563 additions and 2128 deletions

View File

@@ -1,22 +1,26 @@
using Shogi.Domain.Pieces;
using System.Text.RegularExpressions;
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
namespace Shogi.Domain
{
// TODO: Avoid extending dictionary. Use composition instead.
// Then validation can occur when assigning a piece to a position.
public class ShogiBoardState
public class BoardState
{
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
private static readonly char A = 'A';
public delegate void ForEachDelegate(Piece element, Vector2 position);
/// <summary>
/// Key is position notation, such as "E4".
/// </summary>
private readonly Dictionary<string, Piece?> board;
public ShogiBoardState()
public BoardState(Dictionary<string, Piece?> state)
{
board = state;
Player1Hand = new List<Piece>();
Player2Hand = new List<Piece>();
PreviousMoveTo = Vector2.Zero;
}
public BoardState()
{
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
InitializeBoardState();
@@ -25,13 +29,14 @@ namespace Shogi.Domain
PreviousMoveTo = Vector2.Zero;
}
public Dictionary<string, Piece?> State => board;
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
public Vector2 Player1KingPosition => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
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 => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
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;
@@ -47,12 +52,12 @@ namespace Shogi.Domain
/// <summary>
/// Copy constructor.
/// </summary>
public ShogiBoardState(ShogiBoardState other) : this()
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.Create(kvp.Value);
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
}
WhoseTurn = other.WhoseTurn;
InCheck = other.InCheck;
@@ -71,14 +76,14 @@ namespace Shogi.Domain
public Piece? this[Vector2 vector]
{
get => this[ToBoardNotation(vector)];
set => this[ToBoardNotation(vector)] = value;
get => this[Notation.ToBoardNotation(vector)];
set => this[Notation.ToBoardNotation(vector)] = value;
}
public Piece? this[int x, int y]
{
get => this[ToBoardNotation(x, y)];
set => this[ToBoardNotation(x, y)] = value;
get => this[Notation.ToBoardNotation(x, y)];
set => this[Notation.ToBoardNotation(x, y)] = value;
}
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
@@ -111,7 +116,7 @@ namespace Shogi.Domain
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
.Where(kvp => kvp.Value?.Owner == whichPlayer)
.Select(kvp => new BoardTile(FromBoardNotation(kvp.Key), kvp.Value!))
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
.ToList();
internal void Capture(Vector2 to)
@@ -144,27 +149,6 @@ namespace Shogi.Domain
}
return null;
}
public static string ToBoardNotation(Vector2 vector)
{
return ToBoardNotation((int)vector.X, (int)vector.Y);
}
private static string ToBoardNotation(int x, int y)
{
var file = (char)(x + A);
var rank = y + 1;
return $"{file}{rank}";
}
public static Vector2 FromBoardNotation(string notation)
{
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank - 1);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
}
private void InitializeBoardState()
{

33
Shogi.Domain/Notation.cs Normal file
View File

@@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
namespace Shogi.Domain
{
public static class Notation
{
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
private static readonly char A = 'A';
public static string ToBoardNotation(Vector2 vector)
{
return ToBoardNotation((int)vector.X, (int)vector.Y);
}
public static string ToBoardNotation(int x, int y)
{
var file = (char)(x + A);
var rank = y + 1;
return $"{file}{rank}";
}
public static Vector2 FromBoardNotation(string notation)
{
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank - 1);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Shogi.Domain.Pieces
/// <summary>
/// Creates a clone of an existing piece.
/// </summary>
public static Piece Create(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
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
@@ -25,8 +25,6 @@ namespace Shogi.Domain.Pieces
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
};
}
// TODO: MoveSet doesn't account for Player2's pieces which are upside-down.
public abstract IEnumerable<Path> MoveSet { get; }
public WhichPiece WhichPiece { get; }
public WhichPlayer Owner { get; private set; }

View File

@@ -8,21 +8,38 @@ namespace Shogi.Domain
/// 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 Shogi
public sealed class Session
{
private readonly ShogiBoardState boardState;
private readonly Tuple<string, string> Players;
private readonly BoardState boardState;
private readonly StandardRules rules;
private readonly SessionMetadata metadata;
public Shogi() : this(new ShogiBoardState())
public Session() : this(new BoardState())
{
}
public Shogi(ShogiBoardState board)
public Session(BoardState state) : this(state, new SessionMetadata(string.Empty, false, string.Empty, string.Empty))
{
this.rules = new StandardRules(board);
this.boardState = board;
}
public Session(BoardState state, SessionMetadata metadata)
{
rules = new StandardRules(state);
boardState = state;
this.metadata = metadata;
Players = new(string.Empty, string.Empty);
}
public string Name => metadata.Name;
public string Player1Name => metadata.Player1;
public string? Player2Name => metadata.Player2;
public IDictionary<string, Piece?> BoardState => boardState.State;
public IList<Piece> Player1Hand => boardState.Player1Hand;
public IList<Piece> Player2Hand => boardState.Player2Hand;
public WhichPlayer? InCheck => boardState.InCheck;
public bool IsCheckMate => boardState.IsCheckmate;
/// <summary>
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
/// </summary>
@@ -33,7 +50,7 @@ namespace Shogi.Domain
/// <exception cref="InvalidOperationException"></exception>
public void Move(string from, string to, bool isPromotion)
{
var simulationState = new ShogiBoardState(boardState);
var simulationState = new BoardState(boardState);
var simulation = new StandardRules(simulationState);
var moveResult = simulation.Move(from, to, isPromotion);
if (!moveResult.Success)
@@ -83,7 +100,7 @@ namespace Shogi.Domain
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
}
var toVector = ShogiBoardState.FromBoardNotation(to);
var toVector = Notation.FromBoardNotation(to);
switch (pieceInHand)
{
case WhichPiece.Knight:
@@ -109,7 +126,7 @@ namespace Shogi.Domain
}
}
var tempBoard = new ShogiBoardState(boardState);
var tempBoard = new BoardState(boardState);
var simulation = new StandardRules(tempBoard);
var moveResult = simulation.Move(pieceInHand, to);
if (!moveResult.Success)

View File

@@ -0,0 +1,26 @@
namespace Shogi.Domain
{
/// <summary>
/// A representation of a Session without the board and game-rules.
/// </summary>
public class SessionMetadata
{
public string Name { get; }
public string Player1 { get; }
public string? Player2 { get; private set; }
public bool IsPrivate { get; }
public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null)
{
Name = name;
IsPrivate = isPrivate;
Player1 = player1;
Player2 = player2;
}
public void SetPlayer2(string user)
{
Player2 = user;
}
}
}

View File

@@ -6,9 +6,9 @@ namespace Shogi.Domain
{
internal class StandardRules
{
private readonly ShogiBoardState boardState;
private readonly BoardState boardState;
internal StandardRules(ShogiBoardState board)
internal StandardRules(BoardState board)
{
boardState = board;
}
@@ -22,8 +22,8 @@ namespace Shogi.Domain
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false)
{
var from = ShogiBoardState.FromBoardNotation(fromNotation);
var to = ShogiBoardState.FromBoardNotation(toNotation);
var from = Notation.FromBoardNotation(fromNotation);
var to = Notation.FromBoardNotation(toNotation);
var fromPiece = boardState[from];
if (fromPiece == null)
{
@@ -69,7 +69,7 @@ namespace Shogi.Domain
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
internal MoveResult Move(WhichPiece pieceInHand, string toNotation)
{
var to = ShogiBoardState.FromBoardNotation(toNotation);
var to = Notation.FromBoardNotation(toNotation);
var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
if (index == -1)
{
@@ -130,7 +130,7 @@ namespace Shogi.Domain
// Get line equation from king through the now-unoccupied location.
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom);
var slope = Math.Abs(direction.Y / direction.X);
var path = ShogiBoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction));
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, 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.
@@ -253,7 +253,7 @@ namespace Shogi.Domain
var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget);
return path
.SkipLast(1)
.All(position => boardState[ShogiBoardState.ToBoardNotation(position)] == null);
.All(position => boardState[Notation.ToBoardNotation(position)] == null);
}
}
}