diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index b7c58fe..f9ce7f9 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,11 +1,10 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Text; using System.Text.RegularExpressions; -using Domain = Shogi.Domain; namespace Gameboard.ShogiUI.Sockets.Extensions { - public static class ModelExtensions + public static class ModelExtensions { public static string GetShortName(this Models.Piece self) { diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index cb857d0..e0c7407 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -24,9 +24,7 @@ using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using System.Text; -using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets { diff --git a/Shogi.Domain/BoardTile.cs b/Shogi.Domain/BoardTile.cs deleted file mode 100644 index 4662e01..0000000 --- a/Shogi.Domain/BoardTile.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Shogi.Domain.Pieces; - -namespace Shogi.Domain -{ - internal class BoardTile - { - public BoardTile(Piece piece, Vector2 position) - { - Piece = piece; - Position = position; - } - - public Piece Piece { get; } - - public Vector2 Position { get; } - } -} diff --git a/Shogi.Domain/DomainExtensions.cs b/Shogi.Domain/DomainExtensions.cs new file mode 100644 index 0000000..7471221 --- /dev/null +++ b/Shogi.Domain/DomainExtensions.cs @@ -0,0 +1,10 @@ +using Shogi.Domain.Pieces; + +namespace Shogi.Domain +{ + internal static class DomainExtensions + { + public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; + + } +} diff --git a/Shogi.Domain/Pieces/Piece.cs b/Shogi.Domain/Pieces/Piece.cs index 832aefb..6a54cfe 100644 --- a/Shogi.Domain/Pieces/Piece.cs +++ b/Shogi.Domain/Pieces/Piece.cs @@ -55,6 +55,13 @@ namespace Shogi.Domain.Pieces IsPromoted = false; } + /// + /// Respecting the move-set of the Piece, collect all positions from start to end. + /// Useful if you need to iterate a move-set. + /// + /// + /// + /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions. public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) { var steps = new List(10); diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index 5b16827..f4e6bd1 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -1,5 +1,4 @@ using Shogi.Domain.Pieces; -using System; using System.Text; namespace Shogi.Domain @@ -20,8 +19,8 @@ namespace Shogi.Domain public Shogi(ShogiBoardState board) { + this.rules = new StandardRules(board); this.boardState = board; - rules = new StandardRules(this.boardState); } /// diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index a678ff7..69c1645 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -1,5 +1,6 @@ using Shogi.Domain.Pieces; using System.Text.RegularExpressions; +using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { @@ -15,25 +16,6 @@ namespace Shogi.Domain /// private readonly Dictionary board; - public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - /// - /// "Active Player" means the player whose turn it is. - /// - public Vector2 ActivePlayerKingPosition => WhoseTurn == WhichPlayer.Player1 ? Player1KingPosition : Player2KingPosition; - /// - /// "Opposing Player" means the player whose turn it isn't. - /// - public Vector2 OpposingPlayerKingPosition => WhoseTurn == WhichPlayer.Player1 ? Player2KingPosition : Player1KingPosition; - public Vector2 Player1KingPosition { get; set; } - public Vector2 Player2KingPosition { get; set; } - public List Player1Hand { get; } - public List 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 ShogiBoardState() { board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); @@ -41,9 +23,26 @@ namespace Shogi.Domain Player1Hand = new List(); Player2Hand = new List(); PreviousMoveTo = Vector2.Zero; - CacheKingPositions(); } + public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public Vector2 Player1KingPosition => 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 => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; + }).Key); + public List Player1Hand { get; } + public List 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; } /// /// Copy constructor. @@ -61,30 +60,13 @@ namespace Shogi.Domain PreviousMoveTo = other.PreviousMoveTo; Player1Hand.AddRange(other.Player1Hand); Player2Hand.AddRange(other.Player2Hand); - Player1KingPosition = other.Player1KingPosition; - Player2KingPosition = other.Player2KingPosition; } public Piece? this[string notation] { // TODO: Validate "notation" here and throw an exception if invalid. get => board[notation]; - set - { - if (value?.WhichPiece == WhichPiece.King) - { - if (value.Owner == WhichPlayer.Player1) - { - // TODO: This FromBoardNotation() is a waste if the Vector2 indexer was called. :( - Player1KingPosition = FromBoardNotation(notation); - } - else if (value.Owner == WhichPlayer.Player2) - { - Player2KingPosition = FromBoardNotation(notation); - } - } - board[notation] = value; - } + set => board[notation] = value; } public Piece? this[Vector2 vector] @@ -112,21 +94,7 @@ namespace Shogi.Domain { return !path.Any() || path.SkipLast(1).Any(position => this[position] != null) - || this[path.Last()]?.Owner == WhoseTurn; - } - - public void ForEachNotNull(ForEachDelegate callback) - { - for (var x = 0; x < 9; x++) - { - for (var y = 0; y < 9; y++) - { - var position = new Vector2(x, y); - var elem = this[position]; - if (elem != null) - callback(elem, position); - } - } + || this[path.Last()]?.Owner == WhoseTurn; } internal bool IsWithinPromotionZone(Vector2 position) @@ -141,9 +109,10 @@ namespace Shogi.Domain && position.Y <= 8 && position.Y >= 0; } - internal IEnumerable GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(kvp.Value!, FromBoardNotation(kvp.Key))); + .Select(kvp => new BoardTile(FromBoardNotation(kvp.Key), kvp.Value!)) + .ToList(); internal void Capture(Vector2 to) { @@ -167,7 +136,7 @@ namespace Shogi.Domain } } - internal Piece? GetFirstPieceAlongPath(IEnumerable path) + internal Piece? QueryFirstPieceInPath(IEnumerable path) { foreach (var step in path) { @@ -197,24 +166,6 @@ namespace Shogi.Domain throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); } - private void CacheKingPositions() - { - ForEachNotNull((tile, position) => - { - if (tile.WhichPiece == WhichPiece.King) - { - if (tile.Owner == WhichPlayer.Player1) - { - Player1KingPosition = position; - } - else if (tile.Owner == WhichPlayer.Player2) - { - Player2KingPosition = position; - } - } - }); - } - private void InitializeBoardState() { this["A1"] = new Lance(WhichPlayer.Player1); diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index ac11ff7..3352424 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -1,6 +1,5 @@ -using System.Runtime.CompilerServices; +using BoardTile = System.Collections.Generic.KeyValuePair; -[assembly: InternalsVisibleTo("Shogi.Domain.UnitTests")] namespace Shogi.Domain { internal class StandardRules @@ -130,7 +129,7 @@ namespace Shogi.Domain 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 threat = boardState.GetFirstPieceAlongPath(path); + 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. // If absolute slope is 0° or 90°, look for a rook along the line. @@ -173,7 +172,7 @@ namespace Shogi.Domain var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition; var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition); - var threatenedPiece = boardState.GetFirstPieceAlongPath(path); + var threatenedPiece = boardState.QueryFirstPieceInPath(path); if (!path.Any() || threatenedPiece == null) return false; return threatenedPiece.WhichPiece == WhichPiece.King; @@ -183,32 +182,44 @@ namespace Shogi.Domain { if (!boardState.InCheck.HasValue) return false; - // Get all pieces from "other player" who threaten the king in question. var otherPlayer = whichPlayer == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var tilesOccupiedByOtherPlayer = boardState.GetTilesOccupiedBy(otherPlayer); + var kingPosition = whichPlayer == WhichPlayer.Player1 + ? boardState.Player1KingPosition + : boardState.Player2KingPosition; + var threats = tilesOccupiedByOtherPlayer.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList(); - if (tilesOccupiedByOtherPlayer.SingleOrDefault() != default) + + if (threats.Count == 1) { /* If there is exactly one threat it is possible to block the check. * Foreach piece owned by whichPlayer * if piece can intercept check, return false; */ - var threat = tilesOccupiedByOtherPlayer.Single(); - var kingPosition = whichPlayer == WhichPlayer.Player1 - ? boardState.Player1KingPosition - : boardState.Player2KingPosition; - var tiles = boardState.GetTilesOccupiedBy(whichPlayer); - var line = Vector2.Subtract(kingPosition, threat.Position); - var slope = line.Y / line.X; - foreach (var tile in tiles) + var threat = threats.Single(); + var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition); + var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(whichPlayer); + foreach (var threatBlockingPosition in pathFromThreatToKing) { - // y = mx + b; slope intercept - // b = -mx + y; - var b = -slope * tile.Position.X + tile.Position.Y; - //if (tile.Position.Y = slope * tile.Position.X + ) - } + var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat + .Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition)) + .ToList(); + if (tilesThatCouldBlockTheThreat.Any()) + { + return false; // Cannot be check-mate if a piece can intercept the threat. + } + } + //var line = Vector2.Subtract(kingPosition, threat.Position); + //var slope = line.Y / line.X; + //foreach (var tile in tilesThatCouldBlockTheThreat) + //{ + // // y = mx + b; slope intercept + // // b = -mx + y; + // var b = -slope * tile.Position.X + tile.Position.Y; + // //if (tile.Position.Y = slope * tile.Position.X + ) + //} } /* If no ability to block the check, maybe the king can evade check by moving. @@ -217,34 +228,15 @@ namespace Shogi.Domain * Foreach piece owned by "other player", check if piece threatens king position. */ - // return false; + } - - - - - - //foreach (var kvp in boardState) - //{ - // if (kvp.Value == null) continue; - // var position = ShogiBoardState.FromBoardNotation(kvp.Key); - // var piece = kvp.Value; - // foreach (var path in piece.MoveSet) - // { - // if (path.Distance == Distance.OneStep) - // { - // var move = path.Direction + position; - // var simulationState = new ShogiBoardState(boardState); - // var simulation = new Shogi(simulationState); - // simulation.Move(position, move); - // } - // else if (path.Distance == Distance.MultiStep) - // { - - // } - // } - //} + private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) + { + var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); + return path + .SkipLast(1) + .All(position => this.boardState[ShogiBoardState.ToBoardNotation(position)] == null); } public bool EvaluateCheckmate_Old() @@ -253,31 +245,31 @@ namespace Shogi.Domain // Assume true and try to disprove. var isCheckmate = true; - boardState.ForEachNotNull((piece, from) => // For each piece... - { - // Short circuit - if (!isCheckmate) return; + //boardState.ForEachNotNull((piece, from) => // For each piece... + //{ + // Short circuit + //if (!isCheckmate) return; - if (piece.Owner == boardState.InCheck) // ...owned by the player in check... - { - // ...evaluate if any move gets the player out of check. - //PathEvery(from, (other, position) => - //{ - // var simulationBoard = new StandardRules(new ShogiBoardState(board)); - // var fromNotation = ShogiBoardState.ToBoardNotation(from); - // var toNotation = ShogiBoardState.ToBoardNotation(position); - // var simulationResult = simulationBoard.Move(fromNotation, toNotation, false); - // if (simulationResult.Success) - // { - // //if (!IsPlayerInCheckAfterMove(from, position, board.InCheck.Value)) - // //{ - // // isCheckmate = false; - // //} - // } - //}); - } - // TODO: Assert that a player could not place a piece from their hand to avoid check. - }); + //if (piece.Owner == boardState.InCheck) // ...owned by the player in check... + //{ + // ...evaluate if any move gets the player out of check. + //PathEvery(from, (other, position) => + //{ + // var simulationBoard = new StandardRules(new ShogiBoardState(board)); + // var fromNotation = ShogiBoardState.ToBoardNotation(from); + // var toNotation = ShogiBoardState.ToBoardNotation(position); + // var simulationResult = simulationBoard.Move(fromNotation, toNotation, false); + // if (simulationResult.Success) + // { + // //if (!IsPlayerInCheckAfterMove(from, position, board.InCheck.Value)) + // //{ + // // isCheckmate = false; + // //} + // } + //}); + //} + // TODO: Assert that a player could not place a piece from their hand to avoid check. + //}); return isCheckmate; } }