From a7e26f5210034e4304a12c9ac88a083bd9415e17 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 28 Oct 2024 19:21:16 -0500 Subject: [PATCH] Check mate implementation --- Shogi.Api/Controllers/SessionsController.cs | 3 - Shogi.Domain/ValueObjects/ShogiBoard.cs | 66 +++++++++++++----- Tests/UnitTests/Extensions.cs | 10 +-- Tests/UnitTests/ShogiShould.cs | 76 ++++++++++++--------- 4 files changed, 96 insertions(+), 59 deletions(-) diff --git a/Shogi.Api/Controllers/SessionsController.cs b/Shogi.Api/Controllers/SessionsController.cs index 5d640dc..b690410 100644 --- a/Shogi.Api/Controllers/SessionsController.cs +++ b/Shogi.Api/Controllers/SessionsController.cs @@ -1,13 +1,10 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Shogi.Api.Application; using Shogi.Api.Extensions; -using Shogi.Api.Identity; using Shogi.Api.Repositories; using Shogi.Contracts.Api; using Shogi.Contracts.Types; -using System.Security.Claims; namespace Shogi.Api.Controllers; diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/ShogiBoard.cs index f961d65..74356c6 100644 --- a/Shogi.Domain/ValueObjects/ShogiBoard.cs +++ b/Shogi.Domain/ValueObjects/ShogiBoard.cs @@ -6,17 +6,12 @@ namespace Shogi.Domain.ValueObjects; /// The board is always from Player1's perspective. /// [0,0] is the lower-left position, [8,8] is the higher-right position /// -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 Vector2 BoardSize = new(9, 9); - public ShogiBoard(BoardState initialState) - { - BoardState = initialState; - } - - public BoardState BoardState { get; } + public BoardState BoardState { get; } = initialState; /// @@ -67,13 +62,29 @@ public sealed class ShogiBoard : inCheckResult.HasFlag(InCheckResult.Player1InCheck); // Move is legal; mutate the real state. - BoardState.Move(from, to, isPromotion); if (playerPutOpponentInCheck) { BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : 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); } @@ -154,24 +165,35 @@ public sealed class ShogiBoard : inCheckResult.HasFlag(InCheckResult.Player1InCheck); // Move is legal; mutate the real state. - BoardState.Move(pieceInHand, to); if (playerPutOpponentInCheck) { BoardState.InCheck = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : 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. - // if (rules.IsOpponentInCheckMate() && pieceInHand != WhichPiece.Pawn) - // { - // BoardState.IsCheckmate = true; - // } + if (BoardState.InCheck.HasValue && pieceInHand != WhichPiece.Pawn) + { + 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); } - private GameOverResult EvaluateGameOver() { if (!BoardState.InCheck.HasValue) @@ -220,10 +242,17 @@ public sealed class ShogiBoard { var list = new List(10); var position = path.Step + piecePosition; - while (position.IsInsideBoardBoundary()) + if (path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep) + { + + while (position.IsInsideBoardBoundary()) + { + list.Add(position); + position += path.Step; + } + } else if (position.IsInsideBoardBoundary()) { list.Add(position); - position += path.Step; } return list; @@ -292,7 +321,9 @@ public sealed class ShogiBoard { 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()) { 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. var isPathObstructed = GetPositionsAlongPath(from, to, path) + .SkipLast(1) .Any(pos => BoardState[pos] != null); if (isPathObstructed) { diff --git a/Tests/UnitTests/Extensions.cs b/Tests/UnitTests/Extensions.cs index 1aed531..f9486c1 100644 --- a/Tests/UnitTests/Extensions.cs +++ b/Tests/UnitTests/Extensions.cs @@ -51,15 +51,15 @@ public static class Extensions 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 "); + builder.AppendLine(); + builder.AppendLine(); + builder.Append(" "); + builder.Append("Player 1"); + return builder.ToString(); } diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 918c505..512de79 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -65,10 +65,11 @@ namespace UnitTests board["D5"].Should().BeNull(); // Act - var act = () => shogi.Move("D5", "D6", false); + var moveResult = shogi.Move("D5", "D6", false); // Assert - act.Should().Throw(); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["D5"].Should().BeNull(); board["D6"].Should().BeNull(); board.Player1Hand.Should().BeEmpty(); @@ -84,13 +85,13 @@ namespace UnitTests var expectedPiece = board["A3"]; // 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 using (new AssertionScope()) { - act.Should().Throw(); - board["A3"].Should().Be(expectedPiece); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A3"].Should().Be(expectedPiece); board.Player1Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty(); } @@ -106,13 +107,13 @@ namespace UnitTests expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); // Act - Move Lance illegally - var act = () => shogi.Move("A1", "D5", false); + var moveResult = shogi.Move("A1", "D5", false); // Assert using (new AssertionScope()) { - act.Should().Throw(); - board["A1"].Should().Be(expectedPiece); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(expectedPiece); board["A5"].Should().BeNull(); board.Player1Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty(); @@ -130,13 +131,13 @@ namespace UnitTests board.WhoseTurn.Should().Be(WhichPlayer.Player1); // Act - Move Player2 Pawn when it is Player1 turn. - var act = () => shogi.Move("A7", "A6", false); + var moveResult = shogi.Move("A7", "A6", false); // Assert using (new AssertionScope()) { - act.Should().Throw(); - board["A7"].Should().Be(expectedPiece); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A7"].Should().Be(expectedPiece); board["A6"].Should().BeNull(); } } @@ -152,13 +153,13 @@ namespace UnitTests lance!.Owner.Should().Be(pawn!.Owner); // Act - Move P1 Lance through P1 Pawn. - var act = () => shogi.Move("A1", "A5", false); + var moveResult = shogi.Move("A1", "A5", false); // Assert using (new AssertionScope()) { - act.Should().Throw(); - board["A1"].Should().Be(lance); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(lance); board["A3"].Should().Be(pawn); board["A5"].Should().BeNull(); } @@ -175,13 +176,13 @@ namespace UnitTests knight!.Owner.Should().Be(pawn!.Owner); // Act - P1 Knight tries to capture P1 Pawn. - var act = () => shogi.Move("B1", "C3", false); + var moveResult = shogi.Move("B1", "C3", false); // Arrange using (new AssertionScope()) { - act.Should().Throw(); - board["B1"].Should().Be(knight); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["B1"].Should().Be(knight); board["C3"].Should().Be(pawn); board.Player1Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty(); @@ -204,13 +205,13 @@ namespace UnitTests var lance = board["I9"]; // Act - P2 moves Lance while in check. - var act = () => shogi.Move("I9", "I8", false); + var moveResult = shogi.Move("I9", "I8", false); // Assert using (new AssertionScope()) { - act.Should().Throw(); - board.InCheck.Should().Be(WhichPlayer.Player2); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board.InCheck.Should().Be(WhichPlayer.Player2); board["I9"].Should().Be(lance); board["I8"].Should().BeNull(); } @@ -251,29 +252,33 @@ namespace UnitTests // Act | Assert - Illegally placing Knight from the hand in farthest rank. board["H9"].Should().BeNull(); - var act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); + var moveResult = shogi.Move(WhichPiece.Knight, "H9"); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["H9"].Should().BeNull(); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally placing Knight from the hand in second farthest row. board["H8"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H8"); - act.Should().Throw(); + moveResult = shogi.Move(WhichPiece.Knight, "H8"); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["H8"].Should().BeNull(); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally place Lance from the hand. board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); + moveResult = shogi.Move(WhichPiece.Knight, "H9"); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["H9"].Should().BeNull(); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); // Act | Assert - Illegally place Pawn from the hand. board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Pawn, "H9"); - act.Should().Throw(); + moveResult = shogi.Move(WhichPiece.Pawn, "H9"); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["H9"].Should().BeNull(); board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); @@ -305,11 +310,12 @@ namespace UnitTests shogi.BoardState["E5"].Should().BeNull(); // Act - P2 places a Bishop while in check. - var act = () => shogi.Move(WhichPiece.Bishop, "E5"); + var moveResult = shogi.Move(WhichPiece.Bishop, "E5"); // Assert using var scope = new AssertionScope(); - act.Should().Throw(); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); shogi.BoardState["E5"].Should().BeNull(); shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); @@ -334,11 +340,12 @@ namespace UnitTests shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); // 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 using var scope = new AssertionScope(); - act.Should().Throw(); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.BoardState["I9"].Should().NotBeNull(); shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); @@ -439,12 +446,13 @@ namespace UnitTests // P2 King retreat shogi.Move("E8", "E9", false); + console.WriteLine(shogi.ToStringStateAsAscii()); + + // Act - P1 Pawn wins by checkmate. shogi.Move("E7", "E8", false); // Assert - checkmate - console.WriteLine(shogi.ToStringStateAsAscii()); - console.WriteLine(string.Join(",", shogi.BoardState.Player1Hand.Select(p => p.WhichPiece.ToString()))); board.IsCheckmate.Should().BeTrue(); board.InCheck.Should().Be(WhichPlayer.Player2); }