From 3da187be178c1bc41b092ce889c2191bcdf12638 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 1 Mar 2021 20:55:37 -0600 Subject: [PATCH] yep --- Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 183 ++++++++++-------- .../BoardState/ShogiBoardShould.cs | 1 - .../PathFinding/PathFinder2DShould.cs | 4 +- PathFinding/IPlanarCollection.cs | 2 +- PathFinding/PathFinder2D.cs | 73 ++++--- 5 files changed, 154 insertions(+), 109 deletions(-) diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index f11c980..9c16fd8 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -44,11 +44,15 @@ namespace Gameboard.ShogiUI.BoardState { for (var i = 0; i < moves.Count; i++) { - if (!Move(moves[i])) + if (!TryMove(moves[i])) { throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); } } + if (EvaluateCheckAfterMove(WhoseTurn)) + { + InCheck = WhoseTurn; + } } private ShogiBoard(ShogiBoard toCopy) @@ -72,7 +76,6 @@ namespace Gameboard.ShogiUI.BoardState public bool Move(Move move) { - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); if (!moveSuccess) @@ -81,10 +84,13 @@ namespace Gameboard.ShogiUI.BoardState } // Evaluate check - InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; - if (InCheck.HasValue) + if (EvaluateCheckAfterMove(WhoseTurn)) { - //IsCheckmate = EvaluateCheckmate(); + InCheck = WhoseTurn; + if (InCheck.HasValue) + { + //IsCheckmate = EvaluateCheckmate(); + } } return true; } @@ -109,47 +115,17 @@ namespace Gameboard.ShogiUI.BoardState return false; } // Assert that this move does not put the moving player in check. - if (validationBoard.EvaluateCheck(WhoseTurn)) return false; + if (validationBoard.EvaluateCheckAfterMove(WhoseTurn)) + { + // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; + return false; + } // The move is valid and legal; update board state. if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); else PlaceFromBoard(move); return true; } - private bool EvaluateCheckmate() - { - if (!InCheck.HasValue) return false; - - // Assume true and try to disprove. - var isCheckmate = true; - Board.ForEachNotNull((piece, x, y) => // For each piece... - { - if (!isCheckmate) return; // Short circuit - - var from = new Vector2(x, y); - if (piece.Owner == InCheck) // Owned by the player in check... - { - var positionsToCheck = new List(10); - //IterateMoveSet(from, (innerPiece, position) => - //{ - // if (innerPiece?.Owner != InCheck) positionsToCheck.Add(position); // Find possible moves... - //}); - - // And evaluate if any move gets the player out of check. - foreach (var position in positionsToCheck) - { - if (validationBoard == null) validationBoard = new ShogiBoard(this); - var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); - if (moveSuccess) - { - isCheckmate &= validationBoard.EvaluateCheck(InCheck.Value); - validationBoard = null; - } - } - } - }); - return isCheckmate; - } /// True if the move was successful. private bool PlaceFromHand(Move move) { @@ -186,7 +162,7 @@ namespace Gameboard.ShogiUI.BoardState var fromPiece = Board[move.From.X, move.From.Y]; if (fromPiece == null) return false; // Invalid move if (fromPiece.Owner != WhoseTurn) return false; // Invalid move; cannot move other players pieces. - if (IsPathable(move.From, move.To, fromPiece) == false) return false; // Invalid move; move not part of move-set. + if (IsPathable(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -227,10 +203,13 @@ namespace Gameboard.ShogiUI.BoardState return true; } - private bool IsPathable(Vector2 from, Vector2 to, Piece piece) + private bool IsPathable(Vector2 from, Vector2 to) { + var piece = Board[from.X, from.Y]; + if (piece == null) return false; + var isObstructed = false; - var isPathable = pathFinder.PathTo(piece, from, to, (other, position) => + var isPathable = pathFinder.PathTo(from, to, (other, position) => { if (other.Owner == piece.Owner) isObstructed = true; }); @@ -272,39 +251,92 @@ namespace Gameboard.ShogiUI.BoardState Console.WriteLine(builder.ToString()); } #region Rules Validation - /// - /// Evaluate if a player is in check given the current board state. - /// - private bool EvaluateCheck(WhichPlayer whichPlayer) + private bool EvaluateCheckAfterMove(WhichPlayer whichPlayer) { - var destination = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; - var inCheck = false; - // Iterate every board piece... - Board.ForEachNotNull((piece, x, y) => + var isCheck = false; + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; + + // Get last move. + var move = MoveHistory[^1]; + // Check if the move put the king in check. + if (pathFinder.PathTo(move.To, kingPosition)) return true; + + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, move.From); + var slope = Math.Abs(direction.Y / direction.X); + // 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. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) { - var origin = new Vector2(x, y); - // ...that belongs to the opponent within range... - if (piece.Owner != whichPlayer && pathFinder.IsPathable(origin, destination, piece)) + // if slope of the move is also infinity...can skip this? + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => { - pathFinder.PathTo(piece, origin, destination, (threatenedPiece, position) => + if (piece.Owner != whichPlayer) { - // ...and threatens the player's king. - inCheck |= - threatenedPiece.WhichPiece == WhichPiece.King - && threatenedPiece.Owner == whichPlayer; - }); + switch (piece.WhichPiece) + { + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; + } + } + }); + } + else if (slope == 1) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); + } + + return isCheck; + } + private bool EvaluateCheckmate() + { + if (!InCheck.HasValue) return false; + + // Assume true and try to disprove. + var isCheckmate = true; + Board.ForEachNotNull((piece, x, y) => // For each piece... + { + // Short circuit + if (!isCheckmate) return; + + var from = new Vector2(x, y); + if (piece.Owner == InCheck) // ...owned by the player in check... + { + // ...evaluate if any move gets the player out of check. + pathFinder.PathEvery(from, (other, position) => + { + if (validationBoard == null) validationBoard = new ShogiBoard(this); + var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); + if (moveSuccess) + { + isCheckmate = false; + } + }); } }); - return inCheck; + return isCheckmate; } - /// - /// Iterate through the possible moves of a piece at a given position. - /// - - /// - /// Useful for iterating the board for pieces that move many spaces. - /// - /// A function that returns true if walking should continue. #endregion #region Initialize @@ -362,18 +394,5 @@ namespace Gameboard.ShogiUI.BoardState ResetRearRow(WhichPlayer.Player2); } #endregion - - //public static ShogiBoard ConstructWithMoves(IList moves) - //{ - // var s = new ShogiBoard(); - // for (var i = 0; i < moves.Count; i++) - // { - // if (!s.Move(moves[i])) - // { - // throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); - // } - // } - // return s; - //} } } diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 4eaf634..ae234f1 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -177,7 +177,6 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P1 Bishop puts P2 in check new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) } }; - //var shogi = new ShogiBoard(moves); var shogi = new ShogiBoard(moves); // Prerequisit diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs index 98782e5..935051e 100644 --- a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -24,14 +24,14 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding var result = finder.IsPathable( new Vector2(2, 2), new Vector2(7, 7), - new Vector2(3, 3) + new Vector2(1, 1) ); result.Should().BeTrue(); result = finder.IsPathable( new Vector2(2, 2), new Vector2(7, 7), - new Vector2(2, 2) + new Vector2(0, 0) ); result.Should().BeFalse(); diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 3ccf800..95506e1 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -4,7 +4,7 @@ namespace PathFinding { public interface IPlanarCollection : IEnumerable { - T this[int x, int y] { get; set; } + T this[float x, float y] { get; set; } int GetLength(int dimension); } } diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 9aa51e7..5b5c1e3 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -24,46 +25,50 @@ namespace PathFinding } /// - /// Navigate the collection such that each "step" is always towards the destination. + /// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin. /// /// The pathing element. /// The starting location. /// The destination. /// Do cool stuff here. /// True if the element reached the destination. - public bool PathTo(T element, Vector2 origin, Vector2 destination, Callback callback) + public bool PathTo(Vector2 origin, Vector2 destination, Callback callback = null) { if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) { return false; } - + var element = collection[origin.X, origin.Y]; var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); - var next = Vector2.Add(origin, path.Direction); - if (!IsPathable(origin, destination, next)) + if (!IsPathable(origin, destination, path.Direction)) { // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. return false; } var shouldPath = true; - while (shouldPath) + var next = origin; + while (shouldPath && next != destination) { + next = Vector2.Add(next, path.Direction); var collider = collection[(int)next.X, (int)next.Y]; - if (collider != null) callback(collider, next); - if (next == destination) return true; - if (path.Distance == Distance.OneStep) + if (collider != null) + { + callback?.Invoke(collider, next); + shouldPath = false; + } + else if (path.Distance == Distance.OneStep) { shouldPath = false; } - next = Vector2.Add(next, path.Direction); } - return true; + return next == destination; } - public void PathEvery(IPlanarElement element, Vector2 from, Callback callback) + public void PathEvery(Vector2 from, Callback callback) { + var element = collection[from.X, from.Y]; foreach (var path in element.GetPaths()) { var shouldPath = true; @@ -84,6 +89,22 @@ namespace PathFinding } } + /// + /// Path the line from origin to destination, ignoring any Paths defined by the element at origin. + /// + public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback) + { + direction = Vector2.Normalize(direction); + + var next = Vector2.Add(origin, direction); + while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) + { + var element = collection[next.X, next.Y]; + if (element != null) callback(element, next); + next = Vector2.Add(next, direction); + } + } + public Path FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); @@ -91,20 +112,26 @@ namespace PathFinding public bool IsPathable(Vector2 origin, Vector2 destination, T element) { var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); - var next = Vector2.Add(origin, path.Direction); - return IsPathable(origin, destination, next); + return IsPathable(origin, destination, path.Direction); } - public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 next) + public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) { - if (Vector2.Distance(next, destination) < Vector2.Distance(origin, destination)) + direction = Vector2.Normalize(direction); + var next = Vector2.Add(origin, direction); + if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false; + + var slope = (destination.Y - origin.Y) / (destination.X - origin.X); + if (float.IsInfinity(slope)) { - // y = mx + b - // b = -mx + y - var slope = (destination.Y - origin.Y) / (destination.X - origin.X); - var yIntercept = -(slope * origin.X) + origin.Y; - return float.IsInfinity(slope) || next.Y == slope * next.X + yIntercept; + return next.X == destination.X; + } + else + { + // b = -mx + y + var yIntercept = -slope * origin.X + origin.Y; + // y = mx + b + return next.Y == slope * next.X + yIntercept; } - return false; } } }