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."),
- };
- }
}
}