@@ -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
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
||||
<ProjectReference Include="..\PathFinding\PathFinding.csproj" />
|
||||
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
227
Shogi.Domain.UnitTests/RookShould.cs
Normal file
227
Shogi.Domain.UnitTests/RookShould.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<InvalidOperationException>();
|
||||
board["D5"].Should().BeNull();
|
||||
board["D6"].Should().BeNull();
|
||||
board.Player1Hand.Should().BeEmpty();
|
||||
board.Player2Hand.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -81,17 +82,20 @@ 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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board["A3"].Should().NotBeNull();
|
||||
board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
||||
board["A3"].Should().Be(expectedPiece);
|
||||
board.Player1Hand.Should().BeEmpty();
|
||||
board.Player2Hand.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidMoves_MoveSet()
|
||||
@@ -99,18 +103,22 @@ 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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board["A1"].Should().NotBeNull();
|
||||
board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||
board["A1"].Should().Be(expectedPiece);
|
||||
board["A5"].Should().BeNull();
|
||||
board.Player1Hand.Should().BeEmpty();
|
||||
board.Player2Hand.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidMoves_Ownership()
|
||||
@@ -118,19 +126,21 @@ 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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board["A7"].Should().NotBeNull();
|
||||
board["A7"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
||||
board["A7"].Should().Be(expectedPiece);
|
||||
board["A6"].Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidMoves_MoveThroughAllies()
|
||||
@@ -138,18 +148,22 @@ 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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board["A1"].Should().NotBeNull();
|
||||
board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||
board["A3"].Should().NotBeNull();
|
||||
board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
||||
board["A1"].Should().Be(lance);
|
||||
board["A3"].Should().Be(pawn);
|
||||
board["A5"].Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidMoves_CaptureAlly()
|
||||
@@ -157,19 +171,23 @@ 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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board["B1"].Should().NotBeNull();
|
||||
board["B1"]!.WhichPiece.Should().Be(WhichPiece.Knight);
|
||||
board["C3"].Should().NotBeNull();
|
||||
board["C3"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
||||
board["B1"].Should().Be(knight);
|
||||
board["C3"].Should().Be(pawn);
|
||||
board.Player1Hand.Should().BeEmpty();
|
||||
board.Player2Hand.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventInvalidMoves_Check()
|
||||
@@ -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
|
||||
using (new AssertionScope())
|
||||
{
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
||||
board["I9"].Should().NotBeNull();
|
||||
board["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||
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<InvalidOperationException>();
|
||||
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<InvalidOperationException>();
|
||||
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<InvalidOperationException>();
|
||||
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<InvalidOperationException>();
|
||||
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();
|
||||
// }
|
||||
//}
|
||||
|
||||
//[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);
|
||||
//}
|
||||
// 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 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 Bishop captures P2 Bishop
|
||||
shogi.Move("B2", "H8", false);
|
||||
|
||||
// Assert
|
||||
boardState["B2"].Should().BeNull();
|
||||
boardState["H8"].Should().Be(p1Bishop);
|
||||
|
||||
boardState
|
||||
.Player1Hand
|
||||
.Should()
|
||||
.ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1);
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
// Act - P1 Pawn wins by checkmate.
|
||||
shogi.Move("E7", "E8", false);
|
||||
|
||||
// Assert - checkmate
|
||||
boardState.IsCheckmate.Should().BeTrue();
|
||||
boardState.InCheck.Should().Be(WhichPlayer.Player2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Shogi.Domain/BoardTile.cs
Normal file
17
Shogi.Domain/BoardTile.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
public class MoveSet
|
||||
{
|
||||
|
||||
public static readonly MoveSet King = new(new List<Move>(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<Move>(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<Move>(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<Move>(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<Move>(2)
|
||||
{
|
||||
new Move(Direction.KnightLeft),
|
||||
new Move(Direction.KnightRight)
|
||||
});
|
||||
|
||||
public static readonly MoveSet Lance = new(new List<Move>(1)
|
||||
{
|
||||
new Move(Direction.Up, Distance.MultiStep),
|
||||
});
|
||||
|
||||
public static readonly MoveSet Pawn = new(new List<Move>(1)
|
||||
{
|
||||
new Move(Direction.Up)
|
||||
});
|
||||
|
||||
public static readonly MoveSet Rook = new(new List<Move>(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<Move>(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<Move>(4)
|
||||
{
|
||||
new Move(Direction.Up),
|
||||
new Move(Direction.UpLeft),
|
||||
new Move(Direction.UpRight),
|
||||
new Move(Direction.DownLeft),
|
||||
new Move(Direction.DownRight)
|
||||
});
|
||||
|
||||
private readonly ICollection<Move> moves;
|
||||
private readonly ICollection<Move> upsidedownMoves;
|
||||
|
||||
private MoveSet(ICollection<Move> moves)
|
||||
{
|
||||
this.moves = moves;
|
||||
upsidedownMoves = moves.Select(m => new Move(Vector2.Negate(m.Direction), m.Distance)).ToList();
|
||||
}
|
||||
|
||||
public ICollection<Move> GetMoves(bool isUpsideDown) => isUpsideDown ? upsidedownMoves : moves;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Shogi.Domain
|
||||
namespace Shogi.Domain.Pathing
|
||||
{
|
||||
/// <summary>
|
||||
/// Directions are relative to the perspective of Player 1.
|
||||
/// Up points towards player 1. Down points towards player 2.
|
||||
/// </summary>
|
||||
public static class Direction
|
||||
{
|
||||
public static readonly Vector2 Up = new(0, 1);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Shogi.Domain
|
||||
namespace Shogi.Domain.Pathing
|
||||
{
|
||||
public enum Distance
|
||||
{
|
||||
45
Shogi.Domain/Pathing/Path.cs
Normal file
45
Shogi.Domain/Pathing/Path.cs
Normal file
@@ -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<Path> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Shogi.Domain/Pieces/Bishop.cs
Normal file
47
Shogi.Domain/Pieces/Bishop.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class Bishop : Piece
|
||||
{
|
||||
private static readonly ReadOnlyCollection<Path> BishopPaths = new(new List<Path>(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<Path> PromotedBishopPaths = new(new List<Path>(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<Path> Player2Paths =
|
||||
BishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||
PromotedBishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Bishop(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Bishop, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Path> MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths;
|
||||
}
|
||||
}
|
||||
31
Shogi.Domain/Pieces/GoldGeneral.cs
Normal file
31
Shogi.Domain/Pieces/GoldGeneral.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class GoldGeneral : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(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<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.GoldGeneral, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Path> MoveSet => Player1Paths;
|
||||
}
|
||||
}
|
||||
27
Shogi.Domain/Pieces/King.cs
Normal file
27
Shogi.Domain/Pieces/King.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class King : Piece
|
||||
{
|
||||
internal static readonly ReadOnlyCollection<Path> KingPaths = new(new List<Path>(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<Path> MoveSet => KingPaths;
|
||||
}
|
||||
}
|
||||
32
Shogi.Domain/Pieces/Knight.cs
Normal file
32
Shogi.Domain/Pieces/Knight.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class Knight : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(2)
|
||||
{
|
||||
new Path(Direction.KnightLeft),
|
||||
new Path(Direction.KnightRight)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Knight(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Knight, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
31
Shogi.Domain/Pieces/Lance.cs
Normal file
31
Shogi.Domain/Pieces/Lance.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class Lance : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Up, Distance.MultiStep),
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Lance(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Lance, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
31
Shogi.Domain/Pieces/Pawn.cs
Normal file
31
Shogi.Domain/Pieces/Pawn.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class Pawn : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Up)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Pawn(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Pawn, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
78
Shogi.Domain/Pieces/Piece.cs
Normal file
78
Shogi.Domain/Pieces/Piece.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
||||
public abstract class Piece
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a clone of an existing piece.
|
||||
/// </summary>
|
||||
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<Path> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Prep the piece for capture by changing ownership and demoting.
|
||||
/// </summary>
|
||||
public void Capture(WhichPlayer newOwner)
|
||||
{
|
||||
Owner = newOwner;
|
||||
IsPromoted = false;
|
||||
}
|
||||
|
||||
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
||||
{
|
||||
var steps = new List<Vector2>(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<Vector2>();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Shogi.Domain/Pieces/Rook.cs
Normal file
52
Shogi.Domain/Pieces/Rook.cs
Normal file
@@ -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<Path> Player1Paths = new(new List<Path>(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<Path> PromotedPlayer1Paths = new(new List<Path>(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<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(m => m.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||
PromotedPlayer1Paths
|
||||
.Select(m => m.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Rook(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Rook, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
35
Shogi.Domain/Pieces/SilverGeneral.cs
Normal file
35
Shogi.Domain/Pieces/SilverGeneral.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.Pieces
|
||||
{
|
||||
internal class SilverGeneral : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(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<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public SilverGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.SilverGeneral, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System"/>
|
||||
<Using Include="System.Collections.Generic"/>
|
||||
<Using Include="System.Linq"/>
|
||||
<Using Include="System.Numerics"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
namespace Shogi.Domain
|
||||
using Shogi.Domain.Pieces;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
||||
@@ -7,106 +11,232 @@
|
||||
/// </summary>
|
||||
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<Move> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
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.");
|
||||
}
|
||||
///// <summary>
|
||||
///// Attempts a given move. Returns false if the move is illegal.
|
||||
///// </summary>
|
||||
//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;
|
||||
// }
|
||||
// }
|
||||
if (boardState[to] != null)
|
||||
{
|
||||
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
||||
}
|
||||
|
||||
// // The move is valid and legal; update board state.
|
||||
// if (move.PieceFromHand.HasValue) PlaceFromHand(move);
|
||||
// else PlaceFromBoard(move);
|
||||
// return true;
|
||||
//}
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a ASCII representation of the board for debugging board state.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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("- -");
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="piece"></param>
|
||||
/// <returns>
|
||||
/// A string with three characters.
|
||||
/// The first character indicates promotion status.
|
||||
/// The second character indicates piece.
|
||||
/// The third character indicates ownership.
|
||||
/// </returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = @"(?<file>[a-iA-I])(?<rank>[1-9])";
|
||||
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
|
||||
private static readonly char A = 'A';
|
||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||
/// <summary>
|
||||
@@ -15,21 +15,33 @@ namespace Shogi.Domain
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Piece?> board;
|
||||
|
||||
public List<Piece> Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
/// <summary>
|
||||
/// "Active Player" means the player whose turn it is.
|
||||
/// </summary>
|
||||
public Vector2 ActivePlayerKingPosition => WhoseTurn == WhichPlayer.Player1 ? Player1KingPosition : Player2KingPosition;
|
||||
/// <summary>
|
||||
/// "Opposing Player" means the player whose turn it isn't.
|
||||
/// </summary>
|
||||
public Vector2 OpposingPlayerKingPosition => WhoseTurn == WhichPlayer.Player1 ? Player2KingPosition : Player1KingPosition;
|
||||
public Vector2 Player1KingPosition { get; set; }
|
||||
public Vector2 Player2KingPosition { get; set; }
|
||||
public List<Piece> Player1Hand { get; }
|
||||
public List<Piece> Player2Hand { get; }
|
||||
public List<Move> 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<string, Piece?>(81);
|
||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||
InitializeBoardState();
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
MoveHistory = new List<Move>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||
/// </summary>
|
||||
public bool IsPathBlocked(IEnumerable<Vector2> 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<BoardTile> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does not include the start position.
|
||||
/// </summary>
|
||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||
{
|
||||
var next = start;
|
||||
while (IsWithinBoardBoundary(next + direction))
|
||||
{
|
||||
next += direction;
|
||||
yield return next;
|
||||
}
|
||||
}
|
||||
|
||||
internal Piece? GetFirstPieceAlongPath(IEnumerable<Vector2> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <param name="element">Guaranteed to be non-null.</param>
|
||||
/// <param name="position"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,82 +17,64 @@ namespace Shogi.Domain
|
||||
/// </summary>
|
||||
/// <param name="fromNotation">The position of the piece being moved expressed in board notation.</param>
|
||||
/// <param name="toNotation">The target position expressed in board notation.</param>
|
||||
/// <param name="isPromotionRequested">True if a promotion is expected as a result of this move.</param>
|
||||
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
|
||||
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 (board.WhoseTurn == WhichPlayer.Player1 && (to.Y > 5 || from.Y > 5))
|
||||
if (isPromotionRequested && (boardState.IsWithinPromotionZone(to) || boardState.IsWithinPromotionZone(from)))
|
||||
{
|
||||
fromPiece.Promote();
|
||||
}
|
||||
else if (board.WhoseTurn == WhichPlayer.Player2 && (to.Y < 3 || from.Y < 3))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move a piece from the hand to the board ignorant if check or check-mate.
|
||||
/// </summary>
|
||||
/// <param name="pieceInHand"></param>
|
||||
/// <param name="to">The target position expressed in board notation.</param>
|
||||
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
|
||||
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)
|
||||
/// <summary>
|
||||
/// Determines if the last move put the player who moved in check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin.
|
||||
/// </summary>
|
||||
/// <param name="element">The pathing element.</param>
|
||||
/// <param name="origin">The starting location.</param>
|
||||
/// <param name="destination">The destination.</param>
|
||||
/// <param name="callback">Do cool stuff here.</param>
|
||||
/// <returns>True if the element reached the destination.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path the line from origin to destination, ignoring any Paths defined by the element at origin.
|
||||
/// </summary>
|
||||
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<Move> 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."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user