From f55716d2ec906164c95ef6bde76104eea79cc7dd Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 28 Feb 2021 07:52:28 -0600 Subject: [PATCH] UTests pass with Pathfinder2D --- Benchmarking/Benchmarks.cs | 50 ++- Gameboard.ShogiUI.BoardState/Array2D.cs | 12 +- Gameboard.ShogiUI.BoardState/BoardVector.cs | 37 -- Gameboard.ShogiUI.BoardState/Direction.cs | 18 + .../Gameboard.ShogiUI.BoardState.csproj | 4 + Gameboard.ShogiUI.BoardState/Move.cs | 4 +- Gameboard.ShogiUI.BoardState/Piece.cs | 14 +- Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs | 47 +++ .../Pieces/GoldGeneral.cs | 34 ++ Gameboard.ShogiUI.BoardState/Pieces/King.cs | 35 ++ Gameboard.ShogiUI.BoardState/Pieces/Knight.cs | 33 ++ Gameboard.ShogiUI.BoardState/Pieces/Lance.cs | 32 ++ Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs | 32 ++ Gameboard.ShogiUI.BoardState/Pieces/Rook.cs | 44 +++ .../Pieces/SilverGeneral.cs | 35 ++ Gameboard.ShogiUI.BoardState/ShogiBoard.cs | 318 +++++------------- .../ClientActionHandlers/LoadGameHandler.cs | 2 +- .../BoardState/ShogiBoardShould.cs | 25 +- .../PathFinding/PathFinder2DShould.cs | 46 +++ PathFinding/Enums.cs | 19 ++ PathFinding/IPlanarCollection.cs | 7 +- PathFinding/IPlanarElement.cs | 12 + PathFinding/Path.cs | 15 + PathFinding/PathFinder2D.cs | 106 +++++- 24 files changed, 687 insertions(+), 294 deletions(-) delete mode 100644 Gameboard.ShogiUI.BoardState/BoardVector.cs create mode 100644 Gameboard.ShogiUI.BoardState/Direction.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/King.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Knight.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Lance.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/Rook.cs create mode 100644 Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs create mode 100644 Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs create mode 100644 PathFinding/Enums.cs create mode 100644 PathFinding/IPlanarElement.cs create mode 100644 PathFinding/Path.cs diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs index 5a7947f..a972747 100644 --- a/Benchmarking/Benchmarks.cs +++ b/Benchmarking/Benchmarks.cs @@ -1,7 +1,9 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; using Gameboard.ShogiUI.BoardState; using System; +using System.Linq; using System.Numerics; namespace Benchmarking @@ -9,6 +11,8 @@ namespace Benchmarking public class Benchmarks { private readonly Move[] moves; + private readonly Vector2[] directions; + private readonly Consumer consumer = new Consumer(); public Benchmarks() { @@ -35,9 +39,13 @@ namespace Benchmarking // P2 King retreat new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; + var rand = new Random(); + + directions = new Vector2[10]; + for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); } - [Benchmark] + //[Benchmark] public void One() { var board = new ShogiBoard(); @@ -47,7 +55,7 @@ namespace Benchmarking } } - [Benchmark] + //[Benchmark] public void Two() { var board = new ShogiBoard(); @@ -57,15 +65,49 @@ namespace Benchmarking } } + + + public Vector2 FindDirection(Vector2[] directions, Vector2 destination) + { + var smallerDistance = float.MaxValue; + Vector2 found = Vector2.Zero; + foreach (var d in directions) + { + var distance = Vector2.Distance(d, destination); + if (distance < smallerDistance) + { + smallerDistance = distance; + found = d; + } + } + return found; + } + + + public Vector2 FindDirectionLinq(Vector2[] directions, Vector2 destination) => + directions.Aggregate((a, b) => Vector2.Distance(destination, a) < Vector2.Distance(destination, b) ? a : b); + + + + + [Benchmark] + public void Directions_A() + { + FindDirection(directions, new Vector2(8, 7)); + } + [Benchmark] + public void Directions_B() + { + FindDirectionLinq(directions, new Vector2(8, 7)); + } } public class Program { public static void Main(string[] args) { - //BenchmarkRunner.Run(); + BenchmarkRunner.Run(); Console.WriteLine("Done"); - } } } diff --git a/Gameboard.ShogiUI.BoardState/Array2D.cs b/Gameboard.ShogiUI.BoardState/Array2D.cs index 8e46f77..6487646 100644 --- a/Gameboard.ShogiUI.BoardState/Array2D.cs +++ b/Gameboard.ShogiUI.BoardState/Array2D.cs @@ -1,11 +1,12 @@ -using System; +using PathFinding; +using System; using System.Collections; using System.Collections.Generic; using System.Numerics; namespace Gameboard.ShogiUI.BoardState { - public class Array2D : IEnumerable + public class Array2D : IPlanarCollection, IEnumerable { /// False to stop iterating. public delegate void ForEachDelegate(T element, int x, int y); @@ -31,6 +32,13 @@ namespace Gameboard.ShogiUI.BoardState set => array[(int)y * width + (int)x] = value; } + public int GetLength(int dimension) => dimension switch + { + 0 => width, + 1 => height, + _ => throw new IndexOutOfRangeException() + }; + public void ForEach(ForEachDelegate callback) { for (var x = 0; x < width; x++) diff --git a/Gameboard.ShogiUI.BoardState/BoardVector.cs b/Gameboard.ShogiUI.BoardState/BoardVector.cs deleted file mode 100644 index 8f550d3..0000000 --- a/Gameboard.ShogiUI.BoardState/BoardVector.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Numerics; - -namespace Gameboard.ShogiUI.BoardState -{ - /// - /// Provides normalized Vector2s relative to player. - /// "Up" for player 1 is "Down" for player 2; that sort of thing. - /// - public class Direction - { - private static readonly Vector2 PositiveX = new Vector2(1, 0); - private static readonly Vector2 NegativeX = new Vector2(-1, 0); - private static readonly Vector2 PositiveY = new Vector2(0, 1); - private static readonly Vector2 NegativeY = new Vector2(0, -1); - private static readonly Vector2 PositiveYX = new Vector2(1, 1); - private static readonly Vector2 NegativeYX = new Vector2(-1, -1); - private static readonly Vector2 NegativeYPositiveX = new Vector2(1, -1); - private static readonly Vector2 PositiveYNegativeX = new Vector2(-1, 1); - - private readonly WhichPlayer whichPlayer; - public Direction(WhichPlayer whichPlayer) - { - this.whichPlayer = whichPlayer; - } - - public Vector2 Up => whichPlayer == WhichPlayer.Player1 ? PositiveY : NegativeY; - public Vector2 Down => whichPlayer == WhichPlayer.Player1 ? NegativeY : PositiveY; - public Vector2 Left => whichPlayer == WhichPlayer.Player1 ? NegativeX : PositiveX; - public Vector2 Right => whichPlayer == WhichPlayer.Player1 ? PositiveX : NegativeX; - public Vector2 UpLeft => whichPlayer == WhichPlayer.Player1 ? PositiveYNegativeX : NegativeYPositiveX; - public Vector2 UpRight => whichPlayer == WhichPlayer.Player1 ? PositiveYX : NegativeYX; - public Vector2 DownLeft => whichPlayer == WhichPlayer.Player1 ? NegativeYX : PositiveYX; - public Vector2 DownRight => whichPlayer == WhichPlayer.Player1 ? NegativeYPositiveX : PositiveYNegativeX; - public Vector2 KnightLeft => whichPlayer == WhichPlayer.Player1 ? new Vector2(-1, 2) : new Vector2(1, -2); - public Vector2 KnightRight => whichPlayer == WhichPlayer.Player1 ? new Vector2(1, 2) : new Vector2(-1, -2); - } -} diff --git a/Gameboard.ShogiUI.BoardState/Direction.cs b/Gameboard.ShogiUI.BoardState/Direction.cs new file mode 100644 index 0000000..5f1307c --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Direction.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState +{ + public static class Direction + { + public static readonly Vector2 Up = new Vector2(0, 1); + public static readonly Vector2 Down = new Vector2(0, -1); + public static readonly Vector2 Left = new Vector2(-1, 0); + public static readonly Vector2 Right = new Vector2(1, 0); + public static readonly Vector2 UpLeft = new Vector2(1, 1); + public static readonly Vector2 UpRight = new Vector2(-1, 1); + public static readonly Vector2 DownLeft = new Vector2(-1, -1); + public static readonly Vector2 DownRight = new Vector2(1, -1); + public static readonly Vector2 KnightLeft = new Vector2(-1, 2); + public static readonly Vector2 KnightRight = new Vector2(1, 2); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj index f208d30..e1f728c 100644 --- a/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj +++ b/Gameboard.ShogiUI.BoardState/Gameboard.ShogiUI.BoardState.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/Gameboard.ShogiUI.BoardState/Move.cs b/Gameboard.ShogiUI.BoardState/Move.cs index 9df0f89..fc8f860 100644 --- a/Gameboard.ShogiUI.BoardState/Move.cs +++ b/Gameboard.ShogiUI.BoardState/Move.cs @@ -1,7 +1,9 @@ -using System.Numerics; +using System.Diagnostics; +using System.Numerics; namespace Gameboard.ShogiUI.BoardState { + [DebuggerDisplay("{From} - {To}")] public class Move { public WhichPiece? PieceFromCaptured { get; set; } diff --git a/Gameboard.ShogiUI.BoardState/Piece.cs b/Gameboard.ShogiUI.BoardState/Piece.cs index 99b218d..99bca18 100644 --- a/Gameboard.ShogiUI.BoardState/Piece.cs +++ b/Gameboard.ShogiUI.BoardState/Piece.cs @@ -1,9 +1,11 @@ -using System.Diagnostics; +using PathFinding; +using System.Collections.Generic; +using System.Diagnostics; namespace Gameboard.ShogiUI.BoardState { [DebuggerDisplay("{WhichPiece} {Owner}")] - public class Piece + public abstract class Piece : IPlanarElement { public WhichPiece WhichPiece { get; } public WhichPlayer Owner { get; private set; } @@ -48,7 +50,7 @@ namespace Gameboard.ShogiUI.BoardState : WhichPlayer.Player1; } - public void Promote() => IsPromoted = true; + public void Promote() => IsPromoted = CanPromote; public void Demote() => IsPromoted = false; @@ -57,5 +59,11 @@ namespace Gameboard.ShogiUI.BoardState ToggleOwnership(); Demote(); } + + public abstract ICollection GetPaths(); + + public abstract Piece DeepClone(); + + public bool IsUpsideDown => Owner == WhichPlayer.Player2; } } diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs new file mode 100644 index 0000000..cf73954 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Bishop.cs @@ -0,0 +1,47 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Bishop : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }; + private static readonly List PromotedMoveSet = new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }; + public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) + { + // TODO: If this strat works out, we can do away with the Direction class entirely. + PromotedMoveSet.AddRange(MoveSet); + } + + public override Piece DeepClone() + { + var clone = new Bishop(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs new file mode 100644 index 0000000..cd22d62 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/GoldGeneral.cs @@ -0,0 +1,34 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class GoldenGeneral : Piece + { + public static readonly List MoveSet = new List(6) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down) + }; + public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) + { + } + + public override Piece DeepClone() + { + var clone = new GoldenGeneral(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() => Owner == WhichPlayer.Player1 + ? MoveSet + : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/King.cs b/Gameboard.ShogiUI.BoardState/Pieces/King.cs new file mode 100644 index 0000000..76251e4 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/King.cs @@ -0,0 +1,35 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class King : Piece + { + private static readonly List MoveSet = new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public King(WhichPlayer owner) : base(WhichPiece.King, owner) + { + } + + public override Piece DeepClone() + { + var clone = new King(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + public override ICollection GetPaths() => Owner == WhichPlayer.Player1 + ? MoveSet + : MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs new file mode 100644 index 0000000..7e4a4df --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Knight.cs @@ -0,0 +1,33 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Knight : Piece + { + private static readonly List MoveSet = new List(2) + { + new Path(Direction.KnightLeft), + new Path(Direction.KnightRight) + }; + + public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Knight(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs new file mode 100644 index 0000000..a9f2df7 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Lance.cs @@ -0,0 +1,32 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Lance : Piece + { + private static readonly List MoveSet = new List(1) + { + new Path(Direction.Up, Distance.MultiStep), + }; + + public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Lance(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs new file mode 100644 index 0000000..bce9f14 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Pawn.cs @@ -0,0 +1,32 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Pawn : Piece + { + private static readonly List MoveSet = new List(1) + { + new Path(Direction.Up) + }; + + public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Pawn(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs new file mode 100644 index 0000000..dfedd94 --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/Rook.cs @@ -0,0 +1,44 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class Rook : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep) + }; + private static readonly List PromotedMoveSet = new List(8) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) + { + } + + public override Piece DeepClone() + { + var clone = new Rook(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? PromotedMoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs new file mode 100644 index 0000000..ef0bcbd --- /dev/null +++ b/Gameboard.ShogiUI.BoardState/Pieces/SilverGeneral.cs @@ -0,0 +1,35 @@ +using PathFinding; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Gameboard.ShogiUI.BoardState.Pieces +{ + public class SilverGeneral : Piece + { + private static readonly List MoveSet = new List(4) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }; + public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) + { + } + + public override Piece DeepClone() + { + var clone = new SilverGeneral(Owner); + if (IsPromoted) clone.Promote(); + return clone; + } + + public override ICollection GetPaths() + { + var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet; + return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList(); + } + } +} diff --git a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs index 0bd9819..f11c980 100644 --- a/Gameboard.ShogiUI.BoardState/ShogiBoard.cs +++ b/Gameboard.ShogiUI.BoardState/ShogiBoard.cs @@ -1,4 +1,6 @@ -using System; +using Gameboard.ShogiUI.BoardState.Pieces; +using PathFinding; +using System; using System.Collections.Generic; using System.Numerics; using System.Text; @@ -16,6 +18,7 @@ namespace Gameboard.ShogiUI.BoardState private ShogiBoard validationBoard; private Vector2 player1King; private Vector2 player2King; + private PathFinder2D pathFinder; public IReadOnlyDictionary> Hands { get; } public Array2D Board { get; } public List MoveHistory { get; } @@ -31,17 +34,51 @@ namespace Gameboard.ShogiUI.BoardState { WhichPlayer.Player1, new List()}, { WhichPlayer.Player2, new List()}, }; + pathFinder = new PathFinder2D(Board); InitializeBoardState(); player1King = new Vector2(4, 0); player2King = new Vector2(4, 8); } + public ShogiBoard(IList moves) : this() + { + for (var i = 0; i < moves.Count; i++) + { + if (!Move(moves[i])) + { + throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}."); + } + } + } + + private ShogiBoard(ShogiBoard toCopy) + { + Board = new Array2D(9, 9); + for (var x = 0; x < 9; x++) + for (var y = 0; y < 9; y++) + Board[x, y] = toCopy.Board[x, y]?.DeepClone(); + + pathFinder = new PathFinder2D(Board); + MoveHistory = new List(toCopy.MoveHistory); + Hands = new Dictionary> + { + { WhichPlayer.Player1, new List(toCopy.Hands[WhichPlayer.Player1]) }, + { WhichPlayer.Player2, new List(toCopy.Hands[WhichPlayer.Player2]) } + }; + player1King = toCopy.player1King; + player2King = toCopy.player2King; + } + public bool Move(Move move) { + var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; var moveSuccess = TryMove(move); - if (!moveSuccess) return false; + if (!moveSuccess) + { + return false; + } // Evaluate check InCheck = EvaluateCheck(otherPlayer) ? otherPlayer : null; @@ -59,8 +96,9 @@ namespace Gameboard.ShogiUI.BoardState // Try making the move in a "throw away" board. if (validationBoard == null) { - validationBoard = ConstructWithMoves(MoveHistory); + validationBoard = new ShogiBoard(this); } + var isValid = move.PieceFromCaptured.HasValue ? validationBoard.PlaceFromHand(move) : validationBoard.PlaceFromBoard(move); @@ -92,15 +130,15 @@ namespace Gameboard.ShogiUI.BoardState 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... - }); + //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 = ConstructWithMoves(MoveHistory); + if (validationBoard == null) validationBoard = new ShogiBoard(this); var moveSuccess = validationBoard.TryMove(new Move { From = from, To = position }); if (moveSuccess) { @@ -148,7 +186,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 (ValidateMoveAgainstMoveSet(move.From, move.To) == false) return false; // Invalid move; move not part of move-set. + if (IsPathable(move.From, move.To, fromPiece) == false) return false; // Invalid move; move not part of move-set. var captured = Board[move.To.X, move.To.Y]; if (captured != null) @@ -188,6 +226,17 @@ namespace Gameboard.ShogiUI.BoardState MoveHistory.Add(move); return true; } + + private bool IsPathable(Vector2 from, Vector2 to, Piece piece) + { + var isObstructed = false; + var isPathable = pathFinder.PathTo(piece, from, to, (other, position) => + { + if (other.Owner == piece.Owner) isObstructed = true; + }); + return !isObstructed && isPathable; + } + public void PrintStateAsAscii() { var builder = new StringBuilder(); @@ -228,219 +277,34 @@ namespace Gameboard.ShogiUI.BoardState /// private bool EvaluateCheck(WhichPlayer whichPlayer) { - var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; + var destination = whichPlayer == WhichPlayer.Player1 ? player1King : player2King; var inCheck = false; // Iterate every board piece... Board.ForEachNotNull((piece, x, y) => { - var v = new Vector2(x, y); + var origin = new Vector2(x, y); // ...that belongs to the opponent within range... - if (piece.Owner != whichPlayer && (piece.IsRanged || Vector2.Distance(kingPosition, v) < 3)) + if (piece.Owner != whichPlayer && pathFinder.IsPathable(origin, destination, piece)) { - IterateMoveSet(new Vector2(x, y), (threatenedPiece, position) => + pathFinder.PathTo(piece, origin, destination, (threatenedPiece, position) => { // ...and threatens the player's king. inCheck |= - threatenedPiece?.WhichPiece == WhichPiece.King - && threatenedPiece?.Owner == whichPlayer; + threatenedPiece.WhichPiece == WhichPiece.King + && threatenedPiece.Owner == whichPlayer; }); } }); return inCheck; } - private bool ValidateMoveAgainstMoveSet(Vector2 from, Vector2 to) - { - var isValid = false; - IterateMoveSet(from, (piece, position) => - { - if (piece?.Owner != WhoseTurn && position == to) - { - isValid = true; - } - }); - - return isValid; - } /// /// Iterate through the possible moves of a piece at a given position. /// - private void IterateMoveSet(Vector2 from, MoveSetCallback callback) - { - // TODO: Make these are of the move To, so only possible moves towards the move To are iterated. - // Maybe separate functions? Sometimes I need to iterate the whole move-set, sometimes I need to iterate only the move-set towards the move To. - var piece = Board[from.X, from.Y]; - switch (piece?.WhichPiece) - { - case WhichPiece.King: - IterateKingMoveSet(from, callback); - break; - case WhichPiece.GoldenGeneral: - IterateGoldenGeneralMoveSet(from, callback); - break; - case WhichPiece.SilverGeneral: - IterateSilverGeneralMoveSet(from, callback); - break; - case WhichPiece.Bishop: - IterateBishopMoveSet(from, callback); - break; - case WhichPiece.Rook: - IterateRookMoveSet(from, callback); - break; - case WhichPiece.Knight: - IterateKnightMoveSet(from, callback); - break; - case WhichPiece.Lance: - IterateLanceMoveSet(from, callback); - break; - case WhichPiece.Pawn: - IteratePawnMoveSet(from, callback); - break; - } - } - private void IterateKingMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.Down, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - } - private void IterateGoldenGeneralMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.Down, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - } - private void IterateSilverGeneralMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - } - } - private void IterateBishopMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.UpLeft, callback); - BoardWalk(from, direction.UpRight, callback); - BoardWalk(from, direction.DownLeft, callback); - BoardWalk(from, direction.DownRight, callback); - if (piece.IsPromoted) - { - BoardStep(from, direction.Up, callback); - BoardStep(from, direction.Left, callback); - BoardStep(from, direction.Right, callback); - BoardStep(from, direction.Down, callback); - } - } - private void IterateRookMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.Up, callback); - BoardWalk(from, direction.Left, callback); - BoardWalk(from, direction.Right, callback); - BoardWalk(from, direction.Down, callback); - if (piece.IsPromoted) - { - BoardStep(from, direction.UpLeft, callback); - BoardStep(from, direction.UpRight, callback); - BoardStep(from, direction.DownLeft, callback); - BoardStep(from, direction.DownRight, callback); - } - } - private void IterateKnightMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardStep(from, direction.KnightLeft, callback); - BoardStep(from, direction.KnightRight, callback); - } - } - private void IterateLanceMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardWalk(from, direction.Up, callback); - } - } - private void IteratePawnMoveSet(Vector2 from, MoveSetCallback callback) - { - var piece = Board[from.X, from.Y]; - if (piece?.WhichPiece == WhichPiece.Pawn) - { - if (piece.IsPromoted) - { - IterateGoldenGeneralMoveSet(from, callback); - } - else - { - var direction = new Direction(piece.Owner); - BoardStep(from, direction.Up, callback); - } - } - } + /// /// Useful for iterating the board for pieces that move many spaces. /// /// A function that returns true if walking should continue. - private void BoardWalk(Vector2 from, Vector2 direction, MoveSetCallback callback) - { - var foundAnotherPiece = false; - var to = Vector2.Add(from, direction); - while (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9 && !foundAnotherPiece) - { - var piece = Board[to.X, to.Y]; - callback(piece, to); - to = Vector2.Add(to, direction); - foundAnotherPiece = piece != null; - } - } - - /// - /// Useful for iterating the board for pieces that move only one space. - /// - private void BoardStep(Vector2 from, Vector2 direction, MoveSetCallback callback) - { - var to = Vector2.Add(from, direction); - if (to.X >= 0 && to.X < 9 && to.Y >= 0 && to.Y < 9) - { - callback(Board[to.X, to.Y], to); - } - } #endregion #region Initialize @@ -453,7 +317,7 @@ namespace Gameboard.ShogiUI.BoardState private void ResetFrontRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 2 : 6; - for (int x = 0; x < 9; x++) Board[x, y] = new Piece(WhichPiece.Pawn, player); + for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); } private void ResetMiddleRow(WhichPlayer player) { @@ -464,28 +328,28 @@ namespace Gameboard.ShogiUI.BoardState Board[8, y] = null; if (player == WhichPlayer.Player1) { - Board[1, y] = new Piece(WhichPiece.Bishop, player); - Board[7, y] = new Piece(WhichPiece.Rook, player); + Board[1, y] = new Bishop(player); + Board[7, y] = new Rook(player); } else { - Board[1, y] = new Piece(WhichPiece.Rook, player); - Board[7, y] = new Piece(WhichPiece.Bishop, player); + Board[1, y] = new Rook(player); + Board[7, y] = new Bishop(player); } } private void ResetRearRow(WhichPlayer player) { int y = player == WhichPlayer.Player1 ? 0 : 8; - Board[0, y] = new Piece(WhichPiece.Lance, player); - Board[1, y] = new Piece(WhichPiece.Knight, player); - Board[2, y] = new Piece(WhichPiece.SilverGeneral, player); - Board[3, y] = new Piece(WhichPiece.GoldenGeneral, player); - Board[4, y] = new Piece(WhichPiece.King, player); - Board[5, y] = new Piece(WhichPiece.GoldenGeneral, player); - Board[6, y] = new Piece(WhichPiece.SilverGeneral, player); - Board[7, y] = new Piece(WhichPiece.Knight, player); - Board[8, y] = new Piece(WhichPiece.Lance, player); + Board[0, y] = new Lance(player); + Board[1, y] = new Knight(player); + Board[2, y] = new SilverGeneral(player); + Board[3, y] = new GoldenGeneral(player); + Board[4, y] = new King(player); + Board[5, y] = new GoldenGeneral(player); + Board[6, y] = new SilverGeneral(player); + Board[7, y] = new Knight(player); + Board[8, y] = new Lance(player); } private void InitializeBoardState() { @@ -499,17 +363,17 @@ namespace Gameboard.ShogiUI.BoardState } #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; - } + //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.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs index 1ec37d1..db15bfa 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -52,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers communicationManager.SubscribeToGame(sessionModel, userName); var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); - boardManager.Add(getGameResponse.Session.Name, ShogiBoard.ConstructWithMoves(boardMoves)); + boardManager.Add(getGameResponse.Session.Name, new ShogiBoard(boardMoves)); var response = new LoadGameResponse(ClientAction.LoadGame) { diff --git a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs index 2933154..4eaf634 100644 --- a/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs +++ b/Gameboard.ShogiUI.UnitTests/BoardState/ShogiBoardShould.cs @@ -75,7 +75,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState To = new Vector2(0, 3) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); shogi.Board[0, 2].Should().BeNull(); shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); } @@ -99,9 +99,9 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState { var invalidLanceMove = new Move { - // Lance moving adjacent - From = new Vector2(0, 0), - To = new Vector2(1, 5) + // Bishop moving lateral + From = new Vector2(1, 1), + To = new Vector2(2, 1) }; var shogi = new ShogiBoard(); @@ -177,16 +177,12 @@ 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 = ShogiBoard.ConstructWithMoves(moves); - //foreach(var m in moves) - //{ - // shogi.Move(m); - //} + //var shogi = new ShogiBoard(moves); + var shogi = new ShogiBoard(moves); // Prerequisit shogi.InCheck.Should().Be(WhichPlayer.Player2); - // Act - P2 moves Lance while remaining in check. var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 8), To = new Vector2(8, 7) }); @@ -208,7 +204,8 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) }, }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); + shogi.PrintStateAsAscii(); // Act - P1 Bishop, check @@ -229,7 +226,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 Bishop captures P2 Bishop var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(7, 7) }); @@ -275,7 +272,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 Pawn new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) } }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 moves across promote threshold. var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 1), To = new Vector2(6, 6), IsPromotion = true }); @@ -313,7 +310,7 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState // P2 King retreat new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, }; - var shogi = ShogiBoard.ConstructWithMoves(moves); + var shogi = new ShogiBoard(moves); // Act - P1 Pawn wins by checkmate. var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 6), To = new Vector2(4, 7) }); diff --git a/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs new file mode 100644 index 0000000..98782e5 --- /dev/null +++ b/Gameboard.ShogiUI.UnitTests/PathFinding/PathFinder2DShould.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using Gameboard.ShogiUI.BoardState; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PathFinding; +using System.Collections.Generic; +using System.Numerics; + +namespace Gameboard.ShogiUI.UnitTests.PathFinding +{ + [TestClass] + public class PathFinder2DShould + { + class TestElement : IPlanarElement + { + public ICollection GetPaths() => throw new System.NotImplementedException(); + public bool IsUpsideDown => false; + } + + [TestMethod] + public void Maths() + { + var finder = new PathFinder2D(new Array2D(5, 5)); + + var result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(3, 3) + ); + result.Should().BeTrue(); + + result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(2, 2) + ); + result.Should().BeFalse(); + + result = finder.IsPathable( + new Vector2(2, 2), + new Vector2(7, 7), + new Vector2(-1, 1) + ); + result.Should().BeFalse(); + } + } +} diff --git a/PathFinding/Enums.cs b/PathFinding/Enums.cs new file mode 100644 index 0000000..fab3f18 --- /dev/null +++ b/PathFinding/Enums.cs @@ -0,0 +1,19 @@ +namespace PathFinding +{ + public enum HaltCondition + { + /// + /// Do not stop until you reach the collection boundary. + /// + None, + /// + /// Halt after encountering a non-null element. + /// + AfterCollide + } +} +public enum Distance +{ + OneStep, + MultiStep +} diff --git a/PathFinding/IPlanarCollection.cs b/PathFinding/IPlanarCollection.cs index 8a2b046..3ccf800 100644 --- a/PathFinding/IPlanarCollection.cs +++ b/PathFinding/IPlanarCollection.cs @@ -1,7 +1,10 @@ -namespace PathFinding +using System.Collections.Generic; + +namespace PathFinding { - interface IPlanarCollection + public interface IPlanarCollection : IEnumerable { T this[int x, int y] { get; set; } + int GetLength(int dimension); } } diff --git a/PathFinding/IPlanarElement.cs b/PathFinding/IPlanarElement.cs new file mode 100644 index 0000000..4983bb4 --- /dev/null +++ b/PathFinding/IPlanarElement.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; + +namespace PathFinding +{ + public interface IPlanarElement + { + ICollection GetPaths(); + + bool IsUpsideDown { get; } + } +} diff --git a/PathFinding/Path.cs b/PathFinding/Path.cs new file mode 100644 index 0000000..dd3edaf --- /dev/null +++ b/PathFinding/Path.cs @@ -0,0 +1,15 @@ +using System.Numerics; + +namespace PathFinding +{ + public class Path + { + public Vector2 Direction { get; } + public Distance Distance { get; } + public Path(Vector2 direction, Distance distance = Distance.OneStep) + { + Direction = direction; + Distance = distance; + } + } +} diff --git a/PathFinding/PathFinder2D.cs b/PathFinding/PathFinder2D.cs index 93b9bc0..9aa51e7 100644 --- a/PathFinding/PathFinder2D.cs +++ b/PathFinding/PathFinder2D.cs @@ -1,10 +1,110 @@ - +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + namespace PathFinding { - public class PathFinder2D + public class PathFinder2D where T : IPlanarElement { - public PathFinder2D() + /// + /// + /// Guaranteed to be non-null. + /// + public delegate void Callback(T collider, Vector2 position); + + + private readonly IPlanarCollection collection; + private readonly int width; + private readonly int height; + public PathFinder2D(IPlanarCollection collection) { + this.collection = collection; + width = collection.GetLength(0); + height = collection.GetLength(1); + } + + /// + /// Navigate the collection such that each "step" is always towards the destination. + /// + /// 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) + { + if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0) + { + return false; + } + + var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); + var next = Vector2.Add(origin, path.Direction); + + if (!IsPathable(origin, destination, next)) + { + // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. + return false; + } + + var shouldPath = true; + while (shouldPath) + { + 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) + { + shouldPath = false; + } + next = Vector2.Add(next, path.Direction); + } + return true; + } + + public void PathEvery(IPlanarElement element, Vector2 from, Callback callback) + { + foreach (var path in element.GetPaths()) + { + var shouldPath = true; + var next = Vector2.Add(from, path.Direction); + while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) + { + var collider = collection[(int)next.X, (int)next.Y]; + if (collider != null) + { + callback(collider, next); + } + next = Vector2.Add(from, path.Direction); + if (path.Distance == Distance.OneStep) + { + shouldPath = false; + } + } + } + } + + 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); + + + 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); + } + public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 next) + { + if (Vector2.Distance(next, destination) < Vector2.Distance(origin, destination)) + { + // 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 false; } } }