diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs index d5333f4..b7c58fe 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs +++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs @@ -1,6 +1,7 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using System.Text; using System.Text.RegularExpressions; +using Domain = Shogi.Domain; namespace Gameboard.ShogiUI.Sockets.Extensions { diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index e05ef31..4ba5c59 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -22,6 +22,7 @@ + diff --git a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs index 9c565f6..8b130c7 100644 --- a/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs +++ b/Gameboard.ShogiUI.xUnitTests/ShogiShould.cs @@ -7,6 +7,7 @@ using Xunit; using Xunit.Abstractions; using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece; using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective; +using ShogiModel = Gameboard.ShogiUI.Sockets.Models.Shogi; namespace Gameboard.ShogiUI.xUnitTests { @@ -22,7 +23,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void InitializeBoardState() { // Act - var board = new Shogi().Board; + var board = new ShogiModel().Board; // Assert board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); @@ -204,7 +205,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P1 Pawn new Move("A3", "A4") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); shogi.Board["A3"].Should().BeNull(); shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); } @@ -222,7 +223,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P1 Bishop puts P2 in check new Move("B2", "G7"), }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); shogi.InCheck.Should().Be(WhichPerspective.Player2); // Act - P2 is able to un-check theirself. @@ -239,7 +240,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_MoveFromEmptyPosition() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); shogi.Board["D5"].Should().BeNull(); // Act @@ -255,7 +256,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_MoveToCurrentPosition() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); // Act - P1 "moves" pawn to the position it already exists at. var moveSuccess = shogi.Move(new Move("A3", "A3")); @@ -271,7 +272,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_MoveSet() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); // Act - Move Lance illegally var moveSuccess = shogi.Move(new Move("A1", "D5")); @@ -288,7 +289,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_Ownership() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); shogi.WhoseTurn.Should().Be(WhichPerspective.Player1); shogi.Board["A7"].Owner.Should().Be(WhichPerspective.Player2); @@ -305,7 +306,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_MoveThroughAllies() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); // Act - Move P1 Lance through P1 Pawn. var moveSuccess = shogi.Move(new Move("A1", "A5")); @@ -321,7 +322,7 @@ namespace Gameboard.ShogiUI.xUnitTests public void PreventInvalidMoves_CaptureAlly() { // Arrange - var shogi = new Shogi(); + var shogi = new ShogiModel(); // Act - P1 Knight tries to capture P1 Pawn. var moveSuccess = shogi.Move(new Move("B1", "C3")); @@ -347,7 +348,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P1 Bishop puts P2 in check new Move("B2", "G7") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); shogi.InCheck.Should().Be(WhichPerspective.Player2); // Act - P2 moves Lance while in check. @@ -387,7 +388,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P2 Pawn captures P1 Pawn new Move("I4", "I3") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); shogi.Player1Hand.Count.Should().Be(4); shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); @@ -448,7 +449,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P1 drop Bishop, place P2 in check new Move(WhichPiece.Bishop, "G7") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); shogi.InCheck.Should().Be(WhichPerspective.Player2); shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.Board["E5"].Should().BeNull(); @@ -478,7 +479,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P2 Pawn new Move("G6", "G5") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); using (new AssertionScope()) { shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); @@ -512,7 +513,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P2 Pawn new Move("G7", "G6"), }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); // Act - P1 Bishop, check shogi.Move(new Move("B2", "G7")); @@ -532,7 +533,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P2 Pawn new Move("G7", "G6" ) }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); // Act - P1 moves across promote threshold. var moveSuccess = shogi.Move(new Move("B2", "G7", true)); @@ -576,7 +577,7 @@ namespace Gameboard.ShogiUI.xUnitTests // P2 King retreat new Move("E8", "E9"), }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); output.WriteLine(shogi.PrintStateAsAscii()); // Act - P1 Pawn wins by checkmate. @@ -598,7 +599,7 @@ namespace Gameboard.ShogiUI.xUnitTests new Move("C3", "C4"), new Move("G7", "G6") }; - var shogi = new Shogi(moves); + var shogi = new ShogiModel(moves); // Act - P1 Bishop captures P2 Bishop var moveSuccess = shogi.Move(new Move("B2", "H8")); diff --git a/Shogi.Domain.UnitTests/RookShould.cs b/Shogi.Domain.UnitTests/RookShould.cs new file mode 100644 index 0000000..0a2bcf4 --- /dev/null +++ b/Shogi.Domain.UnitTests/RookShould.cs @@ -0,0 +1,227 @@ +using FluentAssertions; +using Shogi.Domain.Pathing; +using Shogi.Domain.Pieces; +using System.Numerics; +using Xunit; + +namespace Shogi.Domain.UnitTests +{ + public class RookShould + { + public class MoveSet + { + private readonly Rook rook1; + private readonly Rook rook2; + + public MoveSet() + { + this.rook1 = new Rook(WhichPlayer.Player1); + this.rook2 = new Rook(WhichPlayer.Player2); + } + + [Fact] + public void Player1_HasCorrectMoveSet() + { + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + } + + [Fact] + public void Player1_Promoted_HasCorrectMoveSet() + { + // Arrange + rook1.Promote(); + rook1.IsPromoted.Should().BeTrue(); + + // Assert + var moveSet = rook1.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + } + + [Fact] + public void Player2_HasCorrectMoveSet() + { + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(4); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + } + + [Fact] + public void Player2_Promoted_HasCorrectMoveSet() + { + // Arrange + rook2.Promote(); + rook2.IsPromoted.Should().BeTrue(); + + // Assert + var moveSet = rook2.MoveSet; + moveSet.Should().HaveCount(8); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + } + + + } + private readonly Rook rookPlayer1; + private readonly Rook rookPlayer2; + + public RookShould() + { + this.rookPlayer1 = new Rook(WhichPlayer.Player1); + this.rookPlayer2 = new Rook(WhichPlayer.Player2); + } + + [Fact] + public void Promote() + { + this.rookPlayer1.IsPromoted.Should().BeFalse(); + this.rookPlayer1.CanPromote.Should().BeTrue(); + this.rookPlayer1.Promote(); + this.rookPlayer1.IsPromoted.Should().BeTrue(); + this.rookPlayer1.CanPromote.Should().BeFalse(); + } + + [Fact] + public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } + + [Fact] + public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); + } + + [Fact] + public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } + + [Fact] + public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); + } + + [Fact] + public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } + + [Fact] + public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeFalse(); + steps.Should().BeEmpty(); + } + + [Fact] + public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(0, 5); + rookPlayer1.Promote(); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(5); + steps.Should().Contain(new Vector2(0, 1)); + steps.Should().Contain(new Vector2(0, 2)); + steps.Should().Contain(new Vector2(0, 3)); + steps.Should().Contain(new Vector2(0, 4)); + steps.Should().Contain(new Vector2(0, 5)); + } + + [Fact] + public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() + { + Vector2 start = new(0, 0); + Vector2 end = new(1, 1); + rookPlayer1.Promote(); + + var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + + rookPlayer1.IsPromoted.Should().BeTrue(); + steps.Should().HaveCount(1); + steps.Should().Contain(new Vector2(1, 1)); + } + } +} diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs index 2e8f734..bde591c 100644 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -1,7 +1,6 @@ using FluentAssertions; using FluentAssertions.Execution; using System; -using System.Linq; using Xunit; using Xunit.Abstractions; @@ -9,10 +8,10 @@ namespace Shogi.Domain.UnitTests { public class ShogiShould { - private readonly ITestOutputHelper output; - public ShogiShould(ITestOutputHelper output) + private readonly ITestOutputHelper logger; + public ShogiShould(ITestOutputHelper logger) { - this.output = output; + this.logger = logger; } [Fact] @@ -73,6 +72,8 @@ namespace Shogi.Domain.UnitTests act.Should().Throw(); board["D5"].Should().BeNull(); board["D6"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); } [Fact] @@ -81,16 +82,19 @@ namespace Shogi.Domain.UnitTests // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); + var expectedPiece = board["A3"]; // Act - P1 "moves" pawn to the position it already exists at. var act = () => shogi.Move("A3", "A3", false); // Assert - act.Should().Throw(); - board["A3"].Should().NotBeNull(); - board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); + using (new AssertionScope()) + { + act.Should().Throw(); + board["A3"].Should().Be(expectedPiece); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } } [Fact] @@ -99,17 +103,21 @@ namespace Shogi.Domain.UnitTests // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); + var expectedPiece = board["A1"]; + expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); // Act - Move Lance illegally var act = () => shogi.Move("A1", "D5", false); // Assert - act.Should().Throw(); - board["A1"].Should().NotBeNull(); - board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); + using (new AssertionScope()) + { + act.Should().Throw(); + board["A1"].Should().Be(expectedPiece); + board["A5"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } } [Fact] @@ -118,18 +126,20 @@ namespace Shogi.Domain.UnitTests // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); + var expectedPiece = board["A7"]; + expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); board.WhoseTurn.Should().Be(WhichPlayer.Player1); - board["A7"].Should().NotBeNull(); - board["A7"]!.Owner.Should().Be(WhichPlayer.Player2); // Act - Move Player2 Pawn when it is Player1 turn. var act = () => shogi.Move("A7", "A6", false); // Assert - act.Should().Throw(); - board["A7"].Should().NotBeNull(); - board["A7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A6"].Should().BeNull(); + using (new AssertionScope()) + { + act.Should().Throw(); + board["A7"].Should().Be(expectedPiece); + board["A6"].Should().BeNull(); + } } [Fact] @@ -138,17 +148,21 @@ namespace Shogi.Domain.UnitTests // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); + var lance = board["A1"]; + var pawn = board["A3"]; + lance!.Owner.Should().Be(pawn!.Owner); // Act - Move P1 Lance through P1 Pawn. var act = () => shogi.Move("A1", "A5", false); // Assert - act.Should().Throw(); - board["A1"].Should().NotBeNull(); - board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A3"].Should().NotBeNull(); - board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A5"].Should().BeNull(); + using (new AssertionScope()) + { + act.Should().Throw(); + board["A1"].Should().Be(lance); + board["A3"].Should().Be(pawn); + board["A5"].Should().BeNull(); + } } [Fact] @@ -157,18 +171,22 @@ namespace Shogi.Domain.UnitTests // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); + var knight = board["B1"]; + var pawn = board["C3"]; + knight!.Owner.Should().Be(pawn!.Owner); // Act - P1 Knight tries to capture P1 Pawn. var act = () => shogi.Move("B1", "C3", false); // Arrange - act.Should().Throw(); - board["B1"].Should().NotBeNull(); - board["B1"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["C3"].Should().NotBeNull(); - board["C3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); + using (new AssertionScope()) + { + act.Should().Throw(); + board["B1"].Should().Be(knight); + board["C3"].Should().Be(pawn); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } } [Fact] @@ -184,26 +202,28 @@ namespace Shogi.Domain.UnitTests // P1 Bishop puts P2 in check shogi.Move("B2", "G7", false); board.InCheck.Should().Be(WhichPlayer.Player2); + var lance = board["I9"]; // Act - P2 moves Lance while in check. var act = () => shogi.Move("I9", "I8", false); // Assert - act.Should().Throw(); - board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().NotBeNull(); - board["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["I8"].Should().BeNull(); + using (new AssertionScope()) + { + act.Should().Throw(); + board.InCheck.Should().Be(WhichPlayer.Player2); + board["I9"].Should().Be(lance); + board["I8"].Should().BeNull(); + } } [Fact] + // TODO: Consider nesting classes to share this setup in a constructor but have act and assert as separate facts. public void PreventInvalidDrops_MoveSet() { // Arrange var board = new ShogiBoardState(); var shogi = new Shogi(board); - - // P1 Pawn shogi.Move("C3", "C4", false); // P2 Pawn @@ -224,7 +244,6 @@ namespace Shogi.Domain.UnitTests shogi.Move("H9", "I9", false); // P2 Pawn captures P1 Pawn shogi.Move("I4", "I3", false); - board.Player1Hand.Count.Should().Be(4); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); @@ -232,37 +251,36 @@ namespace Shogi.Domain.UnitTests board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); board.WhoseTurn.Should().Be(WhichPlayer.Player1); - // Act | Assert - Illegally placing Knight from the hand in farthest row. + // Act | Assert - Illegally placing Knight from the hand in farthest rank. board["H9"].Should().BeNull(); - shogi.Move() - var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + var act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally placing Knight from the hand in second farthest row. - shogi.Board["H8"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8")); - dropSuccess.Should().BeFalse(); - shogi.Board["H8"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + board["H8"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H8"); + act.Should().Throw(); + board["H8"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally place Lance from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act | Assert - Illegally place Pawn from the hand. - shogi.Board["H9"].Should().BeNull(); - dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9")); - dropSuccess.Should().BeFalse(); - shogi.Board["H9"].Should().BeNull(); - shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Pawn, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // TODO + // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // // TODO } //[Fact] @@ -289,14 +307,14 @@ namespace Shogi.Domain.UnitTests // var shogi = new Shogi(moves); // shogi.InCheck.Should().Be(WhichPlayer.Player2); // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // shogi.Board["E5"].Should().BeNull(); + // boardState["E5"].Should().BeNull(); // // Act - P2 places a Bishop while in check. // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); // // Assert // dropSuccess.Should().BeFalse(); - // shogi.Board["E5"].Should().BeNull(); + // boardState["E5"].Should().BeNull(); // shogi.InCheck.Should().Be(WhichPlayer.Player2); // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); //} @@ -320,9 +338,9 @@ namespace Shogi.Domain.UnitTests // using (new AssertionScope()) // { // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // shogi.Board["I9"].Should().NotBeNull(); - // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + // boardState["I9"].Should().NotBeNull(); + // boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // boardState["I9"].Owner.Should().Be(WhichPlayer.Player2); // } // // Act - P1 tries to place a piece where an opponent's piece resides. @@ -333,126 +351,112 @@ namespace Shogi.Domain.UnitTests // { // dropSuccess.Should().BeFalse(); // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // shogi.Board["I9"].Should().NotBeNull(); - // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2); + // boardState["I9"].Should().NotBeNull(); + // boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // boardState["I9"].Owner.Should().Be(WhichPlayer.Player2); // } //} - //[Fact] - //public void Check() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // }; - // var shogi = new Shogi(moves); + [Fact] + public void Check() + { + // Arrange + var boardState = new ShogiBoardState(); + var shogi = new Shogi(boardState); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); - // // Act - P1 Bishop, check - // shogi.Move(new Move("B2", "G7")); + // Act - P1 Bishop, check + shogi.Move("B2", "G7", false); - // // Assert - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - //} + // Assert + boardState.InCheck.Should().Be(WhichPlayer.Player2); + } - //[Fact] - //public void Promote() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4" ), - // // P2 Pawn - // new Move("G7", "G6" ) - // }; - // var shogi = new Shogi(moves); + [Fact] + public void Promote() + { + // Arrange + var boardState = new ShogiBoardState(); + var shogi = new Shogi(boardState); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); - // // Act - P1 moves across promote threshold. - // var moveSuccess = shogi.Move(new Move("B2", "G7", true)); + // Act - P1 moves across promote threshold. + shogi.Move("B2", "G7", true); - // // Assert - // using (new AssertionScope()) - // { - // moveSuccess.Should().BeTrue(); - // shogi.Board["B2"].Should().BeNull(); - // shogi.Board["G7"].Should().NotBeNull(); - // shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop); - // shogi.Board["G7"].Owner.Should().Be(WhichPlayer.Player1); - // shogi.Board["G7"].IsPromoted.Should().BeTrue(); - // } - //} + // Assert + using (new AssertionScope()) + { + boardState["B2"].Should().BeNull(); + boardState["G7"].Should().NotBeNull(); + boardState["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); + boardState["G7"]!.Owner.Should().Be(WhichPlayer.Player1); + boardState["G7"]!.IsPromoted.Should().BeTrue(); + } + } - //[Fact] - //public void CheckMate() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Rook - // new Move("H2", "E2"), - // // P2 Gold - // new Move("F9", "G8"), - // // P1 Pawn - // new Move("E3", "E4"), - // // P2 other Gold - // new Move("D9", "C8"), - // // P1 same Pawn - // new Move("E4", "E5"), - // // P2 Pawn - // new Move("E7", "E6"), - // // P1 Pawn takes P2 Pawn - // new Move("E5", "E6"), - // // P2 King - // new Move("E9", "E8"), - // // P1 Pawn promotes, threatens P2 King - // new Move("E6", "E7", true), - // // P2 King retreat - // new Move("E8", "E9"), - // }; - // var shogi = new Shogi(moves); - // output.WriteLine(shogi.PrintStateAsAscii()); + [Fact] + public void Capture() + { + // Arrange + var boardState = new ShogiBoardState(); + var shogi = new Shogi(boardState); + var p1Bishop = boardState["B2"]; + p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); + shogi.Move("C3", "C4", false); + shogi.Move("G7", "G6", false); - // // Act - P1 Pawn wins by checkmate. - // var moveSuccess = shogi.Move(new Move("E7", "E8")); - // output.WriteLine(shogi.PrintStateAsAscii()); + // Act - P1 Bishop captures P2 Bishop + shogi.Move("B2", "H8", false); - // // Assert - checkmate - // moveSuccess.Should().BeTrue(); - // shogi.IsCheckmate.Should().BeTrue(); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - //} + // Assert + boardState["B2"].Should().BeNull(); + boardState["H8"].Should().Be(p1Bishop); - //[Fact] - //public void Capture() - //{ - // // Arrange - // var moves = new[] - // { - // new Move("C3", "C4"), - // new Move("G7", "G6") - // }; - // var shogi = new Shogi(moves); + boardState + .Player1Hand + .Should() + .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + } - // // Act - P1 Bishop captures P2 Bishop - // var moveSuccess = shogi.Move(new Move("B2", "H8")); + [Fact] + public void CheckMate() + { + // Arrange + var boardState = new ShogiBoardState(); + var shogi = new Shogi(boardState); + // P1 Rook + shogi.Move("H2", "E2", false); + // P2 Gold + shogi.Move("F9", "G8", false); + // P1 Pawn + shogi.Move("E3", "E4", false); + // P2 other Gold + shogi.Move("D9", "C8", false); + // P1 same Pawn + shogi.Move("E4", "E5", false); + // P2 Pawn + shogi.Move("E7", "E6", false); + // P1 Pawn takes P2 Pawn + shogi.Move("E5", "E6", false); + // P2 King + shogi.Move("E9", "E8", false); + // P1 Pawn promotes; threatens P2 King + shogi.Move("E6", "E7", true); + // P2 King retreat + shogi.Move("E8", "E9", false); - // // Assert - // moveSuccess.Should().BeTrue(); - // shogi.Board["B2"].Should().BeNull(); - // shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop); - // shogi.Board["H8"].Owner.Should().Be(WhichPlayer.Player1); - // shogi.Board.Values - // .Where(p => p != null) - // .Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop); + // Act - P1 Pawn wins by checkmate. + shogi.Move("E7", "E8", false); - // shogi.Player1Hand - // .Should() - // .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - //} + // Assert - checkmate + boardState.IsCheckmate.Should().BeTrue(); + boardState.InCheck.Should().Be(WhichPlayer.Player2); + } } } diff --git a/Shogi.Domain.UnitTests/StandardRulesShould.cs b/Shogi.Domain.UnitTests/StandardRulesShould.cs deleted file mode 100644 index 7f132f3..0000000 --- a/Shogi.Domain.UnitTests/StandardRulesShould.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using Xunit; - -namespace Shogi.Domain.UnitTests -{ - public class StandardRulesShould - { - [Fact] - public void AllowValidMoves_AfterCheck() - { - // Arrange - var board = new ShogiBoardState(); - var rules = new StandardRules(board); - rules.Move("C3", "C4"); // P1 Pawn - rules.Move("G7", "G6"); // P2 Pawn - rules.Move("B2", "G7"); // P1 Bishop puts P2 in check - board.InCheck.Should().Be(WhichPlayer.Player2); - - // Act - P2 is able to un-check theirself. - /// P2 King moves out of check - var moveSuccess = rules.Move("E9", "E8"); - - // Assert - using var _ = new AssertionScope(); - moveSuccess.Success.Should().BeTrue(); - moveSuccess.Reason.Should().BeEmpty(); - board.InCheck.Should().BeNull(); - } - } -} diff --git a/Shogi.Domain/BoardTile.cs b/Shogi.Domain/BoardTile.cs new file mode 100644 index 0000000..4662e01 --- /dev/null +++ b/Shogi.Domain/BoardTile.cs @@ -0,0 +1,17 @@ +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/Move.cs b/Shogi.Domain/Move.cs deleted file mode 100644 index 1ad6f08..0000000 --- a/Shogi.Domain/Move.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Diagnostics; -using System.Numerics; - -namespace Shogi.Domain -{ - [DebuggerDisplay("{Direction} - {Distance}")] - public class Move - { - public Vector2 Direction { get; } - public Distance Distance { get; } - public Move(Vector2 direction, Distance distance = Distance.OneStep) - { - Direction = direction; - Distance = distance; - } - } -} diff --git a/Shogi.Domain/MoveSet.cs b/Shogi.Domain/MoveSet.cs deleted file mode 100644 index b1b2d2d..0000000 --- a/Shogi.Domain/MoveSet.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Numerics; - -namespace Shogi.Domain -{ - public class MoveSet - { - - public static readonly MoveSet King = new(new List(8) - { - new Move(Direction.Up), - new Move(Direction.Left), - new Move(Direction.Right), - new Move(Direction.Down), - new Move(Direction.UpLeft), - new Move(Direction.UpRight), - new Move(Direction.DownLeft), - new Move(Direction.DownRight) - }); - - public static readonly MoveSet Bishop = new(new List(4) - { - new Move(Direction.UpLeft, Distance.MultiStep), - new Move(Direction.UpRight, Distance.MultiStep), - new Move(Direction.DownLeft, Distance.MultiStep), - new Move(Direction.DownRight, Distance.MultiStep) - }); - - public static readonly MoveSet PromotedBishop = new(new List(8) - { - new Move(Direction.Up), - new Move(Direction.Left), - new Move(Direction.Right), - new Move(Direction.Down), - new Move(Direction.UpLeft, Distance.MultiStep), - new Move(Direction.UpRight, Distance.MultiStep), - new Move(Direction.DownLeft, Distance.MultiStep), - new Move(Direction.DownRight, Distance.MultiStep) - }); - - public static readonly MoveSet GoldGeneral = new(new List(6) - { - new Move(Direction.Up), - new Move(Direction.UpLeft), - new Move(Direction.UpRight), - new Move(Direction.Left), - new Move(Direction.Right), - new Move(Direction.Down) - }); - - public static readonly MoveSet Knight = new(new List(2) - { - new Move(Direction.KnightLeft), - new Move(Direction.KnightRight) - }); - - public static readonly MoveSet Lance = new(new List(1) - { - new Move(Direction.Up, Distance.MultiStep), - }); - - public static readonly MoveSet Pawn = new(new List(1) - { - new Move(Direction.Up) - }); - - public static readonly MoveSet Rook = new(new List(4) - { - new Move(Direction.Up, Distance.MultiStep), - new Move(Direction.Left, Distance.MultiStep), - new Move(Direction.Right, Distance.MultiStep), - new Move(Direction.Down, Distance.MultiStep) - }); - - public static readonly MoveSet PromotedRook = new(new List(8) - { - new Move(Direction.Up, Distance.MultiStep), - new Move(Direction.Left, Distance.MultiStep), - new Move(Direction.Right, Distance.MultiStep), - new Move(Direction.Down, Distance.MultiStep), - new Move(Direction.UpLeft), - new Move(Direction.UpRight), - new Move(Direction.DownLeft), - new Move(Direction.DownRight) - }); - - public static readonly MoveSet SilverGeneral = new(new List(4) - { - new Move(Direction.Up), - new Move(Direction.UpLeft), - new Move(Direction.UpRight), - new Move(Direction.DownLeft), - new Move(Direction.DownRight) - }); - - private readonly ICollection moves; - private readonly ICollection upsidedownMoves; - - private MoveSet(ICollection moves) - { - this.moves = moves; - upsidedownMoves = moves.Select(m => new Move(Vector2.Negate(m.Direction), m.Distance)).ToList(); - } - - public ICollection GetMoves(bool isUpsideDown) => isUpsideDown ? upsidedownMoves : moves; - } -} diff --git a/Shogi.Domain/Direction.cs b/Shogi.Domain/Pathing/Direction.cs similarity index 76% rename from Shogi.Domain/Direction.cs rename to Shogi.Domain/Pathing/Direction.cs index 5316598..4aae4e5 100644 --- a/Shogi.Domain/Direction.cs +++ b/Shogi.Domain/Pathing/Direction.cs @@ -1,7 +1,11 @@ using System.Numerics; -namespace Shogi.Domain +namespace Shogi.Domain.Pathing { + /// + /// Directions are relative to the perspective of Player 1. + /// Up points towards player 1. Down points towards player 2. + /// public static class Direction { public static readonly Vector2 Up = new(0, 1); diff --git a/Shogi.Domain/Distance.cs b/Shogi.Domain/Pathing/Distance.cs similarity index 64% rename from Shogi.Domain/Distance.cs rename to Shogi.Domain/Pathing/Distance.cs index 9031228..b1078f5 100644 --- a/Shogi.Domain/Distance.cs +++ b/Shogi.Domain/Pathing/Distance.cs @@ -1,4 +1,4 @@ -namespace Shogi.Domain +namespace Shogi.Domain.Pathing { public enum Distance { diff --git a/Shogi.Domain/Pathing/Path.cs b/Shogi.Domain/Pathing/Path.cs new file mode 100644 index 0000000..5c24275 --- /dev/null +++ b/Shogi.Domain/Pathing/Path.cs @@ -0,0 +1,45 @@ +using Shogi.Domain.Pieces; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +namespace Shogi.Domain.Pathing +{ + [DebuggerDisplay("{Direction} - {Distance}")] + public class Path + { + public Vector2 Direction { get; } + public Distance Distance { get; } + public Path(Vector2 direction, Distance distance = Distance.OneStep) + { + Direction = direction; + Distance = distance; + } + + public Path Invert() => new(Vector2.Negate(Direction), Distance); + } + + public static class PathExtensions + { + public static Path GetNearestPath(this IEnumerable paths, Vector2 start, Vector2 end) + { + if (!paths.DefaultIfEmpty().Any()) + { + throw new ArgumentException("No paths to get nearest path from."); + } + var shortestPath = paths.First(); + foreach (var path in paths.Skip(1)) + { + var distance = Vector2.Distance(start + path.Direction, end); + var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); + if (distance < shortestDistance) + { + shortestPath = path; + } + } + return shortestPath; + } + } +} diff --git a/Shogi.Domain/Piece.cs b/Shogi.Domain/Piece.cs deleted file mode 100644 index 0ccc94f..0000000 --- a/Shogi.Domain/Piece.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Diagnostics; - -namespace Shogi.Domain -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public class Piece - { - public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } - public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPlayer.Player2; - - public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = isPromoted; - } - public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted) - { - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void ToggleOwnership() - { - Owner = Owner == WhichPlayer.Player1 - ? WhichPlayer.Player2 - : WhichPlayer.Player1; - } - - public void Promote() => IsPromoted = CanPromote; - - public void Demote() => IsPromoted = false; - - public void Capture() - { - ToggleOwnership(); - Demote(); - } - } -} diff --git a/Shogi.Domain/Pieces/Bishop.cs b/Shogi.Domain/Pieces/Bishop.cs new file mode 100644 index 0000000..64768ae --- /dev/null +++ b/Shogi.Domain/Pieces/Bishop.cs @@ -0,0 +1,47 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class Bishop : Piece + { + private static readonly ReadOnlyCollection BishopPaths = new(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) + }); + + public static readonly ReadOnlyCollection PromotedBishopPaths = new(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 static readonly ReadOnlyCollection Player2Paths = + BishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public static readonly ReadOnlyCollection Player2PromotedPaths = + PromotedBishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Bishop(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Bishop, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths; + } +} diff --git a/Shogi.Domain/Pieces/GoldGeneral.cs b/Shogi.Domain/Pieces/GoldGeneral.cs new file mode 100644 index 0000000..c6dc85e --- /dev/null +++ b/Shogi.Domain/Pieces/GoldGeneral.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class GoldGeneral : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(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 static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public GoldGeneral(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.GoldGeneral, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => Player1Paths; + } +} diff --git a/Shogi.Domain/Pieces/King.cs b/Shogi.Domain/Pieces/King.cs new file mode 100644 index 0000000..7093b73 --- /dev/null +++ b/Shogi.Domain/Pieces/King.cs @@ -0,0 +1,27 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class King : Piece + { + internal static readonly ReadOnlyCollection KingPaths = new(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, bool isPromoted = false) + : base(WhichPiece.King, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => KingPaths; + } +} diff --git a/Shogi.Domain/Pieces/Knight.cs b/Shogi.Domain/Pieces/Knight.cs new file mode 100644 index 0000000..d5fdb71 --- /dev/null +++ b/Shogi.Domain/Pieces/Knight.cs @@ -0,0 +1,32 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class Knight : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(2) + { + new Path(Direction.KnightLeft), + new Path(Direction.KnightRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Knight(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Knight, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/Pieces/Lance.cs b/Shogi.Domain/Pieces/Lance.cs new file mode 100644 index 0000000..7dcb61b --- /dev/null +++ b/Shogi.Domain/Pieces/Lance.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class Lance : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Up, Distance.MultiStep), + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Lance(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Lance, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/Pieces/Pawn.cs b/Shogi.Domain/Pieces/Pawn.cs new file mode 100644 index 0000000..e357691 --- /dev/null +++ b/Shogi.Domain/Pieces/Pawn.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class Pawn : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Up) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Pawn(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Pawn, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/Pieces/Piece.cs b/Shogi.Domain/Pieces/Piece.cs new file mode 100644 index 0000000..832aefb --- /dev/null +++ b/Shogi.Domain/Pieces/Piece.cs @@ -0,0 +1,78 @@ +using Shogi.Domain.Pathing; +using System.Diagnostics; + +namespace Shogi.Domain.Pieces +{ + [DebuggerDisplay("{WhichPiece} {Owner}")] + public abstract class Piece + { + /// + /// Creates a clone of an existing piece. + /// + public static Piece Create(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + return piece switch + { + WhichPiece.King => new King(owner, isPromoted), + WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted), + WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted), + WhichPiece.Bishop => new Bishop(owner, isPromoted), + WhichPiece.Rook => new Rook(owner, isPromoted), + WhichPiece.Knight => new Knight(owner, isPromoted), + WhichPiece.Lance => new Lance(owner, isPromoted), + WhichPiece.Pawn => new Pawn(owner, isPromoted), + _ => 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 MoveSet { get; } + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; + + protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = isPromoted; + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void Promote() => IsPromoted = CanPromote; + + /// + /// Prep the piece for capture by changing ownership and demoting. + /// + public void Capture(WhichPlayer newOwner) + { + Owner = newOwner; + IsPromoted = false; + } + + public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) + { + var steps = new List(10); + + var path = this.MoveSet.GetNearestPath(start, end); + var position = start; + while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) + { + position += path.Direction; + steps.Add(position); + } + + if (position == end) + { + return steps; + } + + return Array.Empty(); + } + } +} diff --git a/Shogi.Domain/Pieces/Rook.cs b/Shogi.Domain/Pieces/Rook.cs new file mode 100644 index 0000000..c91f0f2 --- /dev/null +++ b/Shogi.Domain/Pieces/Rook.cs @@ -0,0 +1,52 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + public sealed class Rook : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(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 ReadOnlyCollection PromotedPlayer1Paths = new(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 static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(m => m.Invert()) + .ToList() + .AsReadOnly(); + + public static readonly ReadOnlyCollection Player2PromotedPaths = + PromotedPlayer1Paths + .Select(m => m.Invert()) + .ToList() + .AsReadOnly(); + + public Rook(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Rook, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/Pieces/SilverGeneral.cs b/Shogi.Domain/Pieces/SilverGeneral.cs new file mode 100644 index 0000000..f2c8623 --- /dev/null +++ b/Shogi.Domain/Pieces/SilverGeneral.cs @@ -0,0 +1,35 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.Pieces +{ + internal class SilverGeneral : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(4) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public SilverGeneral(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.SilverGeneral, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj index 132c02c..5ca4174 100644 --- a/Shogi.Domain/Shogi.Domain.csproj +++ b/Shogi.Domain/Shogi.Domain.csproj @@ -1,9 +1,16 @@ - + - - net6.0 - enable - enable - + + net6.0 + disable + enable + + + + + + + + diff --git a/Shogi.Domain/Shogi.cs b/Shogi.Domain/Shogi.cs index b895ffa..5b16827 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -1,4 +1,8 @@ -namespace Shogi.Domain +using Shogi.Domain.Pieces; +using System; +using System.Text; + +namespace Shogi.Domain { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. @@ -7,106 +11,232 @@ /// public sealed class Shogi { - private readonly ShogiBoardState board; + private readonly ShogiBoardState boardState; private readonly StandardRules rules; - public string Error { get; private set; } + + public Shogi() : this(new ShogiBoardState()) + { + } public Shogi(ShogiBoardState board) { - this.board = board; - rules = new StandardRules(this.board); - } - - //public Shogi(IList moves) : this(new ShogiBoardState()) - //{ - // 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}."); - // } - // } - //} - - - public MoveResult CanMove(string from, string to, bool isPromotion) - { - var simulator = new StandardRules(new ShogiBoardState(board)); - return simulator.Move(from, to, isPromotion); - } - - public MoveResult CanMove(WhichPiece pieceInHand, string to) - { - var simulator = new StandardRules(new ShogiBoardState(board)); - return simulator.Move(pieceInHand, to); + this.boardState = board; + rules = new StandardRules(this.boardState); } + /// + /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. + /// + /// + /// The strategy involves simulating a move on a throw-away board state that can be used to + /// validate legal vs illegal moves without having to worry about reverting board state. + /// + /// public void Move(string from, string to, bool isPromotion) { - var tempBoard = new ShogiBoardState(board); - var simulation = new StandardRules(tempBoard); + var simulationState = new ShogiBoardState(boardState); + var simulation = new StandardRules(simulationState); var moveResult = simulation.Move(from, to, isPromotion); if (!moveResult.Success) { throw new InvalidOperationException(moveResult.Reason); } - var fromVector = ShogiBoardState.FromBoardNotation(from); - var toVector = ShogiBoardState.FromBoardNotation(to); - var otherPlayer = board.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (simulation.IsPlayerInCheckAfterMove(fromVector, toVector, board.WhoseTurn)) + // If already in check, assert the move that resulted in check no longer results in check. + if (boardState.InCheck == boardState.WhoseTurn + && simulation.IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo)) + { + throw new InvalidOperationException("Unable to move because you are still in check."); + } + + var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (simulation.IsPlayerInCheckAfterMove()) { throw new InvalidOperationException("Illegal move. This move places you in check."); } - rules.Move(from, to, isPromotion); - if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) + _ = rules.Move(from, to, isPromotion); + if (rules.IsOpponentInCheckAfterMove()) { - board.InCheck = otherPlayer; - board.IsCheckmate = rules.EvaluateCheckmate(); + boardState.InCheck = otherPlayer; + // TODO: evaluate checkmate. + //if (rules.IsOpponentInCheckMate()) + //{ + // boardState.IsCheckmate = true; + //} } else { - board.InCheck = null; + boardState.InCheck = null; } - board.WhoseTurn = otherPlayer; + boardState.WhoseTurn = otherPlayer; } public void Move(WhichPiece pieceInHand, string to) { + var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); + if (index == -1) + { + throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); + } + if (boardState[to] != null) + { + throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); + } + + var toVector = ShogiBoardState.FromBoardNotation(to); + switch (pieceInHand) + { + case WhichPiece.Knight: + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6) + || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)) + { + throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); + } + break; + } + case WhichPiece.Lance: + case WhichPiece.Pawn: + { + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8) + || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)) + { + throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); + } + break; + } + } + + var tempBoard = new ShogiBoardState(boardState); + var simulation = new StandardRules(tempBoard); + var moveResult = simulation.Move(pieceInHand, to); + if (!moveResult.Success) + { + throw new InvalidOperationException(moveResult.Reason); + } + + var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (boardState.InCheck == boardState.WhoseTurn) + { + //if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn)) + //{ + // throw new InvalidOperationException("Illegal move. You're still in check!"); + //} + } + + var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition; + //if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer)) + //{ + + //} + + //rules.Move(from, to, isPromotion); + //if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) + //{ + // board.InCheck = otherPlayer; + // board.IsCheckmate = rules.EvaluateCheckmate(); + //} + //else + //{ + // board.InCheck = null; + //} + boardState.WhoseTurn = otherPlayer; } - ///// - ///// Attempts a given move. Returns false if the move is illegal. - ///// - //private bool TryMove(Move move) - //{ - // // Try making the move in a "throw away" board. - // var simulator = new StandardRules(new ShogiBoardState(this.board)); - // var simulatedMoveResults = move.PieceFromHand.HasValue - // ? simulator.PlaceFromHand(move) - // : simulator.PlaceFromBoard(move); - // if (!simulatedMoveResults) - // { - // // Surface the error description. - // Error = simulationBoard.Error; - // return false; - // } - // // If already in check, assert the move that resulted in check no longer results in check. - // if (InCheck == WhoseTurn) - // { - // // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - // if (simulationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) - // { - // return false; - // } - // } + /// + /// Prints a ASCII representation of the board for debugging board state. + /// + /// + public string ToStringStateAsAscii() + { + var builder = new StringBuilder(); + builder.Append(" "); + builder.Append("Player 2(.)"); + builder.AppendLine(); + for (var rank = 8; rank >= 0; rank--) + { + // Horizontal line + builder.Append(" - "); + for (var file = 0; file < 8; file++) builder.Append("- - "); + builder.Append("- -"); - // // The move is valid and legal; update board state. - // if (move.PieceFromHand.HasValue) PlaceFromHand(move); - // else PlaceFromBoard(move); - // return true; - //} + // Print Rank ruler. + builder.AppendLine(); + builder.Append($"{rank + 1} "); + + // Print pieces. + builder.Append(" |"); + for (var x = 0; x < 9; x++) + { + var piece = boardState[x, rank]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", ToAscii(piece)); + } + builder.Append('|'); + } + builder.AppendLine(); + } + + // Horizontal line + builder.Append(" - "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" "); + builder.Append("Player 1"); + + builder.AppendLine(); + builder.AppendLine(); + // Print File ruler. + builder.Append(" "); + builder.Append(" A B C D E F G H I "); + + return builder.ToString(); + } + + /// + /// + /// + /// + /// + /// A string with three characters. + /// The first character indicates promotion status. + /// The second character indicates piece. + /// The third character indicates ownership. + /// + public static string ToAscii(Piece piece) + { + var builder = new StringBuilder(); + if (piece.IsPromoted) builder.Append('^'); + else builder.Append(' '); + + var name = piece.WhichPiece switch + { + WhichPiece.King => "K", + WhichPiece.GoldGeneral => "G", + WhichPiece.SilverGeneral => "S", + WhichPiece.Bishop => "B", + WhichPiece.Rook => "R", + WhichPiece.Knight => "k", + WhichPiece.Lance => "L", + WhichPiece.Pawn => "P", + _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), + }; + builder.Append(name); + + if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); + else builder.Append(' '); + + return builder.ToString(); + } } } diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index 3d240bf..a678ff7 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using Shogi.Domain.Pieces; using System.Text.RegularExpressions; namespace Shogi.Domain @@ -7,7 +7,7 @@ namespace Shogi.Domain // Then validation can occur when assigning a piece to a position. public class ShogiBoardState { - private static readonly string BoardNotationRegex = @"(?[a-iA-I])(?[1-9])"; + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; private static readonly char A = 'A'; public delegate void ForEachDelegate(Piece element, Vector2 position); /// @@ -15,21 +15,33 @@ namespace Shogi.Domain /// private readonly Dictionary board; - public List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + 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 List MoveHistory { 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); + board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); InitializeBoardState(); Player1Hand = new List(); Player2Hand = new List(); - MoveHistory = new List(); + PreviousMoveTo = Vector2.Zero; + CacheKingPositions(); } @@ -40,21 +52,39 @@ namespace Shogi.Domain { foreach (var kvp in other.board) { - board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value); + // Replace copy constructor with static factory method in Piece.cs + board[kvp.Key] = kvp.Value == null ? null : Piece.Create(kvp.Value); } WhoseTurn = other.WhoseTurn; InCheck = other.InCheck; IsCheckmate = other.IsCheckmate; - MoveHistory.AddRange(other.MoveHistory); + 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.ToUpper()]; - set => board[notation.ToUpper()] = value; + 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; + } } public Piece? this[Vector2 vector] @@ -69,6 +99,22 @@ namespace Shogi.Domain set => this[ToBoardNotation(x, y)] = value; } + internal void RememberAsMostRecentMove(Vector2 from, Vector2 to) + { + PreviousMoveFrom = from; + PreviousMoveTo = to; + } + + /// + /// Returns true if the given path can be traversed without colliding into a piece. + /// + public bool IsPathBlocked(IEnumerable path) + { + 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++) @@ -83,11 +129,57 @@ namespace Shogi.Domain } } + internal bool IsWithinPromotionZone(Vector2 position) + { + return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) + || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); + } + + internal static bool IsWithinBoardBoundary(Vector2 position) + { + return position.X <= 8 && position.X >= 0 + && position.Y <= 8 && position.Y >= 0; + } + + internal IEnumerable GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + .Where(kvp => kvp.Value?.Owner == whichPlayer) + .Select(kvp => new BoardTile(kvp.Value!, FromBoardNotation(kvp.Key))); + + internal void Capture(Vector2 to) + { + var piece = this[to]; + if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); + + piece.Capture(WhoseTurn); + ActivePlayerHand.Add(piece); + } + + /// + /// Does not include the start position. + /// + internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) + { + var next = start; + while (IsWithinBoardBoundary(next + direction)) + { + next += direction; + yield return next; + } + } + + internal Piece? GetFirstPieceAlongPath(IEnumerable path) + { + foreach (var step in path) + { + if (this[step] != null) return this[step]; + } + return null; + } public static string ToBoardNotation(Vector2 vector) { return ToBoardNotation((int)vector.X, (int)vector.Y); } - public static string ToBoardNotation(int x, int y) + private static string ToBoardNotation(int x, int y) { var file = (char)(x + A); var rank = y + 1; @@ -95,10 +187,9 @@ namespace Shogi.Domain } public static Vector2 FromBoardNotation(string notation) { - notation = notation.ToUpper(); if (Regex.IsMatch(notation, BoardNotationRegex)) { - var match = Regex.Match(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); @@ -106,37 +197,55 @@ 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 Piece(WhichPiece.Lance, WhichPlayer.Player1); - this["B1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); - this["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); - this["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); - this["E1"] = new Piece(WhichPiece.King, WhichPlayer.Player1); - this["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1); - this["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1); - this["H1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1); - this["I1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1); + this["A1"] = new Lance(WhichPlayer.Player1); + this["B1"] = new Knight(WhichPlayer.Player1); + this["C1"] = new SilverGeneral(WhichPlayer.Player1); + this["D1"] = new GoldGeneral(WhichPlayer.Player1); + this["E1"] = new King(WhichPlayer.Player1); + this["F1"] = new GoldGeneral(WhichPlayer.Player1); + this["G1"] = new SilverGeneral(WhichPlayer.Player1); + this["H1"] = new Knight(WhichPlayer.Player1); + this["I1"] = new Lance(WhichPlayer.Player1); this["A2"] = null; - this["B2"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player1); + this["B2"] = new Bishop(WhichPlayer.Player1); this["C2"] = null; this["D2"] = null; this["E2"] = null; this["F2"] = null; this["G2"] = null; - this["H2"] = new Piece(WhichPiece.Rook, WhichPlayer.Player1); + this["H2"] = new Rook(WhichPlayer.Player1); this["I2"] = null; - this["A3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["B3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["C3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["D3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["E3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["F3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["G3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["H3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); - this["I3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1); + this["A3"] = new Pawn(WhichPlayer.Player1); + this["B3"] = new Pawn(WhichPlayer.Player1); + this["C3"] = new Pawn(WhichPlayer.Player1); + this["D3"] = new Pawn(WhichPlayer.Player1); + this["E3"] = new Pawn(WhichPlayer.Player1); + this["F3"] = new Pawn(WhichPlayer.Player1); + this["G3"] = new Pawn(WhichPlayer.Player1); + this["H3"] = new Pawn(WhichPlayer.Player1); + this["I3"] = new Pawn(WhichPlayer.Player1); this["A4"] = null; this["B4"] = null; @@ -168,35 +277,35 @@ namespace Shogi.Domain this["H6"] = null; this["I6"] = null; - this["A7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["B7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["C7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["D7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["E7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["F7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["G7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["H7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); - this["I7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2); + this["A7"] = new Pawn(WhichPlayer.Player2); + this["B7"] = new Pawn(WhichPlayer.Player2); + this["C7"] = new Pawn(WhichPlayer.Player2); + this["D7"] = new Pawn(WhichPlayer.Player2); + this["E7"] = new Pawn(WhichPlayer.Player2); + this["F7"] = new Pawn(WhichPlayer.Player2); + this["G7"] = new Pawn(WhichPlayer.Player2); + this["H7"] = new Pawn(WhichPlayer.Player2); + this["I7"] = new Pawn(WhichPlayer.Player2); this["A8"] = null; - this["B8"] = new Piece(WhichPiece.Rook, WhichPlayer.Player2); + this["B8"] = new Rook(WhichPlayer.Player2); this["C8"] = null; this["D8"] = null; this["E8"] = null; this["F8"] = null; this["G8"] = null; - this["H8"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player2); + this["H8"] = new Bishop(WhichPlayer.Player2); this["I8"] = null; - this["A9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); - this["B9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); - this["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); - this["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); - this["E9"] = new Piece(WhichPiece.King, WhichPlayer.Player2); - this["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2); - this["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2); - this["H9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2); - this["I9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2); + this["A9"] = new Lance(WhichPlayer.Player2); + this["B9"] = new Knight(WhichPlayer.Player2); + this["C9"] = new SilverGeneral(WhichPlayer.Player2); + this["D9"] = new GoldGeneral(WhichPlayer.Player2); + this["E9"] = new King(WhichPlayer.Player2); + this["F9"] = new GoldGeneral(WhichPlayer.Player2); + this["G9"] = new SilverGeneral(WhichPlayer.Player2); + this["H9"] = new Knight(WhichPlayer.Player2); + this["I9"] = new Lance(WhichPlayer.Player2); } } } diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 27e1f70..ac11ff7 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -1,41 +1,15 @@ -using System.Numerics; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Shogi.Domain.UnitTests")] namespace Shogi.Domain { internal class StandardRules { - /// Guaranteed to be non-null. - /// - public delegate void Callback(Piece collider, Vector2 position); + private readonly ShogiBoardState boardState; - private readonly ShogiBoardState board; - private Vector2 player1KingPosition; - private Vector2 player2KingPosition; - - public StandardRules(ShogiBoardState board) + internal StandardRules(ShogiBoardState board) { - this.board = board; - CacheKingPositions(); - } - - private void CacheKingPositions() - { - this.board.ForEachNotNull((tile, position) => - { - if (tile.WhichPiece == WhichPiece.King) - { - if (tile.Owner == WhichPlayer.Player1) - { - player1KingPosition = position; - } - else if (tile.Owner == WhichPlayer.Player2) - { - player2KingPosition = position; - } - } - }); + this.boardState = board; } /// @@ -43,82 +17,64 @@ namespace Shogi.Domain /// /// The position of the piece being moved expressed in board notation. /// The target position expressed in board notation. + /// True if a promotion is expected as a result of this move. /// A describing the success or failure of the move. - public MoveResult Move(string fromNotation, string toNotation, bool isPromotion = false) + internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false) { var from = ShogiBoardState.FromBoardNotation(fromNotation); var to = ShogiBoardState.FromBoardNotation(toNotation); - var fromPiece = board[from]; + var fromPiece = boardState[from]; if (fromPiece == null) { return new MoveResult(false, $"Tile [{fromNotation}] is empty. There is no piece to move."); } - if (fromPiece.Owner != board.WhoseTurn) + if (fromPiece.Owner != boardState.WhoseTurn) { return new MoveResult(false, "Not allowed to move the opponents piece"); } - if (ShogiIsPathable(from, to) == false) + var path = fromPiece.GetPathFromStartToEnd(from, to); + + if (boardState.IsPathBlocked(path)) { - return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); + return new MoveResult(false, "Another piece obstructs the desired move."); } - var captured = board[to]; - if (captured != null) + if (boardState[to] != null) { - if (captured.Owner == board.WhoseTurn) - { - return new MoveResult(false, "Capturing your own piece is not allowed."); - } - captured.Capture(); - board.Hand.Add(captured); + boardState.Capture(to); } - //Mutate the board. - if (isPromotion) + if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from))) { - if (board.WhoseTurn == WhichPlayer.Player1 && (to.Y > 5 || from.Y > 5)) - { - fromPiece.Promote(); - } - else if (board.WhoseTurn == WhichPlayer.Player2 && (to.Y < 3 || from.Y < 3)) - { - fromPiece.Promote(); - } + fromPiece.Promote(); } - board[to] = fromPiece; - board[from] = null; - if (fromPiece.WhichPiece == WhichPiece.King) - { - if (fromPiece.Owner == WhichPlayer.Player1) - { - player1KingPosition = from; - } - else if (fromPiece.Owner == WhichPlayer.Player2) - { - player2KingPosition = from; - } - } - //MoveHistory.Add(move); + + boardState[to] = fromPiece; + boardState[from] = null; + + boardState.RememberAsMostRecentMove(from, to); + var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + boardState.WhoseTurn = otherPlayer; + return new MoveResult(true); } - /// /// Move a piece from the hand to the board ignorant if check or check-mate. /// /// /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public MoveResult Move(WhichPiece pieceInHand, string toNotation) + internal MoveResult Move(WhichPiece pieceInHand, string toNotation) { var to = ShogiBoardState.FromBoardNotation(toNotation); - var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand); + var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { return new MoveResult(false, $"{pieceInHand} does not exist in the hand."); } - if (board[to] != null) + if (boardState[to] != null) { return new MoveResult(false, $"Illegal move - attempting to capture while playing a piece from the hand."); } @@ -128,8 +84,8 @@ namespace Shogi.Domain case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. - if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) - || (board.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) + if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) + || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) { return new MoveResult(false, "Knight has no valid moves after placed."); } @@ -139,8 +95,8 @@ namespace Shogi.Domain case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y == 8) - || (board.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) + if ((boardState.WhoseTurn == WhichPlayer.Player1 && to.Y == 8) + || (boardState.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) { return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); } @@ -149,262 +105,180 @@ namespace Shogi.Domain } // Mutate the board. - board[to] = board.Hand[index]; - board.Hand.RemoveAt(index); + boardState[to] = boardState.ActivePlayerHand[index]; + boardState.ActivePlayerHand.RemoveAt(index); //MoveHistory.Add(move); return new MoveResult(true); } - private bool ShogiIsPathable(Vector2 from, Vector2 to) + /// + /// Determines if the last move put the player who moved in check. + /// + /// + /// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance. + /// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check. + /// + internal bool IsPlayerInCheckAfterMove() { - var piece = board[from]; - if (piece == null) return false; - - var isObstructed = false; - var isPathable = PathTo(from, to, (other, position) => - { - if (other.Owner == piece.Owner) isObstructed = true; - }); - return !isObstructed && isPathable; - } - - public bool EvaluateCheckAfterMove(WhichPiece pieceInHand, Vector2 to, WhichPlayer whichPlayer) - { - if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. + var previousMovedPiece = boardState[boardState.PreviousMoveTo]; + if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMoveTo}."); + var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; var isCheck = false; - var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1KingPosition : player2KingPosition; - - // Check if the move put the king in check. - if (PathTo(to, kingPosition)) return true; - - // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent. - // Go read the shogi rules to be sure this is true. - - return isCheck; - } - - public bool IsPlayerInCheckAfterMove(Vector2 from, Vector2 to, WhichPlayer whichPlayer) - { - if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. - - var isCheck = false; - var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1KingPosition : player2KingPosition; - - // Check if the move put the king in check. - if (PathTo(to, kingPosition)) return true; // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, from); + 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); + 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. // if absolute slope is 0°, look for lance along the line. if (float.IsInfinity(slope)) { - // if slope of the move is also infinity...can skip this? - LinePathTo(kingPosition, direction, (piece, position) => + isCheck = threat.WhichPiece switch { - if (piece.Owner != whichPlayer) - { - switch (piece.WhichPiece) - { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; - } - } - }); + WhichPiece.Lance => !threat.IsPromoted, + WhichPiece.Rook => true, + _ => false + }; } else if (slope == 1) { - LinePathTo(kingPosition, direction, (piece, position) => + isCheck = threat.WhichPiece switch { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) - { - isCheck = true; - } - }); + WhichPiece.Bishop => true, + _ => false + }; } else if (slope == 0) { - LinePathTo(kingPosition, direction, (piece, position) => + isCheck = threat.WhichPiece switch { - if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) - { - isCheck = true; - } - }); + WhichPiece.Rook => true, + _ => false + }; } return isCheck; } - public bool EvaluateCheckmate() + internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo); + + internal bool IsOpposingKingThreatenedByPosition(Vector2 position) { - if (!board.InCheck.HasValue) return false; + var previousMovedPiece = boardState[position]; + if (previousMovedPiece == null) return false; + + var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition; + var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition); + var threatenedPiece = boardState.GetFirstPieceAlongPath(path); + if (!path.Any() || threatenedPiece == null) return false; + + return threatenedPiece.WhichPiece == WhichPiece.King; + } + + internal bool IsPlayerInCheckMate(WhichPlayer whichPlayer) + { + 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); + + if (tilesOccupiedByOtherPlayer.SingleOrDefault() != default) + { + /* 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) + { + // 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. + * + * Foreach position the king can reach + * 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) + // { + + // } + // } + //} + } + + public bool EvaluateCheckmate_Old() + { + if (!boardState.InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; - board.ForEachNotNull((piece, from) => // For each piece... + boardState.ForEachNotNull((piece, from) => // For each piece... { // Short circuit if (!isCheckmate) return; - if (piece.Owner == board.InCheck) // ...owned by the player in 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; - } - } - }); + //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; } - - /// - /// 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(Vector2 origin, Vector2 destination, Callback? callback = null) - { - if (destination.X > 8 || destination.Y > 8 || destination.X < 0 || destination.Y < 0) - { - return false; - } - var piece = board[origin]; - if (piece == null) return false; - - var path = FindDirectionTowardsDestination(GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown), origin, destination); - 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; - var next = origin; - while (shouldPath && next != destination) - { - next = Vector2.Add(next, path.Direction); - var collider = board[next]; - if (collider != null) - { - callback?.Invoke(collider, next); - shouldPath = false; - } - else if (path.Distance == Distance.OneStep) - { - shouldPath = false; - } - } - return next == destination; - } - - public void PathEvery(Vector2 from, Callback callback) - { - var piece = board[from]; - if (piece == null) - { - return; - } - foreach (var path in GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown)) - { - var shouldPath = true; - var next = Vector2.Add(from, path.Direction); ; - while (shouldPath && next.X < 8 && next.Y < 8 && next.X >= 0 && next.Y >= 0) - { - var collider = board[(int)next.Y, (int)next.X]; - if (collider != null) - { - callback(collider, next); - shouldPath = false; - } - if (path.Distance == Distance.OneStep) - { - shouldPath = false; - } - next = Vector2.Add(next, path.Direction); - } - } - } - - public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 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)) - { - 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; - } - } - - /// - /// 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 < 8 && next.Y >= 0 && next.Y < 8) - { - var element = board[next]; - if (element != null) callback(element, next); - next = Vector2.Add(next, direction); - } - } - - public static Move FindDirectionTowardsDestination(ICollection paths, Vector2 origin, Vector2 destination) => - paths.Aggregate((a, b) => - { - var distanceA = Vector2.Distance(destination, Vector2.Add(origin, a.Direction)); - var distanceB = Vector2.Distance(destination, Vector2.Add(origin, b.Direction)); - return distanceA < distanceB ? a : b; - }); - - public static MoveSet GetMoveSet(WhichPiece whichPiece) - { - return whichPiece switch - { - WhichPiece.King => MoveSet.King, - WhichPiece.GoldGeneral => MoveSet.GoldGeneral, - WhichPiece.SilverGeneral => MoveSet.SilverGeneral, - WhichPiece.Bishop => MoveSet.Bishop, - WhichPiece.Rook => MoveSet.Rook, - WhichPiece.Knight => MoveSet.Knight, - WhichPiece.Lance => MoveSet.Lance, - WhichPiece.Pawn => MoveSet.Pawn, - _ => throw new ArgumentException($"{nameof(WhichPiece)} not recognized."), - }; - } } }