checkpoint
This commit is contained in:
@@ -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
33
Shogi.Domain/Notation.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
26
Shogi.Domain/SessionMetadata.cs
Normal file
26
Shogi.Domain/SessionMetadata.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user