From a2f3abb94e48646f2c7a3e1eae2a9afa8f989d5f Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 22 Dec 2021 17:43:32 -0600 Subject: [PATCH] yep --- Gameboard.ShogiUI.Sockets.sln | 9 +- .../Gameboard.ShogiUI.Sockets.csproj | 16 +- .../Gameboard.ShogiUI.UnitTests.csproj | 8 +- .../Gameboard.ShogiUI.xUnitTests.csproj | 4 +- .../Shogi.Domain.UnitTests.csproj | 28 ++ .../ShogiBoardStateShould.cs | 186 ++++++++ Shogi.Domain.UnitTests/ShogiShould.cs | 438 ++++++++++++++++++ Shogi.Domain/Direction.cs | 18 + Shogi.Domain/Distance.cs | 8 + Shogi.Domain/Move.cs | 58 +-- Shogi.Domain/MoveSet.cs | 106 +++++ Shogi.Domain/Shogi.cs | 119 ++--- Shogi.Domain/ShogiBoardState.cs | 6 +- Shogi.Domain/StandardRules.cs | 304 ++++++++---- 14 files changed, 1096 insertions(+), 212 deletions(-) create mode 100644 Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj create mode 100644 Shogi.Domain.UnitTests/ShogiBoardStateShould.cs create mode 100644 Shogi.Domain.UnitTests/ShogiShould.cs create mode 100644 Shogi.Domain/Direction.cs create mode 100644 Shogi.Domain/Distance.cs create mode 100644 Shogi.Domain/MoveSet.cs diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln index 1469cf3..3b54d4e 100644 --- a/Gameboard.ShogiUI.Sockets.sln +++ b/Gameboard.ShogiUI.Sockets.sln @@ -17,7 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,6 +55,10 @@ Global {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.Build.0 = Release|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +66,7 @@ Global GlobalSection(NestedProjects) = preSolution {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} {12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} + {F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj index bf7b641..e05ef31 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -8,15 +8,15 @@ - - - - - - - + + + + + + + - + diff --git a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj index c11752d..16999c2 100644 --- a/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj +++ b/Gameboard.ShogiUI.UnitTests/Gameboard.ShogiUI.UnitTests.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj index 040179a..1de7d82 100644 --- a/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj +++ b/Gameboard.ShogiUI.xUnitTests/Gameboard.ShogiUI.xUnitTests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj b/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj new file mode 100644 index 0000000..724e08f --- /dev/null +++ b/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs new file mode 100644 index 0000000..470a3cb --- /dev/null +++ b/Shogi.Domain.UnitTests/ShogiBoardStateShould.cs @@ -0,0 +1,186 @@ +using FluentAssertions; +using Xunit; + +namespace Shogi.Domain.UnitTests +{ + public class ShogiBoardStateShould + { + [Fact] + public void InitializeBoardState() + { + // Act + var board = new ShogiBoardState(); + + // Assert + board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["A1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["A1"]?.IsPromoted.Should().Be(false); + board["B1"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["B1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B1"]?.IsPromoted.Should().Be(false); + board["C1"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["C1"]?.IsPromoted.Should().Be(false); + board["D1"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["D1"]?.IsPromoted.Should().Be(false); + board["E1"]?.WhichPiece.Should().Be(WhichPiece.King); + board["E1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["E1"]?.IsPromoted.Should().Be(false); + board["F1"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["F1"]?.IsPromoted.Should().Be(false); + board["G1"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["G1"]?.IsPromoted.Should().Be(false); + board["H1"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["H1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H1"]?.IsPromoted.Should().Be(false); + board["I1"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["I1"]?.Owner.Should().Be(WhichPlayer.Player1); + board["I1"]?.IsPromoted.Should().Be(false); + + board["A2"].Should().BeNull(); + board["B2"]?.WhichPiece.Should().Be(WhichPiece.Bishop); + board["B2"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B2"]?.IsPromoted.Should().Be(false); + board["C2"].Should().BeNull(); + board["D2"].Should().BeNull(); + board["E2"].Should().BeNull(); + board["F2"].Should().BeNull(); + board["G2"].Should().BeNull(); + board["H2"]?.WhichPiece.Should().Be(WhichPiece.Rook); + board["H2"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H2"]?.IsPromoted.Should().Be(false); + board["I2"].Should().BeNull(); + + board["A3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["A3"]?.IsPromoted.Should().Be(false); + board["B3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["B3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["B3"]?.IsPromoted.Should().Be(false); + board["C3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["C3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["C3"]?.IsPromoted.Should().Be(false); + board["D3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["D3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["D3"]?.IsPromoted.Should().Be(false); + board["E3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["E3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["E3"]?.IsPromoted.Should().Be(false); + board["F3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["F3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["F3"]?.IsPromoted.Should().Be(false); + board["G3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["G3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["G3"]?.IsPromoted.Should().Be(false); + board["H3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["H3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["H3"]?.IsPromoted.Should().Be(false); + board["I3"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["I3"]?.Owner.Should().Be(WhichPlayer.Player1); + board["I3"]?.IsPromoted.Should().Be(false); + + board["A4"].Should().BeNull(); + board["B4"].Should().BeNull(); + board["C4"].Should().BeNull(); + board["D4"].Should().BeNull(); + board["E4"].Should().BeNull(); + board["F4"].Should().BeNull(); + board["G4"].Should().BeNull(); + board["H4"].Should().BeNull(); + board["I4"].Should().BeNull(); + + board["A5"].Should().BeNull(); + board["B5"].Should().BeNull(); + board["C5"].Should().BeNull(); + board["D5"].Should().BeNull(); + board["E5"].Should().BeNull(); + board["F5"].Should().BeNull(); + board["G5"].Should().BeNull(); + board["H5"].Should().BeNull(); + board["I5"].Should().BeNull(); + + board["A6"].Should().BeNull(); + board["B6"].Should().BeNull(); + board["C6"].Should().BeNull(); + board["D6"].Should().BeNull(); + board["E6"].Should().BeNull(); + board["F6"].Should().BeNull(); + board["G6"].Should().BeNull(); + board["H6"].Should().BeNull(); + board["I6"].Should().BeNull(); + + board["A7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["A7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["A7"]?.IsPromoted.Should().Be(false); + board["B7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["B7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B7"]?.IsPromoted.Should().Be(false); + board["C7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["C7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["C7"]?.IsPromoted.Should().Be(false); + board["D7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["D7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["D7"]?.IsPromoted.Should().Be(false); + board["E7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["E7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["E7"]?.IsPromoted.Should().Be(false); + board["F7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["F7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["F7"]?.IsPromoted.Should().Be(false); + board["G7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["G7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["G7"]?.IsPromoted.Should().Be(false); + board["H7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["H7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H7"]?.IsPromoted.Should().Be(false); + board["I7"]?.WhichPiece.Should().Be(WhichPiece.Pawn); + board["I7"]?.Owner.Should().Be(WhichPlayer.Player2); + board["I7"]?.IsPromoted.Should().Be(false); + + board["A8"].Should().BeNull(); + board["B8"]?.WhichPiece.Should().Be(WhichPiece.Rook); + board["B8"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B8"]?.IsPromoted.Should().Be(false); + board["C8"].Should().BeNull(); + board["D8"].Should().BeNull(); + board["E8"].Should().BeNull(); + board["F8"].Should().BeNull(); + board["G8"].Should().BeNull(); + board["H8"]?.WhichPiece.Should().Be(WhichPiece.Bishop); + board["H8"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H8"]?.IsPromoted.Should().Be(false); + board["I8"].Should().BeNull(); + + board["A9"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["A9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["A9"]?.IsPromoted.Should().Be(false); + board["B9"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["B9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["B9"]?.IsPromoted.Should().Be(false); + board["C9"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["C9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["C9"]?.IsPromoted.Should().Be(false); + board["D9"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["D9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["D9"]?.IsPromoted.Should().Be(false); + board["E9"]?.WhichPiece.Should().Be(WhichPiece.King); + board["E9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["E9"]?.IsPromoted.Should().Be(false); + board["F9"]?.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + board["F9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["F9"]?.IsPromoted.Should().Be(false); + board["G9"]?.WhichPiece.Should().Be(WhichPiece.SilverGeneral); + board["G9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["G9"]?.IsPromoted.Should().Be(false); + board["H9"]?.WhichPiece.Should().Be(WhichPiece.Knight); + board["H9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["H9"]?.IsPromoted.Should().Be(false); + board["I9"]?.WhichPiece.Should().Be(WhichPiece.Lance); + board["I9"]?.Owner.Should().Be(WhichPlayer.Player2); + board["I9"]?.IsPromoted.Should().Be(false); + } + } +} diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs new file mode 100644 index 0000000..fc07cb5 --- /dev/null +++ b/Shogi.Domain.UnitTests/ShogiShould.cs @@ -0,0 +1,438 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Shogi.Domain.UnitTests +{ + public class ShogiShould + { + private readonly ITestOutputHelper output; + public ShogiShould(ITestOutputHelper output) + { + this.output = output; + } + + //[Fact] + //public void InitializeBoardStateWithMoves() + //{ + // var moves = new[] + // { + // // P1 Pawn + // new Move("A3", "A4") + // }; + // var shogi = new Shogi(moves); + // shogi.Board["A3"].Should().BeNull(); + // shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn); + //} + + //[Fact] + //public void AllowValidMoves_AfterCheck() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7"), + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + + // // Act - P2 is able to un-check theirself. + // /// P2 King moves out of check + // var moveSuccess = shogi.Move(new Move("E9", "E8")); + + // // Assert + // using var _ = new AssertionScope(); + // moveSuccess.Should().BeTrue(); + // shogi.InCheck.Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveFromEmptyPosition() + //{ + // // Arrange + // var shogi = new Shogi(); + // shogi.Board["D5"].Should().BeNull(); + + // // Act + // var moveSuccess = shogi.Move(new Move("D5", "D6")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["D5"].Should().BeNull(); + // shogi.Board["D6"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveToCurrentPosition() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - P1 "moves" pawn to the position it already exists at. + // var moveSuccess = shogi.Move(new Move("A3", "A3")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveSet() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - Move Lance illegally + // var moveSuccess = shogi.Move(new Move("A1", "D5")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["A5"].Should().BeNull(); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_Ownership() + //{ + // // Arrange + // var shogi = new Shogi(); + // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + // shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2); + + // // Act - Move Player2 Pawn when it is Player1 turn. + // var moveSuccess = shogi.Move(new Move("A7", "A6")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Board["A6"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_MoveThroughAllies() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - Move P1 Lance through P1 Pawn. + // var moveSuccess = shogi.Move(new Move("A1", "A5")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Board["A5"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidMoves_CaptureAlly() + //{ + // // Arrange + // var shogi = new Shogi(); + + // // Act - P1 Knight tries to capture P1 Pawn. + // var moveSuccess = shogi.Move(new Move("B1", "C3")); + + // // Arrange + // moveSuccess.Should().BeFalse(); + // shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight); + // shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn); + // shogi.Player1Hand.Should().BeEmpty(); + // shogi.Player2Hand.Should().BeEmpty(); + //} + + //[Fact] + //public void PreventInvalidMoves_Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop puts P2 in check + // new Move("B2", "G7") + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + + // // Act - P2 moves Lance while in check. + // var moveSuccess = shogi.Move(new Move("I9", "I8")); + + // // Assert + // moveSuccess.Should().BeFalse(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // shogi.Board["I8"].Should().BeNull(); + //} + + //[Fact] + //public void PreventInvalidDrops_MoveSet() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("I7", "I6"), + // // P1 Bishop takes P2 Pawn. + // new Move("B2", "G7"), + // // P2 Gold, block check from P1 Bishop. + // new Move("F9", "F8"), + // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + // new Move("G7", "H8", true), + // // P2 Pawn again + // new Move("I6", "I5"), + // // P1 Bishop takes P2 Knight + // new Move("H8", "H9"), + // // P2 Pawn again + // new Move("I5", "I4"), + // // P1 Bishop takes P2 Lance + // new Move("H9", "I9"), + // // P2 Pawn captures P1 Pawn + // new Move("I4", "I3") + // }; + // var shogi = new Shogi(moves); + // shogi.Player1Hand.Count.Should().Be(4); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); + + // // Act | Assert - Illegally placing Knight from the hand in farthest row. + // shogi.Board["H9"].Should().BeNull(); + // var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9")); + // dropSuccess.Should().BeFalse(); + // shogi.Board["H9"].Should().BeNull(); + // shogi.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); + + // // 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); + + // // 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); + + // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // // TODO + //} + + //[Fact] + //public void PreventInvalidDrop_Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Pawn, arbitrary move. + // new Move("A3", "A4"), + // // P2 Bishop takes P1 Bishop + // new Move("H8", "B2"), + // // P1 Silver takes P2 Bishop + // new Move("C1", "B2"), + // // P2 Pawn, arbtrary move + // new Move("A7", "A6"), + // // P1 drop Bishop, place P2 in check + // new Move(WhichPiece.Bishop, "G7") + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // shogi.Board["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(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + //} + + //[Fact] + //public void PreventInvalidDrop_Capture() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop capture P2 Bishop + // new Move("B2", "H8"), + // // P2 Pawn + // new Move("G6", "G5") + // }; + // var shogi = new Shogi(moves); + // 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); + // } + + // // Act - P1 tries to place a piece where an opponent's piece resides. + // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); + + // // Assert + // using (new AssertionScope()) + // { + // 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); + // } + //} + + //[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); + + // // Act - P1 Bishop, check + // shogi.Move(new Move("B2", "G7")); + + // // Assert + // shogi.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); + + // // Act - P1 moves across promote threshold. + // var moveSuccess = shogi.Move(new 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(); + // } + //} + + //[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()); + + // // Act - P1 Pawn wins by checkmate. + // var moveSuccess = shogi.Move(new Move("E7", "E8")); + // output.WriteLine(shogi.PrintStateAsAscii()); + + // // Assert - checkmate + // moveSuccess.Should().BeTrue(); + // shogi.IsCheckmate.Should().BeTrue(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + //} + + //[Fact] + //public void Capture() + //{ + // // Arrange + // var moves = new[] + // { + // new Move("C3", "C4"), + // new Move("G7", "G6") + // }; + // var shogi = new Shogi(moves); + + // // Act - P1 Bishop captures P2 Bishop + // var moveSuccess = shogi.Move(new Move("B2", "H8")); + + // // 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); + + // shogi.Player1Hand + // .Should() + // .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + //} + } +} diff --git a/Shogi.Domain/Direction.cs b/Shogi.Domain/Direction.cs new file mode 100644 index 0000000..5316598 --- /dev/null +++ b/Shogi.Domain/Direction.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Shogi.Domain +{ + public static class Direction + { + public static readonly Vector2 Up = new(0, 1); + public static readonly Vector2 Down = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 UpLeft = new(-1, 1); + public static readonly Vector2 UpRight = new(1, 1); + public static readonly Vector2 DownLeft = new(-1, -1); + public static readonly Vector2 DownRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); + } +} diff --git a/Shogi.Domain/Distance.cs b/Shogi.Domain/Distance.cs new file mode 100644 index 0000000..9031228 --- /dev/null +++ b/Shogi.Domain/Distance.cs @@ -0,0 +1,8 @@ +namespace Shogi.Domain +{ + public enum Distance + { + OneStep, + MultiStep + } +} \ No newline at end of file diff --git a/Shogi.Domain/Move.cs b/Shogi.Domain/Move.cs index 30d19d5..1ad6f08 100644 --- a/Shogi.Domain/Move.cs +++ b/Shogi.Domain/Move.cs @@ -3,51 +3,15 @@ using System.Numerics; namespace Shogi.Domain { - [DebuggerDisplay("{From} - {To}")] - public class Move - { - public Vector2? From { get; } // TODO: Use string notation - public bool IsPromotion { get; } - public WhichPiece? PieceFromHand { get; } - public Vector2 To { get; } - - public Move(Vector2 from, Vector2 to, bool isPromotion = false) - { - From = from; - To = to; - IsPromotion = isPromotion; - } - public Move(WhichPiece pieceFromHand, Vector2 to) - { - PieceFromHand = pieceFromHand; - To = to; - } - - /// - /// Constructor to represent moving a piece on the Board to another position on the Board. - /// - /// Position the piece is being moved from. - /// Position the piece is being moved to. - /// If the moving piece should be promoted. - public Move(string fromNotation, string toNotation, bool isPromotion = false) - { - //From = NotationHelper.FromBoardNotation(fromNotation); - //To = NotationHelper.FromBoardNotation(toNotation); - //IsPromotion = isPromotion; - } - - /// - /// Constructor to represent moving a piece from the Hand to the Board. - /// - /// The piece being moved from the Hand to the Board. - /// Position the piece is being moved to. - /// If the moving piece should be promoted. - public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false) - { - //From = null; - //PieceFromHand = pieceFromHand; - //To = NotationHelper.FromBoardNotation(toNotation); - //IsPromotion = isPromotion; - } - } + [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 new file mode 100644 index 0000000..b1b2d2d --- /dev/null +++ b/Shogi.Domain/MoveSet.cs @@ -0,0 +1,106 @@ +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/Shogi.cs b/Shogi.Domain/Shogi.cs index c7cbad6..cb0f990 100644 --- a/Shogi.Domain/Shogi.cs +++ b/Shogi.Domain/Shogi.cs @@ -1,6 +1,4 @@ -using System.Numerics; - -namespace Shogi.Domain +namespace Shogi.Domain { /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. @@ -17,74 +15,89 @@ namespace Shogi.Domain { this.board = board; rules = new StandardRules(this.board); - Error = string.Empty; } - public Shogi(IList moves) : this() + //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) { - for (var i = 0; i < moves.Count; i++) - { - if (!Move(moves[i])) - { - // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc. - throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}"); - } - } + // TODO: ShogiBoardState.FromBoardNotation should not throw an execption in this query method. + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); + var simulator = new StandardRules(new ShogiBoardState(board)); + return simulator.Move(fromVector, toVector, isPromotion); } - public bool Move(Move move) + public MoveResult CanMove(WhichPiece pieceInHand, string to) { - var moveSuccess = TryMove(move); + var toVector = ShogiBoardState.FromBoardNotation(to); + var simulator = new StandardRules(new ShogiBoardState(board)); + return simulator.Move(pieceInHand, toVector); + } - if (!moveSuccess) + public void Move(string from, string to, bool isPromotion) + { + var fromVector = ShogiBoardState.FromBoardNotation(from); + var toVector = ShogiBoardState.FromBoardNotation(to); + var moveResult = rules.Move(fromVector, toVector, isPromotion); + if (!moveResult.Success) { - return false; + throw new InvalidOperationException(moveResult.Reason); } - var otherPlayer = WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (EvaluateCheckAfterMove(move, otherPlayer)) + var otherPlayer = board.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (rules.EvaluateCheckAfterMove(fromVector, toVector, otherPlayer)) { - InCheck = otherPlayer; - IsCheckmate = EvaluateCheckmate(); + board.InCheck = otherPlayer; + board.IsCheckmate = rules.EvaluateCheckmate(); } else { - InCheck = null; + board.InCheck = null; } - return true; } - /// - /// 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)); + ///// + ///// 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) - { - if (simulationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn)) - { - // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn; - return false; - } - } + // 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; + // } + // } - // The move is valid and legal; update board state. - if (move.PieceFromHand.HasValue) PlaceFromHand(move); - else PlaceFromBoard(move); - return true; - } + // // The move is valid and legal; update board state. + // if (move.PieceFromHand.HasValue) PlaceFromHand(move); + // else PlaceFromBoard(move); + // return true; + //} } } diff --git a/Shogi.Domain/ShogiBoardState.cs b/Shogi.Domain/ShogiBoardState.cs index 83e70ae..49e9683 100644 --- a/Shogi.Domain/ShogiBoardState.cs +++ b/Shogi.Domain/ShogiBoardState.cs @@ -13,15 +13,15 @@ namespace Shogi.Domain /// /// Key is position notation, such as "E4". /// - private Dictionary board; + private readonly Dictionary board; public List Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; public List Player1Hand { get; } public List Player2Hand { get; } public List MoveHistory { get; } public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; - public WhichPlayer? InCheck { get; private set; } - public bool IsCheckmate { get; private set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } public ShogiBoardState() { diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 0f04c50..aa9816c 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -4,6 +4,10 @@ namespace Shogi.Domain { internal class StandardRules { + /// Guaranteed to be non-null. + /// + public delegate void Callback(Piece collider, Vector2 position); + private readonly ShogiBoardState board; private Vector2 player1KingPosition; private Vector2 player2KingPosition; @@ -38,18 +42,20 @@ namespace Shogi.Domain /// The position of the piece being moved expressed in board notation. /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public MoveResult Move(string from, string to, bool isPromotion = false) + public MoveResult Move(Vector2 from, Vector2 to, bool isPromotion = false) { var fromPiece = board[from]; if (fromPiece == null) { return new MoveResult(false, $"Tile [{from}] is empty. There is no piece to move."); } + if (fromPiece.Owner != board.WhoseTurn) { return new MoveResult(false, "Not allowed to move the opponents piece"); } - if (IsPathable(move.From.Value, move.To) == false) + + if (IsPathable(from, to) == false) { return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); } @@ -68,13 +74,11 @@ namespace Shogi.Domain //Mutate the board. if (isPromotion) { - var fromVector = ShogiBoardState.FromBoardNotation(from); - var toVector = ShogiBoardState.FromBoardNotation(to); - if (board.WhoseTurn == WhichPlayer.Player1 && (toVector.Y > 5 || fromVector.Y > 5)) + if (board.WhoseTurn == WhichPlayer.Player1 && (to.Y > 5 || from.Y > 5)) { fromPiece.Promote(); } - else if (board.WhoseTurn == WhichPlayer.Player2 && (toVector.Y < 3 || fromVector.Y < 3)) + else if (board.WhoseTurn == WhichPlayer.Player2 && (to.Y < 3 || from.Y < 3)) { fromPiece.Promote(); } @@ -85,17 +89,15 @@ namespace Shogi.Domain { if (fromPiece.Owner == WhichPlayer.Player1) { - player1King.X = move.To.X; - player1King.Y = move.To.Y; + player1KingPosition = from; } else if (fromPiece.Owner == WhichPlayer.Player2) { - player2King.X = move.To.X; - player2King.Y = move.To.Y; + player2KingPosition = from; } } - MoveHistory.Add(move); - return true; + //MoveHistory.Add(move); + return new MoveResult(true); } /// @@ -104,30 +106,27 @@ namespace Shogi.Domain /// /// The target position expressed in board notation. /// A describing the success or failure of the simulation. - public void Move(WhichPiece pieceInHand, string to) + public MoveResult Move(WhichPiece pieceInHand, Vector2 to) { - var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand); + var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand); if (index == -1) { - Error = $"{move.PieceFromHand} does not exist in the hand."; - return false; + return new MoveResult(false, $"{pieceInHand} does not exist in the hand."); } - if (Board[move.To] != null) + if (board[to] != null) { - Error = $"Illegal move - attempting to capture while playing a piece from the hand."; - return false; + return new MoveResult(false, $"Illegal move - attempting to capture while playing a piece from the hand."); } - switch (move.PieceFromHand!.Value) + switch (pieceInHand) { case WhichPiece.Knight: { // Knight cannot be placed onto the farthest two ranks from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2)) + if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y > 6) + || (board.WhoseTurn == WhichPlayer.Player2 && to.Y < 2)) { - Error = $"Knight has no valid moves after placed."; - return false; + return new MoveResult(false, "Knight has no valid moves after placed."); } break; } @@ -135,134 +134,251 @@ namespace Shogi.Domain case WhichPiece.Pawn: { // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8) - || (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0)) + if ((board.WhoseTurn == WhichPlayer.Player1 && to.Y == 8) + || (board.WhoseTurn == WhichPlayer.Player2 && to.Y == 0)) { - Error = $"{move.PieceFromHand} has no valid moves after placed."; - return false; + return new MoveResult(false, $"{pieceInHand} has no valid moves after placed."); } break; } } // Mutate the board. - Board[move.To] = Hand[index]; - Hand.RemoveAt(index); - MoveHistory.Add(move); - - return true; + board[to] = board.Hand[index]; + board.Hand.RemoveAt(index); + //MoveHistory.Add(move); + return new MoveResult(true); } private bool IsPathable(Vector2 from, Vector2 to) { - var piece = Board[from]; + var piece = board[from]; if (piece == null) return false; var isObstructed = false; - var isPathable = pathFinder.PathTo(from, to, (other, position) => + var isPathable = PathTo(from, to, (other, position) => { if (other.Owner == piece.Owner) isObstructed = true; }); return !isObstructed && isPathable; } - private bool EvaluateCheckAfterMove(Move move, WhichPlayer WhichPerspective) + public bool EvaluateCheckAfterMove(WhichPiece pieceInHand, Vector2 to, WhichPlayer whichPlayer) { - if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother. + if (whichPlayer == board.InCheck) return true; // If we already know the player is in check, don't bother. var isCheck = false; - var kingPosition = WhichPerspective == WhichPlayer.Player1 ? player1King : player2King; + var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1KingPosition : player2KingPosition; // Check if the move put the king in check. - if (pathFinder.PathTo(move.To, kingPosition)) return true; + if (PathTo(to, kingPosition)) return true; - if (move.From.HasValue) + // 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 EvaluateCheckAfterMove(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 slope = Math.Abs(direction.Y / direction.X); + // If absolute slope is 45°, look for a bishop along the line. + // If absolute slope is 0° or 90°, look for a rook along the line. + // if absolute slope is 0°, look for lance along the line. + if (float.IsInfinity(slope)) { - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, move.From!.Value); - var slope = Math.Abs(direction.Y / direction.X); - // If absolute slope is 45°, look for a bishop along the line. - // If absolute slope is 0° or 90°, look for a rook along the line. - // if absolute slope is 0°, look for lance along the line. - if (float.IsInfinity(slope)) + // if slope of the move is also infinity...can skip this? + LinePathTo(kingPosition, direction, (piece, position) => { - // if slope of the move is also infinity...can skip this? - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => + if (piece.Owner != whichPlayer) { - if (piece.Owner != WhichPerspective) + switch (piece.WhichPiece) { - switch (piece.WhichPiece) - { - case WhichPiece.Rook: - isCheck = true; - break; - case WhichPiece.Lance: - if (!piece.IsPromoted) isCheck = true; - break; - } + case WhichPiece.Rook: + isCheck = true; + break; + case WhichPiece.Lance: + if (!piece.IsPromoted) isCheck = true; + break; } - }); - } - else if (slope == 1) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop) - { - isCheck = true; - } - }); - } - else if (slope == 0) - { - pathFinder.LinePathTo(kingPosition, direction, (piece, position) => - { - if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook) - { - isCheck = true; - } - }); - } + } + }); } - else + else if (slope == 1) { - // 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. + LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop) + { + isCheck = true; + } + }); + } + else if (slope == 0) + { + LinePathTo(kingPosition, direction, (piece, position) => + { + if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook) + { + isCheck = true; + } + }); } return isCheck; } - private bool EvaluateCheckmate() + public bool EvaluateCheckmate() { - if (!InCheck.HasValue) return false; + if (!board.InCheck.HasValue) return false; // Assume true and try to disprove. var isCheckmate = true; - Board.ForEachNotNull((piece, from) => // For each piece... + board.ForEachNotNull((piece, from) => // For each piece... { // Short circuit if (!isCheckmate) return; - if (piece.Owner == InCheck) // ...owned by the player in check... + if (piece.Owner == board.InCheck) // ...owned by the player in check... { // ...evaluate if any move gets the player out of check. - pathFinder.PathEvery(from, (other, position) => + PathEvery(from, (other, position) => { - var simulationBoard = new Shogi(this); - var moveToTry = new Move(from, position); - var moveSuccess = simulationBoard.TryMove(moveToTry); - if (moveSuccess) + var simulationBoard = new StandardRules(new ShogiBoardState(board)); + var simulationResult = simulationBoard.Move(from, position, false); + if (simulationResult.Success) { - if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value)) + if (!EvaluateCheckAfterMove(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)) + { + // 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); + } + } + } + + /// + /// 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."), + }; + } } }