Check mate implementation

This commit is contained in:
2024-10-28 19:21:16 -05:00
parent a3d6065e95
commit a7e26f5210
4 changed files with 96 additions and 59 deletions

View File

@@ -1,13 +1,10 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Shogi.Api.Application; using Shogi.Api.Application;
using Shogi.Api.Extensions; using Shogi.Api.Extensions;
using Shogi.Api.Identity;
using Shogi.Api.Repositories; using Shogi.Api.Repositories;
using Shogi.Contracts.Api; using Shogi.Contracts.Api;
using Shogi.Contracts.Types; using Shogi.Contracts.Types;
using System.Security.Claims;
namespace Shogi.Api.Controllers; namespace Shogi.Api.Controllers;

View File

@@ -6,17 +6,12 @@ namespace Shogi.Domain.ValueObjects;
/// The board is always from Player1's perspective. /// The board is always from Player1's perspective.
/// [0,0] is the lower-left position, [8,8] is the higher-right position /// [0,0] is the lower-left position, [8,8] is the higher-right position
/// </summary> /// </summary>
public sealed class ShogiBoard public sealed class ShogiBoard(BoardState initialState)
{ {
private static readonly int[] zeroToEight = [0, 1, 2, 3, 4, 5, 6, 7, 8]; private static readonly int[] zeroToEight = [0, 1, 2, 3, 4, 5, 6, 7, 8];
private static readonly Vector2 BoardSize = new(9, 9); private static readonly Vector2 BoardSize = new(9, 9);
public ShogiBoard(BoardState initialState) public BoardState BoardState { get; } = initialState;
{
BoardState = initialState;
}
public BoardState BoardState { get; }
/// <summary> /// <summary>
@@ -67,13 +62,29 @@ public sealed class ShogiBoard
: inCheckResult.HasFlag(InCheckResult.Player1InCheck); : inCheckResult.HasFlag(InCheckResult.Player1InCheck);
// Move is legal; mutate the real state. // Move is legal; mutate the real state.
BoardState.Move(from, to, isPromotion);
if (playerPutOpponentInCheck) if (playerPutOpponentInCheck)
{ {
BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1
? WhichPlayer.Player2 ? WhichPlayer.Player2
: WhichPlayer.Player1; : WhichPlayer.Player1;
} }
else if (inCheckResult == InCheckResult.NobodyInCheck)
{
BoardState.InCheck = null;
}
BoardState.Move(from, to, isPromotion);
if (BoardState.InCheck.HasValue)
{
var gameOverResult = EvaluateGameOver();
BoardState.IsCheckmate = gameOverResult switch
{
GameOverResult.GameIsNotOver => false,
GameOverResult.Player1Wins => true,
GameOverResult.Player2Wins => true,
_ => throw new InvalidOperationException("Unexpected GameOverResult value.")
};
}
return new MoveResult(true); return new MoveResult(true);
} }
@@ -154,24 +165,35 @@ public sealed class ShogiBoard
: inCheckResult.HasFlag(InCheckResult.Player1InCheck); : inCheckResult.HasFlag(InCheckResult.Player1InCheck);
// Move is legal; mutate the real state. // Move is legal; mutate the real state.
BoardState.Move(pieceInHand, to);
if (playerPutOpponentInCheck) if (playerPutOpponentInCheck)
{ {
BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1
? WhichPlayer.Player2 ? WhichPlayer.Player2
: WhichPlayer.Player1; : WhichPlayer.Player1;
} }
else if (inCheckResult == InCheckResult.NobodyInCheck)
{
BoardState.InCheck = null;
}
BoardState.Move(pieceInHand, to);
// A pawn, placed from the hand, cannot be the cause of checkmate. // A pawn, placed from the hand, cannot be the cause of checkmate.
// if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) if (BoardState.InCheck.HasValue && pieceInHand != WhichPiece.Pawn)
// { {
// BoardState.IsCheckmate = true; var gameOverResult = EvaluateGameOver();
// } BoardState.IsCheckmate = gameOverResult switch
{
GameOverResult.GameIsNotOver => false,
GameOverResult.Player1Wins => true,
GameOverResult.Player2Wins => true,
_ => throw new InvalidOperationException("Unexpected GameOverResult value.")
};
}
return new MoveResult(true); return new MoveResult(true);
} }
private GameOverResult EvaluateGameOver() private GameOverResult EvaluateGameOver()
{ {
if (!BoardState.InCheck.HasValue) if (!BoardState.InCheck.HasValue)
@@ -220,11 +242,18 @@ public sealed class ShogiBoard
{ {
var list = new List<Vector2>(10); var list = new List<Vector2>(10);
var position = path.Step + piecePosition; var position = path.Step + piecePosition;
if (path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep)
{
while (position.IsInsideBoardBoundary()) while (position.IsInsideBoardBoundary())
{ {
list.Add(position); list.Add(position);
position += path.Step; position += path.Step;
} }
} else if (position.IsInsideBoardBoundary())
{
list.Add(position);
}
return list; return list;
}) })
@@ -292,7 +321,9 @@ public sealed class ShogiBoard
{ {
return new MoveResult(false, $"There is no piece at position {from}."); return new MoveResult(false, $"There is no piece at position {from}.");
} }
var matchingPaths = piece.MoveSet.Where(p => p.NormalizedStep == Vector2.Normalize(to - from));
var clampedFromTo = Vector2.Clamp(to - from, -Vector2.One, Vector2.One);
var matchingPaths = piece.MoveSet.Where(p => p.Step == clampedFromTo);
if (!matchingPaths.Any()) if (!matchingPaths.Any())
{ {
return new MoveResult(false, "Piece cannot move like that."); return new MoveResult(false, "Piece cannot move like that.");
@@ -303,6 +334,7 @@ public sealed class ShogiBoard
{ {
// Assert that no pieces exist along the from -> to path. // Assert that no pieces exist along the from -> to path.
var isPathObstructed = GetPositionsAlongPath(from, to, path) var isPathObstructed = GetPositionsAlongPath(from, to, path)
.SkipLast(1)
.Any(pos => BoardState[pos] != null); .Any(pos => BoardState[pos] != null);
if (isPathObstructed) if (isPathObstructed)
{ {

View File

@@ -51,15 +51,15 @@ public static class Extensions
for (var x = 0; x < 8; x++) builder.Append("- - "); for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -"); builder.Append("- -");
builder.AppendLine(); builder.AppendLine();
builder.Append(" ");
builder.Append("Player 1");
builder.AppendLine();
builder.AppendLine();
// Print File ruler. // Print File ruler.
builder.Append(" "); builder.Append(" ");
builder.Append(" A B C D E F G H I "); builder.Append(" A B C D E F G H I ");
builder.AppendLine();
builder.AppendLine();
builder.Append(" ");
builder.Append("Player 1");
return builder.ToString(); return builder.ToString();
} }

View File

@@ -65,10 +65,11 @@ namespace UnitTests
board["D5"].Should().BeNull(); board["D5"].Should().BeNull();
// Act // Act
var act = () => shogi.Move("D5", "D6", false); var moveResult = shogi.Move("D5", "D6", false);
// Assert // Assert
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
board["D5"].Should().BeNull(); board["D5"].Should().BeNull();
board["D6"].Should().BeNull(); board["D6"].Should().BeNull();
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
@@ -84,13 +85,13 @@ namespace UnitTests
var expectedPiece = board["A3"]; var expectedPiece = board["A3"];
// Act - P1 "moves" pawn to the position it already exists at. // Act - P1 "moves" pawn to the position it already exists at.
var act = () => shogi.Move("A3", "A3", false); var moveResult = shogi.Move("A3", "A3", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board["A3"].Should().Be(expectedPiece); moveResult.IsSuccess.Should().BeFalse(); board["A3"].Should().Be(expectedPiece);
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
} }
@@ -106,13 +107,13 @@ namespace UnitTests
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
// Act - Move Lance illegally // Act - Move Lance illegally
var act = () => shogi.Move("A1", "D5", false); var moveResult = shogi.Move("A1", "D5", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board["A1"].Should().Be(expectedPiece); moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(expectedPiece);
board["A5"].Should().BeNull(); board["A5"].Should().BeNull();
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
@@ -130,13 +131,13 @@ namespace UnitTests
board.WhoseTurn.Should().Be(WhichPlayer.Player1); board.WhoseTurn.Should().Be(WhichPlayer.Player1);
// Act - Move Player2 Pawn when it is Player1 turn. // Act - Move Player2 Pawn when it is Player1 turn.
var act = () => shogi.Move("A7", "A6", false); var moveResult = shogi.Move("A7", "A6", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board["A7"].Should().Be(expectedPiece); moveResult.IsSuccess.Should().BeFalse(); board["A7"].Should().Be(expectedPiece);
board["A6"].Should().BeNull(); board["A6"].Should().BeNull();
} }
} }
@@ -152,13 +153,13 @@ namespace UnitTests
lance!.Owner.Should().Be(pawn!.Owner); lance!.Owner.Should().Be(pawn!.Owner);
// Act - Move P1 Lance through P1 Pawn. // Act - Move P1 Lance through P1 Pawn.
var act = () => shogi.Move("A1", "A5", false); var moveResult = shogi.Move("A1", "A5", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board["A1"].Should().Be(lance); moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(lance);
board["A3"].Should().Be(pawn); board["A3"].Should().Be(pawn);
board["A5"].Should().BeNull(); board["A5"].Should().BeNull();
} }
@@ -175,13 +176,13 @@ namespace UnitTests
knight!.Owner.Should().Be(pawn!.Owner); knight!.Owner.Should().Be(pawn!.Owner);
// Act - P1 Knight tries to capture P1 Pawn. // Act - P1 Knight tries to capture P1 Pawn.
var act = () => shogi.Move("B1", "C3", false); var moveResult = shogi.Move("B1", "C3", false);
// Arrange // Arrange
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board["B1"].Should().Be(knight); moveResult.IsSuccess.Should().BeFalse(); board["B1"].Should().Be(knight);
board["C3"].Should().Be(pawn); board["C3"].Should().Be(pawn);
board.Player1Hand.Should().BeEmpty(); board.Player1Hand.Should().BeEmpty();
board.Player2Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty();
@@ -204,13 +205,13 @@ namespace UnitTests
var lance = board["I9"]; var lance = board["I9"];
// Act - P2 moves Lance while in check. // Act - P2 moves Lance while in check.
var act = () => shogi.Move("I9", "I8", false); var moveResult = shogi.Move("I9", "I8", false);
// Assert // Assert
using (new AssertionScope()) using (new AssertionScope())
{ {
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
board.InCheck.Should().Be(WhichPlayer.Player2); moveResult.IsSuccess.Should().BeFalse(); board.InCheck.Should().Be(WhichPlayer.Player2);
board["I9"].Should().Be(lance); board["I9"].Should().Be(lance);
board["I8"].Should().BeNull(); board["I8"].Should().BeNull();
} }
@@ -251,29 +252,33 @@ namespace UnitTests
// Act | Assert - Illegally placing Knight from the hand in farthest rank. // Act | Assert - Illegally placing Knight from the hand in farthest rank.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
var act = () => shogi.Move(WhichPiece.Knight, "H9"); var moveResult = shogi.Move(WhichPiece.Knight, "H9");
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally placing Knight from the hand in second farthest row. // Act | Assert - Illegally placing Knight from the hand in second farthest row.
board["H8"].Should().BeNull(); board["H8"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Knight, "H8"); moveResult = shogi.Move(WhichPiece.Knight, "H8");
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
board["H8"].Should().BeNull(); board["H8"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally place Lance from the hand. // Act | Assert - Illegally place Lance from the hand.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Knight, "H9"); moveResult = shogi.Move(WhichPiece.Knight, "H9");
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act | Assert - Illegally place Pawn from the hand. // Act | Assert - Illegally place Pawn from the hand.
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
act = () => shogi.Move(WhichPiece.Pawn, "H9"); moveResult = shogi.Move(WhichPiece.Pawn, "H9");
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
board["H9"].Should().BeNull(); board["H9"].Should().BeNull();
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
@@ -305,11 +310,12 @@ namespace UnitTests
shogi.BoardState["E5"].Should().BeNull(); shogi.BoardState["E5"].Should().BeNull();
// Act - P2 places a Bishop while in check. // Act - P2 places a Bishop while in check.
var act = () => shogi.Move(WhichPiece.Bishop, "E5"); var moveResult = shogi.Move(WhichPiece.Bishop, "E5");
// Assert // Assert
using var scope = new AssertionScope(); using var scope = new AssertionScope();
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
shogi.BoardState["E5"].Should().BeNull(); shogi.BoardState["E5"].Should().BeNull();
shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2);
shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
@@ -334,11 +340,12 @@ namespace UnitTests
shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2);
// Act - P1 tries to place a piece where an opponent's piece resides. // Act - P1 tries to place a piece where an opponent's piece resides.
var act = () => shogi.Move(WhichPiece.Bishop, "I9"); var moveResult = shogi.Move(WhichPiece.Bishop, "I9");
// Assert // Assert
using var scope = new AssertionScope(); using var scope = new AssertionScope();
act.Should().Throw<InvalidOperationException>(); moveResult.Should().NotBeNull();
moveResult.IsSuccess.Should().BeFalse();
shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.BoardState["I9"].Should().NotBeNull(); shogi.BoardState["I9"].Should().NotBeNull();
shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance);
@@ -439,12 +446,13 @@ namespace UnitTests
// P2 King retreat // P2 King retreat
shogi.Move("E8", "E9", false); shogi.Move("E8", "E9", false);
console.WriteLine(shogi.ToStringStateAsAscii());
// Act - P1 Pawn wins by checkmate. // Act - P1 Pawn wins by checkmate.
shogi.Move("E7", "E8", false); shogi.Move("E7", "E8", false);
// Assert - checkmate // Assert - checkmate
console.WriteLine(shogi.ToStringStateAsAscii());
console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString())));
board.IsCheckmate.Should().BeTrue(); board.IsCheckmate.Should().BeTrue();
board.InCheck.Should().Be(WhichPlayer.Player2); board.InCheck.Should().Be(WhichPlayer.Player2);
} }