This commit is contained in:
2021-12-29 16:27:43 -06:00
parent a2f3abb94e
commit 9ec91615a3
5 changed files with 157 additions and 102 deletions

View File

@@ -1,13 +1,13 @@
{ {
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": { "iisSettings": {
"windowsAuthentication": false, "windowsAuthentication": false,
"anonymousAuthentication": true, "anonymousAuthentication": true,
"iisExpress": { "iisExpress": {
"applicationUrl": "http://localhost:63676", "applicationUrl": "http://localhost:50728/",
"sslPort": 44396 "sslPort": 44315
} }
}, },
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",

View File

@@ -17,10 +17,12 @@ namespace Shogi.Domain.UnitTests
//[Fact] //[Fact]
//public void InitializeBoardStateWithMoves() //public void InitializeBoardStateWithMoves()
//{ //{
// var board = new ShogiBoardState();
// var rules = new StandardRules(board);
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("A3", "A4") // new Move("A3", "A4")
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// shogi.Board["A3"].Should().BeNull(); // shogi.Board["A3"].Should().BeNull();
@@ -33,12 +35,12 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6"), // new Move("G7", "G6"),
// // P1 Bishop puts P2 in check // // P1 Bishop puts P2 in check
// new Move("B2", "G7"), // new Move("B2", "G7"),
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// shogi.InCheck.Should().Be(WhichPlayer.Player2); // shogi.InCheck.Should().Be(WhichPlayer.Player2);
@@ -158,12 +160,12 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6"), // new Move("G7", "G6"),
// // P1 Bishop puts P2 in check // // P1 Bishop puts P2 in check
// new Move("B2", "G7") // new Move("B2", "G7")
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// shogi.InCheck.Should().Be(WhichPlayer.Player2); // shogi.InCheck.Should().Be(WhichPlayer.Player2);
@@ -184,26 +186,26 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("I7", "I6"), // new Move("I7", "I6"),
// // P1 Bishop takes P2 Pawn. // // P1 Bishop takes P2 Pawn.
// new Move("B2", "G7"), // new Move("B2", "G7"),
// // P2 Gold, block check from P1 Bishop. // // P2 Gold, block check from P1 Bishop.
// new Move("F9", "F8"), // new Move("F9", "F8"),
// // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance // // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
// new Move("G7", "H8", true), // new Move("G7", "H8", true),
// // P2 Pawn again // // P2 Pawn again
// new Move("I6", "I5"), // new Move("I6", "I5"),
// // P1 Bishop takes P2 Knight // // P1 Bishop takes P2 Knight
// new Move("H8", "H9"), // new Move("H8", "H9"),
// // P2 Pawn again // // P2 Pawn again
// new Move("I5", "I4"), // new Move("I5", "I4"),
// // P1 Bishop takes P2 Lance // // P1 Bishop takes P2 Lance
// new Move("H9", "I9"), // new Move("H9", "I9"),
// // P2 Pawn captures P1 Pawn // // P2 Pawn captures P1 Pawn
// new Move("I4", "I3") // new Move("I4", "I3")
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// shogi.Player1Hand.Count.Should().Be(4); // shogi.Player1Hand.Count.Should().Be(4);
@@ -251,20 +253,20 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6"), // new Move("G7", "G6"),
// // P1 Pawn, arbitrary move. // // P1 Pawn, arbitrary move.
// new Move("A3", "A4"), // new Move("A3", "A4"),
// // P2 Bishop takes P1 Bishop // // P2 Bishop takes P1 Bishop
// new Move("H8", "B2"), // new Move("H8", "B2"),
// // P1 Silver takes P2 Bishop // // P1 Silver takes P2 Bishop
// new Move("C1", "B2"), // new Move("C1", "B2"),
// // P2 Pawn, arbtrary move // // P2 Pawn, arbtrary move
// new Move("A7", "A6"), // new Move("A7", "A6"),
// // P1 drop Bishop, place P2 in check // // P1 drop Bishop, place P2 in check
// new Move(WhichPiece.Bishop, "G7") // new Move(WhichPiece.Bishop, "G7")
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// shogi.InCheck.Should().Be(WhichPlayer.Player2); // shogi.InCheck.Should().Be(WhichPlayer.Player2);
@@ -287,14 +289,14 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6"), // new Move("G7", "G6"),
// // P1 Bishop capture P2 Bishop // // P1 Bishop capture P2 Bishop
// new Move("B2", "H8"), // new Move("B2", "H8"),
// // P2 Pawn // // P2 Pawn
// new Move("G6", "G5") // new Move("G6", "G5")
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// using (new AssertionScope()) // using (new AssertionScope())
@@ -325,10 +327,10 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4"), // new Move("C3", "C4"),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6"), // new Move("G7", "G6"),
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
@@ -345,10 +347,10 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Pawn // // P1 Pawn
// new Move("C3", "C4" ), // new Move("C3", "C4" ),
// // P2 Pawn // // P2 Pawn
// new Move("G7", "G6" ) // new Move("G7", "G6" )
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
@@ -373,26 +375,26 @@ namespace Shogi.Domain.UnitTests
// // Arrange // // Arrange
// var moves = new[] // var moves = new[]
// { // {
// // P1 Rook // // P1 Rook
// new Move("H2", "E2"), // new Move("H2", "E2"),
// // P2 Gold // // P2 Gold
// new Move("F9", "G8"), // new Move("F9", "G8"),
// // P1 Pawn // // P1 Pawn
// new Move("E3", "E4"), // new Move("E3", "E4"),
// // P2 other Gold // // P2 other Gold
// new Move("D9", "C8"), // new Move("D9", "C8"),
// // P1 same Pawn // // P1 same Pawn
// new Move("E4", "E5"), // new Move("E4", "E5"),
// // P2 Pawn // // P2 Pawn
// new Move("E7", "E6"), // new Move("E7", "E6"),
// // P1 Pawn takes P2 Pawn // // P1 Pawn takes P2 Pawn
// new Move("E5", "E6"), // new Move("E5", "E6"),
// // P2 King // // P2 King
// new Move("E9", "E8"), // new Move("E9", "E8"),
// // P1 Pawn promotes, threatens P2 King // // P1 Pawn promotes, threatens P2 King
// new Move("E6", "E7", true), // new Move("E6", "E7", true),
// // P2 King retreat // // P2 King retreat
// new Move("E8", "E9"), // new Move("E8", "E9"),
// }; // };
// var shogi = new Shogi(moves); // var shogi = new Shogi(moves);
// output.WriteLine(shogi.PrintStateAsAscii()); // output.WriteLine(shogi.PrintStateAsAscii());

View File

@@ -0,0 +1,31 @@
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();
}
}
}

View File

@@ -31,25 +31,21 @@
public MoveResult CanMove(string from, string to, bool isPromotion) public MoveResult CanMove(string from, string to, bool isPromotion)
{ {
// TODO: ShogiBoardState.FromBoardNotation should not throw an execption in this query method.
var fromVector = ShogiBoardState.FromBoardNotation(from);
var toVector = ShogiBoardState.FromBoardNotation(to);
var simulator = new StandardRules(new ShogiBoardState(board)); var simulator = new StandardRules(new ShogiBoardState(board));
return simulator.Move(fromVector, toVector, isPromotion); return simulator.Move(from, to, isPromotion);
} }
public MoveResult CanMove(WhichPiece pieceInHand, string to) public MoveResult CanMove(WhichPiece pieceInHand, string to)
{ {
var toVector = ShogiBoardState.FromBoardNotation(to);
var simulator = new StandardRules(new ShogiBoardState(board)); var simulator = new StandardRules(new ShogiBoardState(board));
return simulator.Move(pieceInHand, toVector); return simulator.Move(pieceInHand, to);
} }
public void Move(string from, string to, bool isPromotion) public void Move(string from, string to, bool isPromotion)
{ {
var fromVector = ShogiBoardState.FromBoardNotation(from); var fromVector = ShogiBoardState.FromBoardNotation(from);
var toVector = ShogiBoardState.FromBoardNotation(to); var toVector = ShogiBoardState.FromBoardNotation(to);
var moveResult = rules.Move(fromVector, toVector, isPromotion); var moveResult = rules.Move(from, to, isPromotion);
if (!moveResult.Success) if (!moveResult.Success)
{ {
throw new InvalidOperationException(moveResult.Reason); throw new InvalidOperationException(moveResult.Reason);

View File

@@ -1,5 +1,7 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Shogi.Domain.UnitTests")]
namespace Shogi.Domain namespace Shogi.Domain
{ {
internal class StandardRules internal class StandardRules
@@ -39,11 +41,13 @@ namespace Shogi.Domain
/// <summary> /// <summary>
/// Move a piece from a board tile to another board tile. /// Move a piece from a board tile to another board tile.
/// </summary> /// </summary>
/// <param name="from">The position of the piece being moved expressed in board notation.</param> /// <param name="fromNotation">The position of the piece being moved expressed in board notation.</param>
/// <param name="to">The target position expressed in board notation.</param> /// <param name="toNotation">The target position expressed in board notation.</param>
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns> /// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
public MoveResult Move(Vector2 from, Vector2 to, bool isPromotion = false) public MoveResult Move(string fromNotation, string toNotation, bool isPromotion = false)
{ {
var from = ShogiBoardState.FromBoardNotation(fromNotation);
var to = ShogiBoardState.FromBoardNotation(toNotation);
var fromPiece = board[from]; var fromPiece = board[from];
if (fromPiece == null) if (fromPiece == null)
{ {
@@ -55,7 +59,7 @@ namespace Shogi.Domain
return new MoveResult(false, "Not allowed to move the opponents piece"); return new MoveResult(false, "Not allowed to move the opponents piece");
} }
if (IsPathable(from, to) == false) if (ShogiIsPathable(from, to) == false)
{ {
return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}."); return new MoveResult(false, $"Proposed move is not part of the move-set for piece {fromPiece.WhichPiece}.");
} }
@@ -106,8 +110,9 @@ namespace Shogi.Domain
/// <param name="pieceInHand"></param> /// <param name="pieceInHand"></param>
/// <param name="to">The target position expressed in board notation.</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> /// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
public MoveResult Move(WhichPiece pieceInHand, Vector2 to) public MoveResult Move(WhichPiece pieceInHand, string toNotation)
{ {
var to = ShogiBoardState.FromBoardNotation(toNotation);
var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand); var index = board.Hand.FindIndex(p => p.WhichPiece == pieceInHand);
if (index == -1) if (index == -1)
{ {
@@ -150,7 +155,7 @@ namespace Shogi.Domain
return new MoveResult(true); return new MoveResult(true);
} }
private bool IsPathable(Vector2 from, Vector2 to) private bool ShogiIsPathable(Vector2 from, Vector2 to)
{ {
var piece = board[from]; var piece = board[from];
if (piece == null) return false; if (piece == null) return false;
@@ -255,7 +260,9 @@ namespace Shogi.Domain
PathEvery(from, (other, position) => PathEvery(from, (other, position) =>
{ {
var simulationBoard = new StandardRules(new ShogiBoardState(board)); var simulationBoard = new StandardRules(new ShogiBoardState(board));
var simulationResult = simulationBoard.Move(from, position, false); var fromNotation = ShogiBoardState.ToBoardNotation(from);
var toNotation = ShogiBoardState.ToBoardNotation(position);
var simulationResult = simulationBoard.Move(fromNotation, toNotation, false);
if (simulationResult.Success) if (simulationResult.Success)
{ {
if (!EvaluateCheckAfterMove(from, position, board.InCheck.Value)) if (!EvaluateCheckAfterMove(from, position, board.InCheck.Value))
@@ -288,7 +295,7 @@ namespace Shogi.Domain
if (piece == null) return false; if (piece == null) return false;
var path = FindDirectionTowardsDestination(GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown), origin, destination); var path = FindDirectionTowardsDestination(GetMoveSet(piece.WhichPiece).GetMoves(piece.IsUpsideDown), origin, destination);
if (!IsPathable(origin, destination)) if (!IsPathable(origin, destination, path.Direction))
{ {
// Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen.
return false; return false;
@@ -341,6 +348,25 @@ namespace Shogi.Domain
} }
} }
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> /// <summary>
/// Path the line from origin to destination, ignoring any Paths defined by the element at origin. /// Path the line from origin to destination, ignoring any Paths defined by the element at origin.
/// </summary> /// </summary>