From 0a415a229218f6b77697d04d00e3e5ef8f8073d3 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 5 Sep 2025 18:13:35 -0500 Subject: [PATCH 01/15] checkpoint --- BoardRules/BoardRules.cs | 120 +++ BoardRules/BoardRules.csproj | 9 + Shogi.Domain/ValueObjects/Enums.cs | 6 + .../Movement}/Direction.cs | 2 +- .../Movement}/Distance.cs | 2 +- .../ValueObjects/{ => Movement}/Move.cs | 0 .../ValueObjects/{ => Movement}/MoveResult.cs | 0 .../Pathing => ValueObjects/Movement}/Path.cs | 4 +- .../ValueObjects/{ => Pieces}/Bishop.cs | 2 +- .../ValueObjects/{ => Pieces}/GoldGeneral.cs | 2 +- .../ValueObjects/{ => Pieces}/King.cs | 2 +- .../ValueObjects/{ => Pieces}/Knight.cs | 2 +- .../ValueObjects/{ => Pieces}/Lance.cs | 2 +- .../ValueObjects/{ => Pieces}/Pawn.cs | 2 +- .../ValueObjects/{ => Pieces}/Piece.cs | 2 +- .../ValueObjects/{ => Pieces}/Rook.cs | 2 +- .../{ => Pieces}/SilverGeneral.cs | 2 +- .../ValueObjects/{ => Rules}/ShogiBoard.cs | 9 +- .../ValueObjects/{ => Rules}/StandardRules.cs | 0 Shogi.Domain/ValueObjects/WhichPlayer.cs | 8 - Shogi.sln | 6 + Tests/UnitTests/NotationShould.cs | 39 +- Tests/UnitTests/RookShould.cs | 2 +- Tests/UnitTests/ShogiShould.cs | 889 +++++++++--------- 24 files changed, 622 insertions(+), 492 deletions(-) create mode 100644 BoardRules/BoardRules.cs create mode 100644 BoardRules/BoardRules.csproj rename Shogi.Domain/{YetToBeAssimilatedIntoDDD/Pathing => ValueObjects/Movement}/Direction.cs (91%) rename Shogi.Domain/{YetToBeAssimilatedIntoDDD/Pathing => ValueObjects/Movement}/Distance.cs (80%) rename Shogi.Domain/ValueObjects/{ => Movement}/Move.cs (100%) rename Shogi.Domain/ValueObjects/{ => Movement}/MoveResult.cs (100%) rename Shogi.Domain/{YetToBeAssimilatedIntoDDD/Pathing => ValueObjects/Movement}/Path.cs (85%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Bishop.cs (96%) rename Shogi.Domain/ValueObjects/{ => Pieces}/GoldGeneral.cs (93%) rename Shogi.Domain/ValueObjects/{ => Pieces}/King.cs (93%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Knight.cs (94%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Lance.cs (94%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Pawn.cs (94%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Piece.cs (98%) rename Shogi.Domain/ValueObjects/{ => Pieces}/Rook.cs (96%) rename Shogi.Domain/ValueObjects/{ => Pieces}/SilverGeneral.cs (95%) rename Shogi.Domain/ValueObjects/{ => Rules}/ShogiBoard.cs (97%) rename Shogi.Domain/ValueObjects/{ => Rules}/StandardRules.cs (100%) delete mode 100644 Shogi.Domain/ValueObjects/WhichPlayer.cs diff --git a/BoardRules/BoardRules.cs b/BoardRules/BoardRules.cs new file mode 100644 index 0000000..6b98a7b --- /dev/null +++ b/BoardRules/BoardRules.cs @@ -0,0 +1,120 @@ +//using System.Drawing; +//using System.Numerics; + +//namespace BoardRules; + +//public static class PieceMoves +//{ +// public static readonly ICollection GoldGeneralMoves = +// [ +// new(-1, 1), +// new(0, 1), +// new(1, 1), +// new(-1, 0), +// new(1, 0), +// new(0, -1) +// ]; + +// public static readonly ICollection PawnMoves = [new(0, 1)]; + +// public static readonly ICollection KnightMoves = [new(-1, 2), new(1, 2)]; + +// public static readonly ICollection BishopMoves = +// [ +// new(float.NegativeInfinity, float.NegativeInfinity), +// new(float.NegativeInfinity, float.PositiveInfinity), +// new(float.PositiveInfinity, float.PositiveInfinity), +// new(float.PositiveInfinity, float.NegativeInfinity), +// ]; +//} + +//public class BoardRules +//{ +// private readonly Dictionary pieces = []; + +// public BoardRules WithSize(int width, int height) +// { +// this.BoardSize = new Size(width, height); +// return this; +// } + +// public Size BoardSize { get; private set; } + +// public BoardRules AddPiece(IPiece piece) +// { +// pieces.Add(piece.Name, piece); +// return this; +// } +//} + +//public class BoardPieceRules(BoardRules rules, IPiece piece) +//{ +// public IPiece Piece { get; } = piece; +// public BoardRules WithStartingPositions(ICollection positions) +// { +// // Validate positions against board size +// foreach (var pos in positions) +// { +// if (pos.X < 0 || pos.Y < 0 || pos.X >= rules.BoardSize.Width || pos.Y >= rules.BoardSize.Height) +// { +// throw new ArgumentOutOfRangeException(nameof(positions), $"Position {pos} is out of bounds for board size {rules.BoardSize}."); +// } +// } +// // Assuming piece has a way to set starting positions, which it currently does not. +// // This is just a placeholder to show intent. +// // piece.SetStartingPositions(positions); +// return rules; +// } +//} + +//public interface IPiece +//{ +// public string Name { get; } +// public ICollection MoveSet { get; } +// public ICollection PromotedMoveSet { get; } + +// /// +// /// The starting positions for this type of piece on the board. There could be one or many. +// /// +// public ICollection StartingPositions { get; } +//} + +//public class GoldGeneral : IPiece +//{ +// public string Name => nameof(GoldGeneral); +// public ICollection MoveSet => PieceMoves.GoldGeneralMoves; +// public ICollection PromotedMoveSet => PieceMoves.GoldGeneralMoves; + +// public ICollection StartingPositions => [new(3, 0), new(5, 0), new(4, 1)]; +//} + +//public class Pawn : IPiece +//{ +// public string Name => nameof(Pawn); +// public ICollection MoveSet => PieceMoves.PawnMoves; +// public ICollection PromotedMoveSet => PieceMoves.GoldGeneralMoves; +//} + +//public class Knight : IPiece +//{ +// public string Name => nameof(Knight); +// public ICollection MoveSet => PieceMoves.KnightMoves; +// public ICollection PromotedMoveSet => PieceMoves.GoldGeneralMoves; +//} + +//public class Bishop : IPiece +//{ +// public string Name => nameof(Bishop); +// public ICollection MoveSet => PieceMoves.BishopMoves; +// public ICollection PromotedMoveSet => PieceMoves.BishopMoves; +//} + +//public class Luke +//{ +// public void Yep() +// { +// var board = new BoardRules() +// .WithSize(9, 9) +// .AddPiece(new Pawn()) +// } +//} \ No newline at end of file diff --git a/BoardRules/BoardRules.csproj b/BoardRules/BoardRules.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/BoardRules/BoardRules.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Shogi.Domain/ValueObjects/Enums.cs b/Shogi.Domain/ValueObjects/Enums.cs index 0bc6749..e259d0c 100644 --- a/Shogi.Domain/ValueObjects/Enums.cs +++ b/Shogi.Domain/ValueObjects/Enums.cs @@ -32,3 +32,9 @@ public enum WhichPiece Pawn, //PromotedPawn, } + +public enum WhichPlayer +{ + Player1, + Player2 +} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs b/Shogi.Domain/ValueObjects/Movement/Direction.cs similarity index 91% rename from Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs rename to Shogi.Domain/ValueObjects/Movement/Direction.cs index 24dc12e..e2e71c8 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs +++ b/Shogi.Domain/ValueObjects/Movement/Direction.cs @@ -1,4 +1,4 @@ -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +namespace Shogi.Domain.ValueObjects; public static class Direction { diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs b/Shogi.Domain/ValueObjects/Movement/Distance.cs similarity index 80% rename from Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs rename to Shogi.Domain/ValueObjects/Movement/Distance.cs index 915f3af..dfa7362 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Distance.cs +++ b/Shogi.Domain/ValueObjects/Movement/Distance.cs @@ -1,4 +1,4 @@ -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +namespace Shogi.Domain.ValueObjects; public enum Distance { diff --git a/Shogi.Domain/ValueObjects/Move.cs b/Shogi.Domain/ValueObjects/Movement/Move.cs similarity index 100% rename from Shogi.Domain/ValueObjects/Move.cs rename to Shogi.Domain/ValueObjects/Movement/Move.cs diff --git a/Shogi.Domain/ValueObjects/MoveResult.cs b/Shogi.Domain/ValueObjects/Movement/MoveResult.cs similarity index 100% rename from Shogi.Domain/ValueObjects/MoveResult.cs rename to Shogi.Domain/ValueObjects/Movement/MoveResult.cs diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs b/Shogi.Domain/ValueObjects/Movement/Path.cs similarity index 85% rename from Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs rename to Shogi.Domain/ValueObjects/Movement/Path.cs index ffd22f3..14ee1a5 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs +++ b/Shogi.Domain/ValueObjects/Movement/Path.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +namespace Shogi.Domain.ValueObjects.Movement; [DebuggerDisplay("{Step} - {Distance}")] public record Path @@ -17,7 +17,7 @@ public record Path public Path(Vector2 step, Distance distance = Distance.OneStep) { Step = step; - this.Distance = distance; + Distance = distance; } public Path Invert() => new(Vector2.Negate(Step), Distance); diff --git a/Shogi.Domain/ValueObjects/Bishop.cs b/Shogi.Domain/ValueObjects/Pieces/Bishop.cs similarity index 96% rename from Shogi.Domain/ValueObjects/Bishop.cs rename to Shogi.Domain/ValueObjects/Pieces/Bishop.cs index 7f7d315..4e721f4 100644 --- a/Shogi.Domain/ValueObjects/Bishop.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Bishop.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs similarity index 93% rename from Shogi.Domain/ValueObjects/GoldGeneral.cs rename to Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs index e5b0628..a45a0b2 100644 --- a/Shogi.Domain/ValueObjects/GoldGeneral.cs +++ b/Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects; diff --git a/Shogi.Domain/ValueObjects/King.cs b/Shogi.Domain/ValueObjects/Pieces/King.cs similarity index 93% rename from Shogi.Domain/ValueObjects/King.cs rename to Shogi.Domain/ValueObjects/Pieces/King.cs index d7cf66c..6f66cf5 100644 --- a/Shogi.Domain/ValueObjects/King.cs +++ b/Shogi.Domain/ValueObjects/Pieces/King.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects; diff --git a/Shogi.Domain/ValueObjects/Knight.cs b/Shogi.Domain/ValueObjects/Pieces/Knight.cs similarity index 94% rename from Shogi.Domain/ValueObjects/Knight.cs rename to Shogi.Domain/ValueObjects/Pieces/Knight.cs index d54cb98..acccfb8 100644 --- a/Shogi.Domain/ValueObjects/Knight.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Knight.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Lance.cs b/Shogi.Domain/ValueObjects/Pieces/Lance.cs similarity index 94% rename from Shogi.Domain/ValueObjects/Lance.cs rename to Shogi.Domain/ValueObjects/Pieces/Lance.cs index 690a77c..aaf54e9 100644 --- a/Shogi.Domain/ValueObjects/Lance.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Lance.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Pawn.cs b/Shogi.Domain/ValueObjects/Pieces/Pawn.cs similarity index 94% rename from Shogi.Domain/ValueObjects/Pawn.cs rename to Shogi.Domain/ValueObjects/Pieces/Pawn.cs index 6494f74..7ce3e22 100644 --- a/Shogi.Domain/ValueObjects/Pawn.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Pawn.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Pieces/Piece.cs similarity index 98% rename from Shogi.Domain/ValueObjects/Piece.cs rename to Shogi.Domain/ValueObjects/Pieces/Piece.cs index 51826d8..d57e8ec 100644 --- a/Shogi.Domain/ValueObjects/Piece.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Piece.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Diagnostics; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/Rook.cs b/Shogi.Domain/ValueObjects/Pieces/Rook.cs similarity index 96% rename from Shogi.Domain/ValueObjects/Rook.cs rename to Shogi.Domain/ValueObjects/Pieces/Rook.cs index 45ab9d6..5624600 100644 --- a/Shogi.Domain/ValueObjects/Rook.cs +++ b/Shogi.Domain/ValueObjects/Pieces/Rook.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects; diff --git a/Shogi.Domain/ValueObjects/SilverGeneral.cs b/Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs similarity index 95% rename from Shogi.Domain/ValueObjects/SilverGeneral.cs rename to Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs index a37becb..82c5519 100644 --- a/Shogi.Domain/ValueObjects/SilverGeneral.cs +++ b/Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Collections.ObjectModel; namespace Shogi.Domain.ValueObjects diff --git a/Shogi.Domain/ValueObjects/ShogiBoard.cs b/Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs similarity index 97% rename from Shogi.Domain/ValueObjects/ShogiBoard.cs rename to Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs index e0bb92e..b139566 100644 --- a/Shogi.Domain/ValueObjects/ShogiBoard.cs +++ b/Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs @@ -1,4 +1,5 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD; +using Shogi.Domain.ValueObjects.Movement; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; namespace Shogi.Domain.ValueObjects; /// @@ -247,7 +248,7 @@ public sealed class ShogiBoard(BoardState initialState) { var list = new List(10); var position = path.Step + piecePosition; - if (path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep) + if (path.Distance == Distance.MultiStep) { while (position.IsInsideBoardBoundary()) @@ -340,7 +341,7 @@ public sealed class ShogiBoard(BoardState initialState) else { var multiStepPaths = matchingPaths - .Where(path => path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep) + .Where(path => path.Distance == Distance.MultiStep) .ToArray(); if (multiStepPaths.Length == 0) { @@ -371,7 +372,7 @@ public sealed class ShogiBoard(BoardState initialState) return new MoveResult(true); } - private static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, YetToBeAssimilatedIntoDDD.Pathing.Path path) + private static IEnumerable GetPositionsAlongPath(Vector2 from, Vector2 to, Path path) { var next = from; while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9) diff --git a/Shogi.Domain/ValueObjects/StandardRules.cs b/Shogi.Domain/ValueObjects/Rules/StandardRules.cs similarity index 100% rename from Shogi.Domain/ValueObjects/StandardRules.cs rename to Shogi.Domain/ValueObjects/Rules/StandardRules.cs diff --git a/Shogi.Domain/ValueObjects/WhichPlayer.cs b/Shogi.Domain/ValueObjects/WhichPlayer.cs deleted file mode 100644 index 796c095..0000000 --- a/Shogi.Domain/ValueObjects/WhichPlayer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Shogi.Domain.ValueObjects -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Shogi.sln b/Shogi.sln index bba4cc1..349696f 100644 --- a/Shogi.sln +++ b/Shogi.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Api", "Shogi.Api\Shog EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETests", "Tests\E2ETests\E2ETests.csproj", "{401120C3-45D6-4A23-8D87-C2BED29F4950}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\BoardRules.csproj", "{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {401120C3-45D6-4A23-8D87-C2BED29F4950}.Debug|Any CPU.Build.0 = Debug|Any CPU {401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.ActiveCfg = Release|Any CPU {401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.Build.0 = Release|Any CPU + {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/UnitTests/NotationShould.cs b/Tests/UnitTests/NotationShould.cs index d42f72f..577b8a4 100644 --- a/Tests/UnitTests/NotationShould.cs +++ b/Tests/UnitTests/NotationShould.cs @@ -1,26 +1,25 @@ -using System.Numerics; -using Shogi.Domain.YetToBeAssimilatedIntoDDD; +using Shogi.Domain.YetToBeAssimilatedIntoDDD; +using System.Numerics; -namespace UnitTests +namespace UnitTests; + +public class NotationShould { - public class NotationShould + [Fact] + public void ConvertFromNotationToVector() { - [Fact] - public void ConvertFromNotationToVector() - { - Notation.FromBoardNotation("A1").Should().Be(new Vector2(0, 0)); - Notation.FromBoardNotation("E5").Should().Be(new Vector2(4, 4)); - Notation.FromBoardNotation("I9").Should().Be(new Vector2(8, 8)); - Notation.FromBoardNotation("C3").Should().Be(new Vector2(2, 2)); - } + Notation.FromBoardNotation("A1").Should().Be(new Vector2(0, 0)); + Notation.FromBoardNotation("E5").Should().Be(new Vector2(4, 4)); + Notation.FromBoardNotation("I9").Should().Be(new Vector2(8, 8)); + Notation.FromBoardNotation("C3").Should().Be(new Vector2(2, 2)); + } - [Fact] - public void ConvertFromVectorToNotation() - { - Notation.ToBoardNotation(new Vector2(0, 0)).Should().Be("A1"); - Notation.ToBoardNotation(new Vector2(4, 4)).Should().Be("E5"); - Notation.ToBoardNotation(new Vector2(8, 8)).Should().Be("I9"); - Notation.ToBoardNotation(new Vector2(2, 2)).Should().Be("C3"); - } + [Fact] + public void ConvertFromVectorToNotation() + { + Notation.ToBoardNotation(new Vector2(0, 0)).Should().Be("A1"); + Notation.ToBoardNotation(new Vector2(4, 4)).Should().Be("E5"); + Notation.ToBoardNotation(new Vector2(8, 8)).Should().Be("I9"); + Notation.ToBoardNotation(new Vector2(2, 2)).Should().Be("C3"); } } diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index c48b0ea..5d0ab72 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -1,5 +1,5 @@ using Shogi.Domain.ValueObjects; -using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using Shogi.Domain.ValueObjects.Movement; using System.Numerics; namespace UnitTests; diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 064e303..f35ee88 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -1,460 +1,457 @@ using Shogi.Domain.ValueObjects; -using System; -using System.Linq; -namespace UnitTests +namespace UnitTests; + +public class ShogiShould { - public class ShogiShould + private readonly ITestOutputHelper console; + public ShogiShould(ITestOutputHelper console) { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) + this.console = console; + } + + [Fact] + public void MoveAPieceToAnEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + + board["A4"].Should().BeNull(); + var expectedPiece = board["A3"]; + expectedPiece.Should().NotBeNull(); + + // Act + shogi.Move("A3", "A4", false); + + // Assert + board["A3"].Should().BeNull(); + board["A4"].Should().Be(expectedPiece); + } + + [Fact] + public void AllowValidMoves_AfterCheck() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop puts P2 in check + shogi.Move("B2", "G7", false); + board.InCheck.Should().Be(WhichPlayer.Player2); + + // Act - P2 is able to un-check theirself. + /// P2 King moves out of check + shogi.Move("E9", "E8", false); + + // Assert + using (new AssertionScope()) { - this.console = console; + board.InCheck.Should().BeNull(); } + } - [Fact] - public void MoveAPieceToAnEmptyPosition() + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + board["D5"].Should().BeNull(); + + // Act + var moveResult = shogi.Move("D5", "D6", false); + + // Assert + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); + board["D5"].Should().BeNull(); + board["D6"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } + + [Fact] + public void PreventInvalidMoves_MoveToCurrentPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A3"]; + + // Act - P1 "moves" pawn to the position it already exists at. + var moveResult = shogi.Move("A3", "A3", false); + + // Assert + using (new AssertionScope()) { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - - board["A4"].Should().BeNull(); - var expectedPiece = board["A3"]; - expectedPiece.Should().NotBeNull(); - - // Act - shogi.Move("A3", "A4", false); - - // Assert - board["A3"].Should().BeNull(); - board["A4"].Should().Be(expectedPiece); - } - - [Fact] - public void AllowValidMoves_AfterCheck() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop puts P2 in check - shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); - - // Act - P2 is able to un-check theirself. - /// P2 King moves out of check - shogi.Move("E9", "E8", false); - - // Assert - using (new AssertionScope()) - { - board.InCheck.Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - board["D5"].Should().BeNull(); - - // Act - var moveResult = shogi.Move("D5", "D6", false); - - // Assert moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["D5"].Should().BeNull(); - board["D6"].Should().BeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A3"].Should().Be(expectedPiece); board.Player1Hand.Should().BeEmpty(); board.Player2Hand.Should().BeEmpty(); } - - [Fact] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["A3"]; - - // Act - P1 "moves" pawn to the position it already exists at. - var moveResult = shogi.Move("A3", "A3", false); - - // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A3"].Should().Be(expectedPiece); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveSet() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["D1"]; - expectedPiece!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - - // Act - Move General illegally - var moveResult = shogi.Move("D1", "D5", false); - - // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["D1"].Should().Be(expectedPiece); - board["D5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } - - [Fact] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var expectedPiece = board["A7"]; - expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); - - // Act - Move Player2 Pawn when it is Player1 turn. - var moveResult = shogi.Move("A7", "A6", false); - - // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A7"].Should().Be(expectedPiece); - board["A6"].Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var lance = board["A1"]; - var pawn = board["A3"]; - lance!.Owner.Should().Be(pawn!.Owner); - - // Act - Move P1 Lance through P1 Pawn. - var moveResult = shogi.Move("A1", "A5", false); - - // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(lance); - board["A3"].Should().Be(pawn); - board["A5"].Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_CaptureAlly() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var knight = board["B1"]; - var pawn = board["C3"]; - knight!.Owner.Should().Be(pawn!.Owner); - - // Act - P1 Knight tries to capture P1 Pawn. - var moveResult = shogi.Move("B1", "C3", false); - - // Arrange - using (new AssertionScope()) - { - 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(); - } - } - - [Fact] - public void PreventInvalidMoves_Check() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // 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 moveResult = shogi.Move("I9", "I8", false); - - // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().Be(lance); - board["I8"].Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("I7", "I6", false); - // P1 Bishop takes P2 Pawn. - shogi.Move("B2", "G7", false); - // P2 Gold, block check from P1 Bishop. - shogi.Move("F9", "F8", false); - // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - shogi.Move("G7", "H8", true); - // P2 Pawn again - shogi.Move("I6", "I5", false); - // P1 Bishop takes P2 Knight - shogi.Move("H8", "H9", false); - // P2 Pawn again - shogi.Move("I5", "I4", false); - // P1 Bishop takes P2 Lance - 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); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); - - // Act | Assert - Illegally placing Knight from the hand in farthest rank. - board["H9"].Should().BeNull(); - 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(); - 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(); - 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(); - moveResult = shogi.Move(WhichPiece.Pawn, "H9"); - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - 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 - } - - [Fact] - public void PreventInvalidDrop_Check() - { - // Arrange - var shogi = MockShogiBoard(); - // P1 Pawn - shogi.Move("C3", "C4", false).IsSuccess.Should().BeTrue(); - // P2 Pawn - shogi.Move("G7", "G6", false).IsSuccess.Should().BeTrue(); - // P1 Pawn, arbitrary move. - shogi.Move("A3", "A4", false).IsSuccess.Should().BeTrue(); - // P2 Bishop takes P1 Bishop - shogi.Move("H8", "B2", false).IsSuccess.Should().BeTrue(); - // P1 Silver takes P2 Bishop - shogi.Move("C1", "B2", false).IsSuccess.Should().BeTrue(); - // P2 Pawn, arbtrary move - shogi.Move("A7", "A6", false).IsSuccess.Should().BeTrue(); - // P1 drop Bishop, place P2 in check - shogi.Move(WhichPiece.Bishop, "G7").IsSuccess.Should().BeTrue(); - shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); - shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["E5"].Should().BeNull(); - - // Act - P2 places a Bishop while in check. - var moveResult = shogi.Move(WhichPiece.Bishop, "E5"); - - // Assert - using var scope = new AssertionScope(); - 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); - } - - [Fact] - public void PreventInvalidDrop_Capture() - { - // Arrange - var shogi = MockShogiBoard(); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop capture P2 Bishop - shogi.Move("B2", "H8", false); - // P2 Pawn - shogi.Move("G6", "G5", false); - shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["I9"].Should().NotBeNull(); - shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - - // Act - P1 tries to place a piece where an opponent's piece resides. - var moveResult = shogi.Move(WhichPiece.Bishop, "I9"); - - // Assert - using var scope = new AssertionScope(); - 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); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - } - - [Fact] - public void Check() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - - // Act - P1 Bishop, check - shogi.Move("B2", "G7", false); - - // Assert - board.InCheck.Should().Be(WhichPlayer.Player2); - } - - [Fact] - public void Promote() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - - // Act - P1 moves across promote threshold. - shogi.Move("B2", "G7", true); - - // Assert - using (new AssertionScope()) - { - board["B2"].Should().BeNull(); - board["G7"].Should().NotBeNull(); - board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G7"]!.IsPromoted.Should().BeTrue(); - } - } - - [Fact] - public void Capture() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - var p1Bishop = board["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 - board["B2"].Should().BeNull(); - board["H8"].Should().Be(p1Bishop); - - board - .Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - } - - [Fact] - public void CheckMate() - { - // Arrange - var shogi = MockShogiBoard(); - var board = 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 - board.IsCheckmate.Should().BeTrue(); - board.InCheck.Should().Be(WhichPlayer.Player2); - } - - private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); } + + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["D1"]; + expectedPiece!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + + // Act - Move General illegally + var moveResult = shogi.Move("D1", "D5", false); + + // Assert + using (new AssertionScope()) + { + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); + board["D1"].Should().Be(expectedPiece); + board["D5"].Should().BeNull(); + board.Player1Hand.Should().BeEmpty(); + board.Player2Hand.Should().BeEmpty(); + } + } + + [Fact] + public void PreventInvalidMoves_Ownership() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A7"]; + expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); + + // Act - Move Player2 Pawn when it is Player1 turn. + var moveResult = shogi.Move("A7", "A6", false); + + // Assert + using (new AssertionScope()) + { + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A7"].Should().Be(expectedPiece); + board["A6"].Should().BeNull(); + } + } + + [Fact] + public void PreventInvalidMoves_MoveThroughAllies() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var lance = board["A1"]; + var pawn = board["A3"]; + lance!.Owner.Should().Be(pawn!.Owner); + + // Act - Move P1 Lance through P1 Pawn. + var moveResult = shogi.Move("A1", "A5", false); + + // Assert + using (new AssertionScope()) + { + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(lance); + board["A3"].Should().Be(pawn); + board["A5"].Should().BeNull(); + } + } + + [Fact] + public void PreventInvalidMoves_CaptureAlly() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var knight = board["B1"]; + var pawn = board["C3"]; + knight!.Owner.Should().Be(pawn!.Owner); + + // Act - P1 Knight tries to capture P1 Pawn. + var moveResult = shogi.Move("B1", "C3", false); + + // Arrange + using (new AssertionScope()) + { + 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(); + } + } + + [Fact] + public void PreventInvalidMoves_Check() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // 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 moveResult = shogi.Move("I9", "I8", false); + + // Assert + using (new AssertionScope()) + { + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); board.InCheck.Should().Be(WhichPlayer.Player2); + board["I9"].Should().Be(lance); + board["I8"].Should().BeNull(); + } + } + + [Fact] + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("I7", "I6", false); + // P1 Bishop takes P2 Pawn. + shogi.Move("B2", "G7", false); + // P2 Gold, block check from P1 Bishop. + shogi.Move("F9", "F8", false); + // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance + shogi.Move("G7", "H8", true); + // P2 Pawn again + shogi.Move("I6", "I5", false); + // P1 Bishop takes P2 Knight + shogi.Move("H8", "H9", false); + // P2 Pawn again + shogi.Move("I5", "I4", false); + // P1 Bishop takes P2 Lance + 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); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + board.WhoseTurn.Should().Be(WhichPlayer.Player1); + + // Act | Assert - Illegally placing Knight from the hand in farthest rank. + board["H9"].Should().BeNull(); + 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(); + 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(); + 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(); + moveResult = shogi.Move(WhichPiece.Pawn, "H9"); + moveResult.Should().NotBeNull(); + moveResult.IsSuccess.Should().BeFalse(); + 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 + } + + [Fact] + public void PreventInvalidDrop_Check() + { + // Arrange + var shogi = MockShogiBoard(); + // P1 Pawn + shogi.Move("C3", "C4", false).IsSuccess.Should().BeTrue(); + // P2 Pawn + shogi.Move("G7", "G6", false).IsSuccess.Should().BeTrue(); + // P1 Pawn, arbitrary move. + shogi.Move("A3", "A4", false).IsSuccess.Should().BeTrue(); + // P2 Bishop takes P1 Bishop + shogi.Move("H8", "B2", false).IsSuccess.Should().BeTrue(); + // P1 Silver takes P2 Bishop + shogi.Move("C1", "B2", false).IsSuccess.Should().BeTrue(); + // P2 Pawn, arbtrary move + shogi.Move("A7", "A6", false).IsSuccess.Should().BeTrue(); + // P1 drop Bishop, place P2 in check + shogi.Move(WhichPiece.Bishop, "G7").IsSuccess.Should().BeTrue(); + shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); + shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.BoardState["E5"].Should().BeNull(); + + // Act - P2 places a Bishop while in check. + var moveResult = shogi.Move(WhichPiece.Bishop, "E5"); + + // Assert + using var scope = new AssertionScope(); + 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); + } + + [Fact] + public void PreventInvalidDrop_Capture() + { + // Arrange + var shogi = MockShogiBoard(); + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + // P1 Bishop capture P2 Bishop + shogi.Move("B2", "H8", false); + // P2 Pawn + shogi.Move("G6", "G5", false); + shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + shogi.BoardState["I9"].Should().NotBeNull(); + shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); + shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + + // Act - P1 tries to place a piece where an opponent's piece resides. + var moveResult = shogi.Move(WhichPiece.Bishop, "I9"); + + // Assert + using var scope = new AssertionScope(); + 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); + shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + } + + [Fact] + public void Check() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + + // Act - P1 Bishop, check + shogi.Move("B2", "G7", false); + + // Assert + board.InCheck.Should().Be(WhichPlayer.Player2); + } + + [Fact] + public void Promote() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + // P1 Pawn + shogi.Move("C3", "C4", false); + // P2 Pawn + shogi.Move("G7", "G6", false); + + // Act - P1 moves across promote threshold. + shogi.Move("B2", "G7", true); + + // Assert + using (new AssertionScope()) + { + board["B2"].Should().BeNull(); + board["G7"].Should().NotBeNull(); + board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); + board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); + board["G7"]!.IsPromoted.Should().BeTrue(); + } + } + + [Fact] + public void Capture() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var p1Bishop = board["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 + board["B2"].Should().BeNull(); + board["H8"].Should().Be(p1Bishop); + + board + .Player1Hand + .Should() + .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + } + + [Fact] + public void CheckMate() + { + // Arrange + var shogi = MockShogiBoard(); + var board = 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 + board.IsCheckmate.Should().BeTrue(); + board.InCheck.Should().Be(WhichPlayer.Player2); + } + + private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); } From 357c3d9932052a66e31c535324ab8208866917b4 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Mon, 22 Dec 2025 11:44:06 -0600 Subject: [PATCH 02/15] Upgrade to .net 10 --- BoardRules/BoardRules.csproj | 2 +- Shogi.Api/Shogi.Api.csproj | 2 +- Shogi.Contracts/Shogi.Contracts.csproj | 2 +- Shogi.Domain/Shogi.Domain.csproj | 2 +- Shogi.UI/Shogi.UI.csproj | 2 +- Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj | 2 +- Tests/E2ETests/E2ETests.csproj | 2 +- Tests/UnitTests/UnitTests.csproj | 2 +- azure-pipelines.yml | 8 ++++---- global.json | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/BoardRules/BoardRules.csproj b/BoardRules/BoardRules.csproj index fa71b7a..b760144 100644 --- a/BoardRules/BoardRules.csproj +++ b/BoardRules/BoardRules.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/Shogi.Api/Shogi.Api.csproj b/Shogi.Api/Shogi.Api.csproj index 59214e2..499f7e4 100644 --- a/Shogi.Api/Shogi.Api.csproj +++ b/Shogi.Api/Shogi.Api.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 true 5 enable diff --git a/Shogi.Contracts/Shogi.Contracts.csproj b/Shogi.Contracts/Shogi.Contracts.csproj index 7b85818..8ca8e19 100644 --- a/Shogi.Contracts/Shogi.Contracts.csproj +++ b/Shogi.Contracts/Shogi.Contracts.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 true 5 enable diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj index deb29af..ca661e0 100644 --- a/Shogi.Domain/Shogi.Domain.csproj +++ b/Shogi.Domain/Shogi.Domain.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 disable enable diff --git a/Shogi.UI/Shogi.UI.csproj b/Shogi.UI/Shogi.UI.csproj index 2478355..515869a 100644 --- a/Shogi.UI/Shogi.UI.csproj +++ b/Shogi.UI/Shogi.UI.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj index b84ee1c..6df2195 100644 --- a/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj +++ b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/Tests/E2ETests/E2ETests.csproj b/Tests/E2ETests/E2ETests.csproj index d91738a..74fdf80 100644 --- a/Tests/E2ETests/E2ETests.csproj +++ b/Tests/E2ETests/E2ETests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 69bf354..639f4c4 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8764968..cbef4a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,10 +21,10 @@ steps: - task: NuGetToolInstaller@1 - task: UseDotNet@2 - inputs: - packageType: sdk - #useGlobalJson: true - version: 8.x +inputs: + packageType: sdk + #useGlobalJson: true + version: 10.x installationPath: $(Agent.ToolsDirectory)/dotnet #Install if not already present. - task: CmdLine@2 diff --git a/global.json b/global.json index 9e0754e..f72210c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "10.0.100", "rollForward": "latestFeature" } } \ No newline at end of file From dcbf8a3ac37b720c7eb4079ca2cd34b4800a1cdf Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 24 Dec 2025 16:43:51 -0600 Subject: [PATCH 03/15] convert to blazor server side render --- Shogi.Api/Extensions/ContractsExtensions.cs | 67 --- Shogi.Api/Program.cs | 100 ----- Shogi.Api/Properties/launchSettings.json | 24 -- Shogi.Api/Shogi.Api.csproj | 45 -- Shogi.Contracts/Shogi.Contracts.csproj | 17 - .../ShogiApiJsonSerializerSettings.cs | 11 - Shogi.Contracts/Types/Piece.cs | 9 - Shogi.Database/FirstTimeSetup.sql | 6 +- Shogi.Domain/Shogi.Domain.csproj | 20 - .../ValueObjects/Movement/Direction.cs | 15 - .../ValueObjects/Movement/MoveResult.cs | 6 - Shogi.Domain/ValueObjects/Pieces/Bishop.cs | 47 --- Shogi.Domain/ValueObjects/Pieces/Knight.cs | 32 -- Shogi.Domain/ValueObjects/Pieces/Lance.cs | 31 -- Shogi.Domain/ValueObjects/Pieces/Pawn.cs | 31 -- Shogi.Domain/ValueObjects/Pieces/Piece.cs | 99 ----- .../ValueObjects/Pieces/SilverGeneral.cs | 35 -- .../ValueObjects/Rules/StandardRules.cs | 166 -------- .../DomainExtensions.cs | 19 - .../YetToBeAssimilatedIntoDDD/Notation.cs | 33 -- .../YetToBeAssimilatedIntoDDD/ReadMe.md | 4 - Shogi.UI/.config/dotnet-tools.json | 12 - Shogi.UI/Identity/CookieMessageHandler.cs | 14 - Shogi.UI/Pages/Home/_Imports.razor | 1 - .../Pages/Play/GameBoard/EmptyGameBoard.razor | 6 - Shogi.UI/Program.cs | 64 --- Shogi.UI/Properties/Resources.Designer.cs | 63 --- Shogi.UI/Properties/Resources.resx | 101 ----- Shogi.UI/Properties/launchSettings.json | 15 - Shogi.UI/Properties/serviceDependencies.json | 8 - .../Properties/serviceDependencies.local.json | 8 - Shogi.UI/Shared/ShogiApi.cs | 79 ---- Shogi.UI/Shogi.UI.csproj | 63 --- Shogi.UI/wwwroot/appsettings.Development.json | 2 - Shogi.UI/wwwroot/appsettings.json | 15 - Shogi.UI/wwwroot/index.html | 33 -- Shogi.sln | 71 ++-- .../.config/dotnet-tools.json | 0 {Shogi.Api => Shogi}/ApiKeys.cs | 2 +- .../BackEnd}/Application/GameHub.cs | 2 +- .../BackEnd}/Application/GameHubContext.cs | 2 +- .../BackEnd}/Application/ShogiApplication.cs | 21 +- .../BackEnd}/Controllers/AccountController.cs | 4 +- .../BackEnd}/Controllers/Extentions.cs | 2 +- ...entityApiEndpointRouteBuilderExtensions.cs | 4 +- .../Controllers/SessionsController.cs | 35 +- .../BackEnd/Domains}/Aggregates/Session.cs | 5 +- .../Domains}/ValueObjects/BoardState.cs | 11 +- .../BackEnd/Domains}/ValueObjects/Enums.cs | 2 +- .../ValueObjects/Movement/Direction.cs | 17 + .../ValueObjects/Movement/Distance.cs | 4 +- .../Domains}/ValueObjects/Movement/Move.cs | 4 +- .../ValueObjects/Movement/MoveResult.cs | 5 + .../Domains}/ValueObjects/Movement/Path.cs | 7 +- .../Domains/ValueObjects/Pieces/Bishop.cs | 47 +++ .../ValueObjects/Pieces/GoldGeneral.cs | 5 +- .../Domains}/ValueObjects/Pieces/King.cs | 5 +- .../Domains/ValueObjects/Pieces/Knight.cs | 32 ++ .../Domains/ValueObjects/Pieces/Lance.cs | 31 ++ .../Domains/ValueObjects/Pieces/Pawn.cs | 31 ++ .../Domains/ValueObjects/Pieces/Piece.cs | 100 +++++ .../Domains}/ValueObjects/Pieces/Rook.cs | 5 +- .../ValueObjects/Pieces/SilverGeneral.cs | 35 ++ .../Domains}/ValueObjects/Rules/ShogiBoard.cs | 10 +- .../ValueObjects/Rules/StandardRules.cs | 166 ++++++++ .../DomainExtensions.cs | 19 + .../YetToBeAssimilatedIntoDDD/Notation.cs | 33 ++ .../BackEnd/Extensions/ContractsExtensions.cs | 68 +++ .../BackEnd}/Identity/ApplicationDbContext.cs | 2 +- .../BackEnd}/Identity/ShogiUser.cs | 2 +- .../20240816002834_InitialCreate.Designer.cs | 14 +- .../20240816002834_InitialCreate.cs | 2 +- .../ApplicationDbContextModelSnapshot.cs | 14 +- .../CouchModels/BoardStateDocument.cs | 0 .../CouchModels/CouchCreatedResult.cs | 0 .../Repositories/CouchModels/CouchDocument.cs | 0 .../CouchModels/CouchFindResult.cs | 0 .../CouchModels/CouchViewResult.cs | 0 .../BackEnd}/Repositories/CouchModels/Move.cs | 0 .../Repositories/CouchModels/Piece.cs | 0 .../CouchModels/SessionDocument.cs | 0 .../Repositories/CouchModels/UserDocument.cs | 0 .../CouchModels/WhichDocumentType.cs | 0 .../BackEnd}/Repositories/Dto/MoveDto.cs | 4 +- .../BackEnd}/Repositories/Dto/SessionDto.cs | 2 +- .../BackEnd}/Repositories/EmailSender.cs | 2 +- .../Repositories/GameboardRepository.cs | 0 .../BackEnd}/Repositories/QueryRepository.cs | 4 +- .../Repositories/SessionRepository.cs | 9 +- .../BackEnd}/Types/BoardState.cs | 5 +- .../BackEnd/Types}/MovePieceCommand.cs | 4 +- Shogi/BackEnd/Types/Piece.cs | 8 + .../BackEnd}/Types/Session.cs | 6 +- .../BackEnd}/Types/SessionMetadata.cs | 4 +- .../BackEnd}/Types/WhichPiece.cs | 2 +- .../BackEnd/Types/WhichPlayer.cs | 2 +- .../CookieAuthenticationStateProvider.cs | 2 +- .../FrontEnd/Client}/FormResult.cs | 2 +- .../FrontEnd/Client}/GameHubNode.cs | 23 +- .../FrontEnd/Client}/IAccountManagement.cs | 2 +- .../FrontEnd/Client}/LocalStorage.cs | 2 +- Shogi/FrontEnd/Client/ShogiService.cs | 111 +++++ .../FrontEnd/Client}/UserInfo.cs | 2 +- Shogi/FrontEnd/Components/App.razor | 20 + .../FrontEnd/Components}/IconButton.razor | 0 .../FrontEnd/Components}/IconButton.razor.css | 0 .../Components}/Icons/ChevronDownIcon.razor | 0 .../Components}/Icons/ChevronUpIcon.razor | 0 .../Components}/Icons/TrashCanIcon.razor | 0 .../Components}/Layout/MainLayout.razor | 2 +- .../Components}/Layout/MainLayout.razor.css | 0 .../FrontEnd/Components}/Layout/NavMenu.razor | 15 +- .../Components}/Layout/NavMenu.razor.css | 0 .../Components}/Pages/FancyErrorPage.razor | 0 .../Pages/FancyErrorPage.razor.css | 0 .../Components}/Pages/Home/HomePage.razor | 1 - .../Components}/Pages/Home/HomePage.razor.css | 0 .../Pages/Home/VisualAids/BishopMoves.razor | 0 .../Home/VisualAids/BoardSetupVisualAid.razor | 4 +- .../VisualAids/BoardSetupVisualAid.razor.css | 0 .../Pages/Home/VisualAids/DragonMoves.razor | 0 .../Home/VisualAids/GoldGeneralMoves.razor | 0 .../Pages/Home/VisualAids/HorseMoves.razor | 0 .../Pages/Home/VisualAids/KingMoves.razor | 0 .../Pages/Home/VisualAids/KnightMoves.razor | 0 .../Pages/Home/VisualAids/LanceMoves.razor | 0 .../Pages/Home/VisualAids/PawnMoves.razor | 0 .../Home/VisualAids/PieceMovesVisualAid.razor | 0 .../VisualAids/PieceMovesVisualAid.razor.css | 0 .../Home/VisualAids/PromotedKnightMoves.razor | 0 .../Home/VisualAids/PromotedLanceMoves.razor | 0 .../Home/VisualAids/PromotedPawnMoves.razor | 0 .../VisualAids/PromotedPieceVisualAid.razor | 0 .../PromotedPieceVisualAid.razor.css | 0 .../Home/VisualAids/PromotedSilverMoves.razor | 0 .../Pages/Home/VisualAids/RookMoves.razor | 0 .../Pages/Home/VisualAids/SilverMoves.razor | 0 .../Components/Pages/Home/_Imports.razor | 1 + .../Pages/Identity/ForgotPassword.razor | 0 .../Pages/Identity/ForgotPassword.razor.css | 0 .../Pages/Identity/LoginPage.razor | 0 .../Pages/Identity/LoginPage.razor.css | 0 .../Pages/Identity/LogoutPage.razor | 0 .../Pages/Identity/RegisterPage.razor | 0 .../Pages/Identity/RegisterPage.razor.css | 0 .../Pages/Play/GameBoard/EmptyGameBoard.razor | 4 + .../Pages/Play/GameBoard/GameBoard.razor | 8 +- .../GameBoard/GameBoardPresentation.razor | 3 +- .../GameBoard/GameboardPresentation.razor.css | 0 .../Pages/Play/GameBoard/OpponentName.razor | 3 +- .../Play/GameBoard/OpponentName.razor.css | 0 .../Pages/Play/GameBoard/PlayerName.razor | 3 +- .../Pages/Play/GameBoard/PlayerName.razor.css | 0 .../Play/GameBoard/SeatedGameBoard.razor | 22 +- .../Play/GameBoard/SeatedGameBoard.razor.css | 0 .../Play/GameBoard/SpectatorGameBoard.razor | 16 +- .../Components}/Pages/Play/GameBrowser.razor | 7 +- .../Pages/Play/GameBrowser.razor.css | 0 .../Pages/Play/GameBrowserEntry.razor | 16 +- .../Pages/Play/GameBrowserEntry.razor.css | 0 .../Components}/Pages/Play/GamePiece.razor | 4 +- .../Pages/Play/GamePiece.razor.css | 0 .../Pages/Play/Pieces/Bishop.razor | 0 .../Pages/Play/Pieces/ChallengingKing.razor | 0 .../Pages/Play/Pieces/GoldGeneral.razor | 0 .../Pages/Play/Pieces/Knight.razor | 0 .../Components}/Pages/Play/Pieces/Lance.razor | 0 .../Components}/Pages/Play/Pieces/Pawn.razor | 0 .../Pages/Play/Pieces/ReigningKing.razor | 0 .../Components}/Pages/Play/Pieces/Rook.razor | 0 .../Pages/Play/Pieces/SilverGeneral.razor | 0 .../Components}/Pages/Play/PlayPage.razor | 0 .../Components}/Pages/SearchPage.razor | 0 .../Components}/Pages/SearchPage.razor.css | 0 .../Components}/RotatingCogsSvg.razor | 0 .../Components}/RotatingCogsSvg.razor.css | 0 .../FrontEnd/Components/Routes.razor | 7 +- .../FrontEnd/Components}/Stretch.razor | 0 .../FrontEnd/Components}/Stretch.razor.css | 0 .../FrontEnd/Components}/TemporaryModal.razor | 0 .../Components}/TemporaryModal.razor.css | 0 .../FrontEnd/Components}/_Imports.razor | 18 +- Shogi/Program.cs | 117 ++++++ .../PublishProfiles/FolderProfile.pubxml | 0 .../local/secrets1.arm.json | 0 Shogi/Properties/launchSettings.json | 35 ++ .../Properties/serviceDependencies.json | 0 .../Properties/serviceDependencies.local.json | 0 {Shogi.Api => Shogi}/Readme.md | 0 Shogi/Shogi.csproj | 90 ++++ .../appsettings.Development.json | 0 {Shogi.Api => Shogi}/appsettings.json | 2 +- {Shogi.UI => Shogi}/wwwroot/css/app.css | 0 {Shogi.UI => Shogi}/wwwroot/css/themes.css | 0 {Shogi.UI => Shogi}/wwwroot/favicon.ico | Bin .../wwwroot/svgs/camera-reels.svg | 0 Tests/AcceptanceTests/ApiTests.cs | 388 +++++++++--------- .../Shogi.AcceptanceTests.csproj | 44 +- .../Shogi.AcceptanceTests.csproj.Backup.tmp | 33 ++ .../TestSetup/AatTestFixture.cs | 143 +++---- Tests/AcceptanceTests/Usings.cs | 3 +- .../appsettings.Development.json | 3 - Tests/AcceptanceTests/appsettings.json | 4 - Tests/E2ETests/E2ETests.csproj | 26 -- Tests/E2ETests/MicrosoftLoginTests.cs | 30 -- Tests/E2ETests/PlaywriteExample.cs | 31 -- Tests/E2ETests/Usings.cs | 1 - Tests/E2ETests/appsettings.json | 3 - Tests/UnitTests/Extensions.cs | 2 +- Tests/UnitTests/NotationShould.cs | 18 +- Tests/UnitTests/RookShould.cs | 168 ++++---- Tests/UnitTests/ShogiBoardStateShould.cs | 324 +++++++-------- Tests/UnitTests/ShogiShould.cs | 261 ++++++------ Tests/UnitTests/UnitTests.csproj | 17 +- azure-pipelines.yml | 4 +- 215 files changed, 1867 insertions(+), 2350 deletions(-) delete mode 100644 Shogi.Api/Extensions/ContractsExtensions.cs delete mode 100644 Shogi.Api/Program.cs delete mode 100644 Shogi.Api/Properties/launchSettings.json delete mode 100644 Shogi.Api/Shogi.Api.csproj delete mode 100644 Shogi.Contracts/Shogi.Contracts.csproj delete mode 100644 Shogi.Contracts/ShogiApiJsonSerializerSettings.cs delete mode 100644 Shogi.Contracts/Types/Piece.cs delete mode 100644 Shogi.Domain/Shogi.Domain.csproj delete mode 100644 Shogi.Domain/ValueObjects/Movement/Direction.cs delete mode 100644 Shogi.Domain/ValueObjects/Movement/MoveResult.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/Bishop.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/Knight.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/Lance.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/Pawn.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/Piece.cs delete mode 100644 Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs delete mode 100644 Shogi.Domain/ValueObjects/Rules/StandardRules.cs delete mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs delete mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs delete mode 100644 Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md delete mode 100644 Shogi.UI/.config/dotnet-tools.json delete mode 100644 Shogi.UI/Identity/CookieMessageHandler.cs delete mode 100644 Shogi.UI/Pages/Home/_Imports.razor delete mode 100644 Shogi.UI/Pages/Play/GameBoard/EmptyGameBoard.razor delete mode 100644 Shogi.UI/Program.cs delete mode 100644 Shogi.UI/Properties/Resources.Designer.cs delete mode 100644 Shogi.UI/Properties/Resources.resx delete mode 100644 Shogi.UI/Properties/launchSettings.json delete mode 100644 Shogi.UI/Properties/serviceDependencies.json delete mode 100644 Shogi.UI/Properties/serviceDependencies.local.json delete mode 100644 Shogi.UI/Shared/ShogiApi.cs delete mode 100644 Shogi.UI/Shogi.UI.csproj delete mode 100644 Shogi.UI/wwwroot/appsettings.Development.json delete mode 100644 Shogi.UI/wwwroot/appsettings.json delete mode 100644 Shogi.UI/wwwroot/index.html rename {Shogi.Api => Shogi}/.config/dotnet-tools.json (100%) rename {Shogi.Api => Shogi}/ApiKeys.cs (78%) rename {Shogi.Api => Shogi/BackEnd}/Application/GameHub.cs (91%) rename {Shogi.Api => Shogi/BackEnd}/Application/GameHubContext.cs (92%) rename {Shogi.Api => Shogi/BackEnd}/Application/ShogiApplication.cs (89%) rename {Shogi.Api => Shogi/BackEnd}/Controllers/AccountController.cs (96%) rename {Shogi.Api => Shogi/BackEnd}/Controllers/Extentions.cs (85%) rename {Shogi.Api => Shogi/BackEnd}/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs (99%) rename {Shogi.Api => Shogi/BackEnd}/Controllers/SessionsController.cs (70%) rename {Shogi.Domain => Shogi/BackEnd/Domains}/Aggregates/Session.cs (86%) rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/BoardState.cs (96%) rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Enums.cs (91%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Movement/Distance.cs (81%) rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Movement/Move.cs (86%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Movement/MoveResult.cs rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Movement/Path.cs (83%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Pieces/GoldGeneral.cs (81%) rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Pieces/King.cs (82%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Pieces/Rook.cs (90%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs rename {Shogi.Domain => Shogi/BackEnd/Domains}/ValueObjects/Rules/ShogiBoard.cs (97%) create mode 100644 Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs create mode 100644 Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/DomainExtensions.cs create mode 100644 Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs create mode 100644 Shogi/BackEnd/Extensions/ContractsExtensions.cs rename {Shogi.Api => Shogi/BackEnd}/Identity/ApplicationDbContext.cs (86%) rename {Shogi.Api => Shogi/BackEnd}/Identity/ShogiUser.cs (71%) rename {Shogi.Api => Shogi/BackEnd}/Migrations/20240816002834_InitialCreate.Designer.cs (96%) rename {Shogi.Api => Shogi/BackEnd}/Migrations/20240816002834_InitialCreate.cs (99%) rename {Shogi.Api => Shogi/BackEnd}/Migrations/ApplicationDbContextModelSnapshot.cs (96%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/BoardStateDocument.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/CouchCreatedResult.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/CouchDocument.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/CouchFindResult.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/CouchViewResult.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/Move.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/Piece.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/SessionDocument.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/UserDocument.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/CouchModels/WhichDocumentType.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/Dto/MoveDto.cs (68%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/Dto/SessionDto.cs (67%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/EmailSender.cs (97%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/GameboardRepository.cs (100%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/QueryRepository.cs (90%) rename {Shogi.Api => Shogi/BackEnd}/Repositories/SessionRepository.cs (90%) rename {Shogi.Contracts => Shogi/BackEnd}/Types/BoardState.cs (83%) rename {Shogi.Contracts/Api/Commands => Shogi/BackEnd/Types}/MovePieceCommand.cs (95%) create mode 100644 Shogi/BackEnd/Types/Piece.cs rename {Shogi.Contracts => Shogi/BackEnd}/Types/Session.cs (74%) rename {Shogi.Contracts => Shogi/BackEnd}/Types/SessionMetadata.cs (77%) rename {Shogi.Contracts => Shogi/BackEnd}/Types/WhichPiece.cs (74%) rename Shogi.Contracts/Types/WhichPerspective.cs => Shogi/BackEnd/Types/WhichPlayer.cs (57%) rename {Shogi.UI/Identity => Shogi/FrontEnd/Client}/CookieAuthenticationStateProvider.cs (99%) rename {Shogi.UI/Identity => Shogi/FrontEnd/Client}/FormResult.cs (89%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Client}/GameHubNode.cs (64%) rename {Shogi.UI/Identity => Shogi/FrontEnd/Client}/IAccountManagement.cs (96%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Client}/LocalStorage.cs (97%) create mode 100644 Shogi/FrontEnd/Client/ShogiService.cs rename {Shogi.UI/Identity => Shogi/FrontEnd/Client}/UserInfo.cs (93%) create mode 100644 Shogi/FrontEnd/Components/App.razor rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/IconButton.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/IconButton.razor.css (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/Icons/ChevronDownIcon.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/Icons/ChevronUpIcon.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/Icons/TrashCanIcon.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Layout/MainLayout.razor (56%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Layout/MainLayout.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Layout/NavMenu.razor (79%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Layout/NavMenu.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/FancyErrorPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/FancyErrorPage.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/HomePage.razor (98%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/HomePage.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/BishopMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/BoardSetupVisualAid.razor (98%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/BoardSetupVisualAid.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/DragonMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/GoldGeneralMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/HorseMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/KingMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/KnightMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/LanceMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PawnMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PieceMovesVisualAid.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PieceMovesVisualAid.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedKnightMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedLanceMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedPawnMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedPieceVisualAid.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedPieceVisualAid.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/PromotedSilverMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/RookMoves.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Home/VisualAids/SilverMoves.razor (100%) create mode 100644 Shogi/FrontEnd/Components/Pages/Home/_Imports.razor rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/ForgotPassword.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/ForgotPassword.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/LoginPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/LoginPage.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/LogoutPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/RegisterPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Identity/RegisterPage.razor.css (100%) create mode 100644 Shogi/FrontEnd/Components/Pages/Play/GameBoard/EmptyGameBoard.razor rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/GameBoard.razor (90%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/GameBoardPresentation.razor (99%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/GameboardPresentation.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/OpponentName.razor (90%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/OpponentName.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/PlayerName.razor (89%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/PlayerName.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/SeatedGameBoard.razor (86%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/SeatedGameBoard.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBoard/SpectatorGameBoard.razor (59%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBrowser.razor (83%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBrowser.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBrowserEntry.razor (79%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GameBrowserEntry.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GamePiece.razor (94%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/GamePiece.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/Bishop.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/ChallengingKing.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/GoldGeneral.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/Knight.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/Lance.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/Pawn.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/ReigningKing.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/Rook.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/Pieces/SilverGeneral.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/Play/PlayPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/SearchPage.razor (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/Pages/SearchPage.razor.css (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/RotatingCogsSvg.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/RotatingCogsSvg.razor.css (100%) rename Shogi.UI/App.razor => Shogi/FrontEnd/Components/Routes.razor (74%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/Stretch.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/Stretch.razor.css (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/TemporaryModal.razor (100%) rename {Shogi.UI/Shared => Shogi/FrontEnd/Components}/TemporaryModal.razor.css (100%) rename {Shogi.UI => Shogi/FrontEnd/Components}/_Imports.razor (50%) create mode 100644 Shogi/Program.cs rename {Shogi.Api => Shogi}/Properties/PublishProfiles/FolderProfile.pubxml (100%) rename {Shogi.Api => Shogi}/Properties/ServiceDependencies/local/secrets1.arm.json (100%) create mode 100644 Shogi/Properties/launchSettings.json rename {Shogi.Api => Shogi}/Properties/serviceDependencies.json (100%) rename {Shogi.Api => Shogi}/Properties/serviceDependencies.local.json (100%) rename {Shogi.Api => Shogi}/Readme.md (100%) create mode 100644 Shogi/Shogi.csproj rename {Shogi.Api => Shogi}/appsettings.Development.json (100%) rename {Shogi.Api => Shogi}/appsettings.json (90%) rename {Shogi.UI => Shogi}/wwwroot/css/app.css (100%) rename {Shogi.UI => Shogi}/wwwroot/css/themes.css (100%) rename {Shogi.UI => Shogi}/wwwroot/favicon.ico (100%) rename {Shogi.UI => Shogi}/wwwroot/svgs/camera-reels.svg (100%) create mode 100644 Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj.Backup.tmp delete mode 100644 Tests/AcceptanceTests/appsettings.Development.json delete mode 100644 Tests/AcceptanceTests/appsettings.json delete mode 100644 Tests/E2ETests/E2ETests.csproj delete mode 100644 Tests/E2ETests/MicrosoftLoginTests.cs delete mode 100644 Tests/E2ETests/PlaywriteExample.cs delete mode 100644 Tests/E2ETests/Usings.cs delete mode 100644 Tests/E2ETests/appsettings.json diff --git a/Shogi.Api/Extensions/ContractsExtensions.cs b/Shogi.Api/Extensions/ContractsExtensions.cs deleted file mode 100644 index 71321e0..0000000 --- a/Shogi.Api/Extensions/ContractsExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Shogi.Contracts.Types; -using System.Collections.ObjectModel; - -namespace Shogi.Api.Extensions; - -public static class ContractsExtensions -{ - public static WhichPlayer ToContract(this Domain.ValueObjects.WhichPlayer player) - { - return player switch - { - Domain.ValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1, - Domain.ValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2, - _ => throw new NotImplementedException(), - }; - } - - public static WhichPiece ToContract(this Domain.ValueObjects.WhichPiece piece) - { - return piece switch - { - Domain.ValueObjects.WhichPiece.King => WhichPiece.King, - Domain.ValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, - Domain.ValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, - Domain.ValueObjects.WhichPiece.Bishop => WhichPiece.Bishop, - Domain.ValueObjects.WhichPiece.Rook => WhichPiece.Rook, - Domain.ValueObjects.WhichPiece.Knight => WhichPiece.Knight, - Domain.ValueObjects.WhichPiece.Lance => WhichPiece.Lance, - Domain.ValueObjects.WhichPiece.Pawn => WhichPiece.Pawn, - _ => throw new NotImplementedException(), - }; - } - - public static Piece ToContract(this Domain.ValueObjects.Piece piece) => new() - { - IsPromoted = piece.IsPromoted, - Owner = piece.Owner.ToContract(), - WhichPiece = piece.WhichPiece.ToContract() - }; - - public static IReadOnlyList ToContract(this List pieces) - { - return pieces - .Select(ToContract) - .ToList() - .AsReadOnly(); - } - - public static Dictionary ToContract(this ReadOnlyDictionary boardState) => - boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract()); - - public static Domain.ValueObjects.WhichPiece ToDomain(this WhichPiece piece) - { - return piece switch - { - WhichPiece.King => Domain.ValueObjects.WhichPiece.King, - WhichPiece.GoldGeneral => Domain.ValueObjects.WhichPiece.GoldGeneral, - WhichPiece.SilverGeneral => Domain.ValueObjects.WhichPiece.SilverGeneral, - WhichPiece.Bishop => Domain.ValueObjects.WhichPiece.Bishop, - WhichPiece.Rook => Domain.ValueObjects.WhichPiece.Rook, - WhichPiece.Knight => Domain.ValueObjects.WhichPiece.Knight, - WhichPiece.Lance => Domain.ValueObjects.WhichPiece.Lance, - WhichPiece.Pawn => Domain.ValueObjects.WhichPiece.Pawn, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Api/Program.cs b/Shogi.Api/Program.cs deleted file mode 100644 index 673199a..0000000 --- a/Shogi.Api/Program.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.AspNetCore.Identity.UI.Services; -using Microsoft.AspNetCore.ResponseCompression; -using Microsoft.EntityFrameworkCore; -using Shogi.Api; -using Shogi.Api.Application; -using Shogi.Api.Controllers; -using Shogi.Api.Identity; -using Shogi.Api.Repositories; - -var builder = WebApplication.CreateBuilder(args); -var allowedOrigins = builder - .Configuration - .GetSection("Cors:AllowedOrigins") - .Get() ?? throw new InvalidOperationException("Configuration for allowed origins is missing."); - -builder.Services - .AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.WriteIndented = true; - }); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddHttpClient(); -builder.Services.Configure(builder.Configuration.GetSection("ApiKeys")); - -AddIdentity(builder, builder.Configuration); -builder.Services.AddSignalR(); -builder.Services.AddResponseCompression(opts => -{ - opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); -}); -var app = builder.Build(); - -app.MyMapIdentityApi(builder.Environment); - -if (app.Environment.IsDevelopment()) -{ - app.UseHttpsRedirection(); // Apache handles HTTPS in production. -} -else -{ - app.UseResponseCompression(); -} -app.UseSwagger(); -app.UseSwaggerUI(options => options.DocumentTitle = "Shogi.Api"); -app.UseAuthorization(); -app.Map("/", () => "OK"); -app.MapControllers(); -app.UseCors(policy => -{ - policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod().AllowCredentials(); -}); - -app.MapHub("/gamehub"); - -app.Run(); - -static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration) -{ - builder.Services - .AddAuthorizationBuilder() - .AddPolicy("Admin", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireAssertion(context => context.User?.Identity?.Name switch - { - "Hauth@live.com" => true, - "aat-account" => true, - _ => false - }); - }); - - builder.Services - .AddDbContext(options => - { - var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured."); - options.UseSqlServer(cs); - - // This is helpful to debug account issues without affecting the database. - //options.UseInMemoryDatabase("AppDb"); - }) - .AddIdentityApiEndpoints(options => - { - options.SignIn.RequireConfirmedEmail = true; - options.User.RequireUniqueEmail = true; - }) - .AddEntityFrameworkStores(); - - builder.Services.ConfigureApplicationCookie(options => - { - options.SlidingExpiration = true; - options.ExpireTimeSpan = TimeSpan.FromDays(3); - }); - -} \ No newline at end of file diff --git a/Shogi.Api/Properties/launchSettings.json b/Shogi.Api/Properties/launchSettings.json deleted file mode 100644 index 0851a5a..0000000 --- a/Shogi.Api/Properties/launchSettings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "profiles": { - "Kestrel": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/", - "AZURE_USERNAME": "Hauth@live.com" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50728/", - "sslPort": 44315 - } - } -} \ No newline at end of file diff --git a/Shogi.Api/Shogi.Api.csproj b/Shogi.Api/Shogi.Api.csproj deleted file mode 100644 index 499f7e4..0000000 --- a/Shogi.Api/Shogi.Api.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net10.0 - true - 5 - enable - False - False - enable - 973a1f5f-ef25-4f1c-a24d-b0fc7d016ab8 - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - diff --git a/Shogi.Contracts/Shogi.Contracts.csproj b/Shogi.Contracts/Shogi.Contracts.csproj deleted file mode 100644 index 8ca8e19..0000000 --- a/Shogi.Contracts/Shogi.Contracts.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net10.0 - true - 5 - enable - False - Shogi Service Models - Contains DTOs use for http requests to Shogi backend services. - - - - - - - diff --git a/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs b/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs deleted file mode 100644 index 89277f8..0000000 --- a/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json; - -namespace Shogi.Contracts; - -public class ShogiApiJsonSerializerSettings -{ - public readonly static JsonSerializerOptions SystemTextJsonSerializerOptions = new(JsonSerializerDefaults.Web) - { - WriteIndented = true, - }; -} diff --git a/Shogi.Contracts/Types/Piece.cs b/Shogi.Contracts/Types/Piece.cs deleted file mode 100644 index 3c7b335..0000000 --- a/Shogi.Contracts/Types/Piece.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Shogi.Contracts.Types -{ - public class Piece - { - public bool IsPromoted { get; set; } - public WhichPiece WhichPiece { get; set; } - public WhichPlayer Owner { get; set; } - } -} diff --git a/Shogi.Database/FirstTimeSetup.sql b/Shogi.Database/FirstTimeSetup.sql index 5dfe63b..b61353c 100644 --- a/Shogi.Database/FirstTimeSetup.sql +++ b/Shogi.Database/FirstTimeSetup.sql @@ -1,10 +1,10 @@ --- Create a user named Shogi.Api +-- Create a user named Shogi -- Create a role and grant execute permission to that role --CREATE ROLE db_executor --GRANT EXECUTE To db_executor --- Give Shogi.Api user permission to db_executor, db_datareader, db_datawriter +-- Give Shogi user permission to db_executor, db_datareader, db_datawriter /** @@ -13,5 +13,5 @@ * * 2. Setup Entity Framework because that's what the login system uses. * 2.a. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef -* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update +* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi as the target project: dotnet ef database update */ \ No newline at end of file diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj deleted file mode 100644 index ca661e0..0000000 --- a/Shogi.Domain/Shogi.Domain.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net10.0 - disable - enable - - - - - - - - - - - - - - diff --git a/Shogi.Domain/ValueObjects/Movement/Direction.cs b/Shogi.Domain/ValueObjects/Movement/Direction.cs deleted file mode 100644 index e2e71c8..0000000 --- a/Shogi.Domain/ValueObjects/Movement/Direction.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Shogi.Domain.ValueObjects; - -public static class Direction - { - public static readonly Vector2 Forward = new(0, 1); - public static readonly Vector2 Backward = new(0, -1); - public static readonly Vector2 Left = new(-1, 0); - public static readonly Vector2 Right = new(1, 0); - public static readonly Vector2 ForwardLeft = new(-1, 1); - public static readonly Vector2 ForwardRight = new(1, 1); - public static readonly Vector2 BackwardLeft = new(-1, -1); - public static readonly Vector2 BackwardRight = new(1, -1); - public static readonly Vector2 KnightLeft = new(-1, 2); - public static readonly Vector2 KnightRight = new(1, 2); - } diff --git a/Shogi.Domain/ValueObjects/Movement/MoveResult.cs b/Shogi.Domain/ValueObjects/Movement/MoveResult.cs deleted file mode 100644 index f66140f..0000000 --- a/Shogi.Domain/ValueObjects/Movement/MoveResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Shogi.Domain.ValueObjects -{ - public record MoveResult(bool IsSuccess, string Reason = "") - { - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/Bishop.cs b/Shogi.Domain/ValueObjects/Pieces/Bishop.cs deleted file mode 100644 index 4e721f4..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/Bishop.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.ValueObjects -{ - internal record class Bishop : Piece - { - private static readonly ReadOnlyCollection BishopPaths = new(new List(4) - { - new Path(Direction.ForwardLeft, Distance.MultiStep), - new Path(Direction.ForwardRight, Distance.MultiStep), - new Path(Direction.BackwardLeft, Distance.MultiStep), - new Path(Direction.BackwardRight, Distance.MultiStep) - }); - - public static readonly ReadOnlyCollection PromotedBishopPaths = new(new List(8) - { - new Path(Direction.Forward), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Backward), - new Path(Direction.ForwardLeft, Distance.MultiStep), - new Path(Direction.ForwardRight, Distance.MultiStep), - new Path(Direction.BackwardLeft, Distance.MultiStep), - new Path(Direction.BackwardRight, Distance.MultiStep) - }); - - public static readonly ReadOnlyCollection Player2Paths = - BishopPaths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public static readonly ReadOnlyCollection Player2PromotedPaths = - PromotedBishopPaths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Bishop(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Bishop, owner, isPromoted) - { - } - - public override IEnumerable MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths; - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/Knight.cs b/Shogi.Domain/ValueObjects/Pieces/Knight.cs deleted file mode 100644 index acccfb8..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/Knight.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.ValueObjects -{ - internal record class Knight : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(2) - { - new Path(Direction.KnightLeft), - new Path(Direction.KnightRight) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Knight(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Knight, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/Lance.cs b/Shogi.Domain/ValueObjects/Pieces/Lance.cs deleted file mode 100644 index aaf54e9..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/Lance.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.ValueObjects -{ - internal record class Lance : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(1) - { - new Path(Direction.Forward, Distance.MultiStep), - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Lance(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Lance, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/Pawn.cs b/Shogi.Domain/ValueObjects/Pieces/Pawn.cs deleted file mode 100644 index 7ce3e22..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/Pawn.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.ValueObjects -{ - internal record class Pawn : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(1) - { - new Path(Direction.Forward) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Pawn(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Pawn, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/Piece.cs b/Shogi.Domain/ValueObjects/Pieces/Piece.cs deleted file mode 100644 index d57e8ec..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/Piece.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Diagnostics; - -namespace Shogi.Domain.ValueObjects -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public abstract record class Piece - { - 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)}.") - }; - } - public abstract IEnumerable MoveSet { get; } - public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } - public bool IsPromoted { get; private set; } - protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = isPromoted; - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void Promote() => IsPromoted = CanPromote; - - /// - /// Prep the piece for capture by changing ownership and demoting. - /// - public void Capture(WhichPlayer newOwner) - { - Owner = newOwner; - IsPromoted = false; - } - - /// - /// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end. - /// Useful if you need to iterate a move-set. - /// - /// - /// - /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions. - public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) - { - var steps = new List(10); - - var path = GetNearestPath(MoveSet, start, end); - var position = start; - while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) - { - position += path.Step; - steps.Add(position); - - if (path.Distance == Distance.OneStep) break; - } - - if (position == end) - { - return steps; - } - - return []; - } - - private static Path GetNearestPath(IEnumerable paths, Vector2 start, Vector2 end) - { - if (!paths.DefaultIfEmpty().Any()) - { - throw new ArgumentException("No paths to get nearest path from."); - } - - var shortestPath = paths.First(); - foreach (var path in paths.Skip(1)) - { - var distance = Vector2.Distance(start + path.Step, end); - var shortestDistance = Vector2.Distance(start + shortestPath.Step, end); - if (distance < shortestDistance) - { - shortestPath = path; - } - } - return shortestPath; - } - } -} diff --git a/Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs b/Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs deleted file mode 100644 index 82c5519..0000000 --- a/Shogi.Domain/ValueObjects/Pieces/SilverGeneral.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shogi.Domain.ValueObjects.Movement; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.ValueObjects -{ - internal record class SilverGeneral : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(4) - { - new Path(Direction.Forward), - new Path(Direction.ForwardLeft), - new Path(Direction.ForwardRight), - new Path(Direction.BackwardLeft), - new Path(Direction.BackwardRight) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public SilverGeneral(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.SilverGeneral, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/ValueObjects/Rules/StandardRules.cs b/Shogi.Domain/ValueObjects/Rules/StandardRules.cs deleted file mode 100644 index 911520a..0000000 --- a/Shogi.Domain/ValueObjects/Rules/StandardRules.cs +++ /dev/null @@ -1,166 +0,0 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD; -using BoardTile = System.Collections.Generic.KeyValuePair; - -namespace Shogi.Domain.ValueObjects -{ - internal class StandardRules - { - private readonly BoardState boardState; - - internal StandardRules(BoardState board) - { - boardState = board; - } - - /// - /// Determines if the last move put the player who moved in check. - /// - /// - /// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance. - /// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check. - /// - internal bool DidPlayerPutThemselfInCheck() - { - if (boardState.PreviousMove.From == null) - { - // You can't place yourself in check by placing a piece from your hand. - return false; - } - - var previousMovedPiece = boardState[boardState.PreviousMove.To]; - if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}."); - var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; - - - var isDiscoverCheck = false; - // Get line equation from king through the now-unoccupied location. - var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From.Value); - var slope = Math.Abs(direction.Y / direction.X); - var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From.Value, Vector2.Normalize(direction)); - var threat = boardState.QueryFirstPieceInPath(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)) - { - isDiscoverCheck = threat.WhichPiece switch - { - WhichPiece.Lance => !threat.IsPromoted, - WhichPiece.Rook => true, - _ => false - }; - } - else if (slope == 1) - { - isDiscoverCheck = threat.WhichPiece switch - { - WhichPiece.Bishop => true, - _ => false - }; - } - else if (slope == 0) - { - isDiscoverCheck = threat.WhichPiece switch - { - WhichPiece.Rook => true, - _ => false - }; - } - return isDiscoverCheck; - } - - internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); - - internal bool IsOpposingKingThreatenedByPosition(Vector2 position) - { - 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.QueryFirstPieceInPath(path); - if (!path.Any() || threatenedPiece == null) return false; - - return threatenedPiece.WhichPiece == WhichPiece.King; - } - - internal bool IsOpponentInCheckMate() - { - // Assume checkmate, then try to disprove. - if (!boardState.InCheck.HasValue) return false; - // Get all pieces from opponent who threaten the king in question. - var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent); - var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1 - ? boardState.Player1KingPosition - : boardState.Player2KingPosition; - var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList(); - if (threats.Count == 1) - { - /* 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 = threats.Single(); - var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition); - var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn); - foreach (var threatBlockingPosition in pathFromThreatToKing) - { - var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat - .Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition)) - .ToList(); - - if (tilesThatDoBlockThreat.Any()) - { - return false; // Cannot be check-mate if a piece can intercept the threat. - } - } - } - else - { - /* - * If no ability to block the check, maybe the king can evade check by moving. - */ - - foreach (var maybeSafePosition in GetPossiblePositionsForKing(boardState.WhoseTurn)) - { - threats = tilesOccupiedByOpponent - .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) - .ToList(); - if (!threats.Any()) - { - return false; - } - } - - } - - return true; - } - - private List GetPossiblePositionsForKing(WhichPlayer whichPlayer) - { - var kingPosition = whichPlayer == WhichPlayer.Player1 - ? boardState.Player1KingPosition - : boardState.Player2KingPosition; - - var paths = boardState[kingPosition]!.MoveSet; - return paths - .Select(path => path.Step + kingPosition) - // Because the king could be on the edge of the board, where some of its paths do not make sense. - .Where(newPosition => newPosition.IsInsideBoardBoundary()) - // Where tile at position is empty, meaning the king could move there. - .Where(newPosition => boardState[newPosition] == null) - .ToList(); - } - - private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) - { - var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); - return path - .SkipLast(1) - .All(position => boardState[Notation.ToBoardNotation(position)] == null); - } - } -} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs deleted file mode 100644 index dd5c72a..0000000 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/DomainExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Shogi.Domain.ValueObjects; - -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD -{ - internal static class DomainExtensions - { - public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; - - public static bool IsBetween(this float self, float min, float max) - { - return self >= min && self <= max; - } - - public static bool IsInsideBoardBoundary(this Vector2 self) - { - return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8); - } - } -} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs deleted file mode 100644 index f75600e..0000000 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Notation.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD -{ - public static class Notation - { - private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; - private static readonly char A = 'A'; - - public static string ToBoardNotation(Vector2 vector) - { - return ToBoardNotation((int)vector.X, (int)vector.Y); - } - - public static string ToBoardNotation(int x, int y) - { - var file = (char)(x + A); - var rank = y + 1; - return $"{file}{rank}"; - } - public static Vector2 FromBoardNotation(string notation) - { - if (Regex.IsMatch(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); - } - throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); - } - } -} diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md b/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md deleted file mode 100644 index 51bfb0f..0000000 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/ReadMe.md +++ /dev/null @@ -1,4 +0,0 @@ -# Shogi.Domain - -### TODO: -* There are enough non-DDD classes around navigating the standard 9x9 Shogi board that probably a new value object or entity is merited. See classes within Pathing folder as well as Notation.cs. \ No newline at end of file diff --git a/Shogi.UI/.config/dotnet-tools.json b/Shogi.UI/.config/dotnet-tools.json deleted file mode 100644 index 4b64fb7..0000000 --- a/Shogi.UI/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "microsoft.dotnet-msidentity": { - "version": "1.0.5", - "commands": [ - "dotnet-msidentity" - ] - } - } -} \ No newline at end of file diff --git a/Shogi.UI/Identity/CookieMessageHandler.cs b/Shogi.UI/Identity/CookieMessageHandler.cs deleted file mode 100644 index 3d78081..0000000 --- a/Shogi.UI/Identity/CookieMessageHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Http; - -namespace Shogi.UI.Identity; - -public class CookieCredentialsMessageHandler : DelegatingHandler -{ - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); - request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]); - - return base.SendAsync(request, cancellationToken); - } -} diff --git a/Shogi.UI/Pages/Home/_Imports.razor b/Shogi.UI/Pages/Home/_Imports.razor deleted file mode 100644 index 0a497b0..0000000 --- a/Shogi.UI/Pages/Home/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@using Shogi.UI.Pages.Home.VisualAids \ No newline at end of file diff --git a/Shogi.UI/Pages/Play/GameBoard/EmptyGameBoard.razor b/Shogi.UI/Pages/Play/GameBoard/EmptyGameBoard.razor deleted file mode 100644 index 0473868..0000000 --- a/Shogi.UI/Pages/Play/GameBoard/EmptyGameBoard.razor +++ /dev/null @@ -1,6 +0,0 @@ -@using Contracts.Types; - - - -@code { -} diff --git a/Shogi.UI/Program.cs b/Shogi.UI/Program.cs deleted file mode 100644 index 9dd0486..0000000 --- a/Shogi.UI/Program.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.AspNetCore.ResponseCompression; -using Shogi.UI; -using Shogi.UI.Identity; -using Shogi.UI.Shared; -using System.Text.Json; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); -builder.Logging.AddConfiguration( - builder.Configuration.GetSection("Logging")); -ConfigureDependencies(builder.Services, builder.Configuration); - -builder.Services.AddResponseCompression(options => -{ - options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); -}); - -await builder.Build().RunAsync(); - -static void ConfigureDependencies(IServiceCollection services, IConfiguration configuration) -{ - /** - * Why two HTTP clients? - * See https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?source=recommendations&view=aspnetcore-6.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client - */ - var baseUrl = configuration["ShogiApiUrl"]; - if (string.IsNullOrWhiteSpace(baseUrl)) - { - throw new InvalidOperationException("ShogiApiUrl configuration is missing."); - } - - var shogiApiUrl = new Uri(baseUrl, UriKind.Absolute); - - services - .AddTransient() - .AddTransient(); - - // Identity - services - .AddAuthorizationCore(options => options.AddPolicy("Admin", policy => policy.RequireUserName("Hauth@live.com"))) - .AddScoped() - .AddScoped(sp => (IAccountManagement)sp.GetRequiredService()) - .AddHttpClient("Auth", client => client.BaseAddress = shogiApiUrl) // "Auth" is the name expected by the auth library. - .AddHttpMessageHandler(); - - // Network clients - services - .AddHttpClient(client => client.BaseAddress = shogiApiUrl) - .AddHttpMessageHandler(); - services - .AddSingleton(); - - - var serializerOptions = new JsonSerializerOptions - { - WriteIndented = true - }; - services.AddSingleton((sp) => serializerOptions); -} \ No newline at end of file diff --git a/Shogi.UI/Properties/Resources.Designer.cs b/Shogi.UI/Properties/Resources.Designer.cs deleted file mode 100644 index 59e260a..0000000 --- a/Shogi.UI/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Shogi.UI.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Shogi.UI.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/Shogi.UI/Properties/Resources.resx b/Shogi.UI/Properties/Resources.resx deleted file mode 100644 index 4fdb1b6..0000000 --- a/Shogi.UI/Properties/Resources.resx +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Shogi.UI/Properties/launchSettings.json b/Shogi.UI/Properties/launchSettings.json deleted file mode 100644 index 8c7658c..0000000 --- a/Shogi.UI/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "profiles": { - "Shogi.UI": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:3000", - "nativeDebugging": true - } - } -} \ No newline at end of file diff --git a/Shogi.UI/Properties/serviceDependencies.json b/Shogi.UI/Properties/serviceDependencies.json deleted file mode 100644 index 44cc45e..0000000 --- a/Shogi.UI/Properties/serviceDependencies.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "identityapp1": { - "type": "identityapp", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/Shogi.UI/Properties/serviceDependencies.local.json b/Shogi.UI/Properties/serviceDependencies.local.json deleted file mode 100644 index 3c85224..0000000 --- a/Shogi.UI/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "identityapp1": { - "type": "identityapp.default", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/Shogi.UI/Shared/ShogiApi.cs b/Shogi.UI/Shared/ShogiApi.cs deleted file mode 100644 index 34cb91e..0000000 --- a/Shogi.UI/Shared/ShogiApi.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Shogi.Contracts.Api.Commands; -using Shogi.Contracts.Types; -using System.Net; -using System.Net.Http.Json; -using System.Reflection.Metadata.Ecma335; -using System.Text.Json; - -namespace Shogi.UI.Shared; - -public class ShogiApi(HttpClient httpClient) -{ - private readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - public async Task Register(string email, string password) - { - var response = await httpClient.PostAsJsonAsync(Relative("register"), new { email, password }); - response.EnsureSuccessStatusCode(); - } - - public async Task LoginEventArgs(string email, string password) - { - var response = await httpClient.PostAsJsonAsync("login?useCookies=true", new { email, password }); - response.EnsureSuccessStatusCode(); - } - - public async Task Logout() - { - var response = await httpClient.PutAsync(Relative("User/GuestLogout"), null); - response.EnsureSuccessStatusCode(); - } - - public async Task GetSession(string name) - { - var response = await httpClient.GetAsync(Relative($"Sessions/{name}")); - if (response.IsSuccessStatusCode) - { - return (await response.Content.ReadFromJsonAsync(this.serializerOptions)); - } - return null; - } - - public async Task GetAllSessionsMetadata() - { - var response = await httpClient.GetAsync(Relative("Sessions")); - if (response.IsSuccessStatusCode) - { - return (await response.Content.ReadFromJsonAsync(this.serializerOptions))!; - } - return []; - } - - /// - /// Returns false if the move was not accepted by the server. - /// - public async Task Move(Guid sessionName, MovePieceCommand command) - { - var response = await httpClient.PatchAsync(Relative($"Sessions/{sessionName}/Move"), JsonContent.Create(command)); - return response.IsSuccessStatusCode; - } - - public async Task PostSession() - { - var response = await httpClient.PostAsync(Relative("Sessions"), null); - var sessionId = response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : null; - return sessionId; - } - - public Task PatchJoinGame(string name) - { - return httpClient.PatchAsync(Relative($"Sessions/{name}/Join"), null); - } - - public Task DeleteSession(Guid sessionId) - { - return httpClient.DeleteAsync(Relative($"Sessions/{sessionId}")); - } - - private static Uri Relative(string path) => new(path, UriKind.Relative); -} diff --git a/Shogi.UI/Shogi.UI.csproj b/Shogi.UI/Shogi.UI.csproj deleted file mode 100644 index 515869a..0000000 --- a/Shogi.UI/Shogi.UI.csproj +++ /dev/null @@ -1,63 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - diff --git a/Shogi.UI/wwwroot/appsettings.Development.json b/Shogi.UI/wwwroot/appsettings.Development.json deleted file mode 100644 index 2c63c08..0000000 --- a/Shogi.UI/wwwroot/appsettings.Development.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/Shogi.UI/wwwroot/appsettings.json b/Shogi.UI/wwwroot/appsettings.json deleted file mode 100644 index 12680a0..0000000 --- a/Shogi.UI/wwwroot/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Error", - "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information", - "System.Net.Http.HttpClient": "Error", - "Microsoft.AspNetCore.SignalR": "Debug", - "Microsoft.AspNetCore.Http.Connections": "Debug" - } - }, - "ShogiApiUrl": "https://localhost:5001", - "ShogiApiUrl2": "https://api.lucaserver.space/Shogi.Api/" -} \ No newline at end of file diff --git a/Shogi.UI/wwwroot/index.html b/Shogi.UI/wwwroot/index.html deleted file mode 100644 index 65953da..0000000 --- a/Shogi.UI/wwwroot/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - Shogi.UI - - - - - - - - -
- -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - - diff --git a/Shogi.sln b/Shogi.sln index 349696f..d1948fd 100644 --- a/Shogi.sln +++ b/Shogi.sln @@ -1,18 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11312.210 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A968C8E6-47B7-4F72-A27A-AC9B643FD320}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.AcceptanceTests", "Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj", "{30F4E3DB-027F-4885-BE06-884167C1C6CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Contracts", "Shogi.Contracts\Shogi.Contracts.csproj", "{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E69DE334-29A7-46AE-9647-54DC0187CD8D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -22,65 +12,52 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.UI", "Shogi.UI\Shogi.UI.csproj", "{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}" -EndProject Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Shogi.Database", "Shogi.Database\Shogi.Database.sqlproj", "{9B115B71-088F-41EF-858F-C7B155271A9F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Api", "Shogi.Api\Shogi.Api.csproj", "{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETests", "Tests\E2ETests\E2ETests.csproj", "{401120C3-45D6-4A23-8D87-C2BED29F4950}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\BoardRules.csproj", "{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.AcceptanceTests", "Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj", "{768F37ED-FB62-A57F-BCFA-91F26B4F794F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{9D1DD2CD-7B04-4472-4377-027563F356CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi", "Shogi\Shogi.csproj", "{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.Build.0 = Release|Any CPU - {4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30F4E3DB-027F-4885-BE06-884167C1C6CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30F4E3DB-027F-4885-BE06-884167C1C6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30F4E3DB-027F-4885-BE06-884167C1C6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Release|Any CPU.Build.0 = Release|Any CPU - {12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Release|Any CPU.Build.0 = Release|Any CPU {9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {9B115B71-088F-41EF-858F-C7B155271A9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B115B71-088F-41EF-858F-C7B155271A9F}.Release|Any CPU.Deploy.0 = Release|Any CPU - {62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Release|Any CPU.Build.0 = Release|Any CPU - {401120C3-45D6-4A23-8D87-C2BED29F4950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {401120C3-45D6-4A23-8D87-C2BED29F4950}.Debug|Any CPU.Build.0 = Debug|Any CPU - {401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.ActiveCfg = Release|Any CPU - {401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.Build.0 = Release|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU + {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Release|Any CPU.Build.0 = Release|Any CPU + {9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D1DD2CD-7B04-4472-4377-027563F356CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D1DD2CD-7B04-4472-4377-027563F356CA}.Release|Any CPU.Build.0 = Release|Any CPU + {E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320} - {30F4E3DB-027F-4885-BE06-884167C1C6CF} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320} - {401120C3-45D6-4A23-8D87-C2BED29F4950} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320} + {768F37ED-FB62-A57F-BCFA-91F26B4F794F} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B} + {9D1DD2CD-7B04-4472-4377-027563F356CA} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} diff --git a/Shogi.Api/.config/dotnet-tools.json b/Shogi/.config/dotnet-tools.json similarity index 100% rename from Shogi.Api/.config/dotnet-tools.json rename to Shogi/.config/dotnet-tools.json diff --git a/Shogi.Api/ApiKeys.cs b/Shogi/ApiKeys.cs similarity index 78% rename from Shogi.Api/ApiKeys.cs rename to Shogi/ApiKeys.cs index f19377d..3c7891e 100644 --- a/Shogi.Api/ApiKeys.cs +++ b/Shogi/ApiKeys.cs @@ -1,4 +1,4 @@ -namespace Shogi.Api; +namespace Shogi; public class ApiKeys { diff --git a/Shogi.Api/Application/GameHub.cs b/Shogi/BackEnd/Application/GameHub.cs similarity index 91% rename from Shogi.Api/Application/GameHub.cs rename to Shogi/BackEnd/Application/GameHub.cs index 4eb4b2d..f03e7fc 100644 --- a/Shogi.Api/Application/GameHub.cs +++ b/Shogi/BackEnd/Application/GameHub.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.SignalR; -namespace Shogi.Api.Application; +namespace Shogi.BackEnd.Application; /// /// Used to receive signals from connected clients. diff --git a/Shogi.Api/Application/GameHubContext.cs b/Shogi/BackEnd/Application/GameHubContext.cs similarity index 92% rename from Shogi.Api/Application/GameHubContext.cs rename to Shogi/BackEnd/Application/GameHubContext.cs index cbcead0..218d416 100644 --- a/Shogi.Api/Application/GameHubContext.cs +++ b/Shogi/BackEnd/Application/GameHubContext.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.SignalR; -namespace Shogi.Api.Application; +namespace Shogi.BackEnd.Application; /// /// Used to send signals to connected clients. diff --git a/Shogi.Api/Application/ShogiApplication.cs b/Shogi/BackEnd/Application/ShogiApplication.cs similarity index 89% rename from Shogi.Api/Application/ShogiApplication.cs rename to Shogi/BackEnd/Application/ShogiApplication.cs index 792e18a..5aa1f61 100644 --- a/Shogi.Api/Application/ShogiApplication.cs +++ b/Shogi/BackEnd/Application/ShogiApplication.cs @@ -1,15 +1,14 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Shogi.Api.Controllers; -using Shogi.Api.Extensions; -using Shogi.Api.Identity; -using Shogi.Api.Repositories; -using Shogi.Api.Repositories.Dto; -using Shogi.Contracts.Api.Commands; -using Shogi.Domain.Aggregates; +using Shogi.BackEnd.Controllers; +using Shogi.BackEnd.Extensions; +using Shogi.BackEnd.Identity; +using Shogi.BackEnd.Repositories; +using Shogi.BackEnd.Repositories.Dto; +using Shogi.BackEnd.Domains.Aggregates; using System.Data.SqlClient; -namespace Shogi.Api.Application; +namespace Shogi.BackEnd.Application; public class ShogiApplication( QueryRepository queryRepository, @@ -72,10 +71,10 @@ public class ShogiApplication( return session; } - public async Task MovePiece(string playerId, string sessionId, MovePieceCommand command) + public async Task MovePiece(string playerId, string sessionId, Types.MovePieceCommand command) { var session = await this.ReadSession(sessionId); - if (session == null) + if (session is null) { return new NotFoundResult(); } @@ -105,7 +104,7 @@ public class ShogiApplication( public async Task JoinSession(string sessionId, string player2Id) { var session = await this.ReadSession(sessionId); - if (session == null) return new NotFoundResult(); + if (session is null) return new NotFoundResult(); if (string.IsNullOrEmpty(session.Player2)) { diff --git a/Shogi.Api/Controllers/AccountController.cs b/Shogi/BackEnd/Controllers/AccountController.cs similarity index 96% rename from Shogi.Api/Controllers/AccountController.cs rename to Shogi/BackEnd/Controllers/AccountController.cs index a744f09..dda50b7 100644 --- a/Shogi.Api/Controllers/AccountController.cs +++ b/Shogi/BackEnd/Controllers/AccountController.cs @@ -1,10 +1,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Shogi.Api.Identity; +using Shogi.BackEnd.Identity; using System.Security.Claims; -namespace Shogi.Api.Controllers; +namespace Shogi.BackEnd.Controllers; [Authorize] [Route("[controller]")] diff --git a/Shogi.Api/Controllers/Extentions.cs b/Shogi/BackEnd/Controllers/Extentions.cs similarity index 85% rename from Shogi.Api/Controllers/Extentions.cs rename to Shogi/BackEnd/Controllers/Extentions.cs index eb828cb..4d6668e 100644 --- a/Shogi.Api/Controllers/Extentions.cs +++ b/Shogi/BackEnd/Controllers/Extentions.cs @@ -1,6 +1,6 @@ using System.Security.Claims; -namespace Shogi.Api.Controllers; +namespace Shogi.BackEnd.Controllers; public static class Extentions { diff --git a/Shogi.Api/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs b/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs similarity index 99% rename from Shogi.Api/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs rename to Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs index 85fe6e8..d64764f 100644 --- a/Shogi.Api/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs +++ b/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs @@ -15,7 +15,7 @@ using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; -namespace Shogi.Api.Controllers; +namespace Shogi.BackEnd.Controllers; /// /// Provides extension methods for to add identity endpoints. @@ -407,7 +407,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions routeValues.Add("changedEmail", email); } - HostString? host = environment.IsDevelopment() ? null : new HostString("api.lucaserver.space/Shogi.Api"); + HostString? host = environment.IsDevelopment() ? null : new HostString("api.lucaserver.space/Shogi"); var confirmEmailUrl = linkGenerator.GetUriByName(context, confirmEmailEndpointName, routeValues, host: host) ?? throw new NotSupportedException($"Could not find endpoint named '{confirmEmailEndpointName}'."); diff --git a/Shogi.Api/Controllers/SessionsController.cs b/Shogi/BackEnd/Controllers/SessionsController.cs similarity index 70% rename from Shogi.Api/Controllers/SessionsController.cs rename to Shogi/BackEnd/Controllers/SessionsController.cs index 95b3a2b..f6695f6 100644 --- a/Shogi.Api/Controllers/SessionsController.cs +++ b/Shogi/BackEnd/Controllers/SessionsController.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Shogi.Api.Application; -using Shogi.Api.Extensions; -using Shogi.Api.Repositories; -using Shogi.Contracts.Api.Commands; -using Shogi.Contracts.Types; +using Shogi.BackEnd.Application; +using Shogi.BackEnd.Extensions; +using Shogi.BackEnd.Repositories; +using Shogi.BackEnd.Types; -namespace Shogi.Api.Controllers; +namespace Shogi.BackEnd.Controllers; [Authorize] [ApiController] @@ -57,25 +56,25 @@ public class SessionsController( [AllowAnonymous] public async Task> GetSession(Guid sessionId) { - var session = await application.ReadSession(sessionId.ToString()); - if (session == null) return this.NotFound(); + var domainSession = await application.ReadSession(sessionId.ToString()); + if (domainSession is null) return this.NotFound(); return new Session { BoardState = new BoardState { - Board = session.Board.BoardState.State.ToContract(), - Player1Hand = session.Board.BoardState.Player1Hand.ToContract(), - Player2Hand = session.Board.BoardState.Player2Hand.ToContract(), - PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(), - WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(), - Victor = session.Board.BoardState.IsCheckmate - ? session.Board.BoardState.InCheck == Domain.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1 + Board = domainSession.Board.BoardState.State.ToContract(), + Player1Hand = domainSession.Board.BoardState.Player1Hand.ToContract(), + Player2Hand = domainSession.Board.BoardState.Player2Hand.ToContract(), + PlayerInCheck = domainSession.Board.BoardState.InCheck?.ToContract(), + WhoseTurn = domainSession.Board.BoardState.WhoseTurn.ToContract(), + Victor = domainSession.Board.BoardState.IsCheckmate + ? domainSession.Board.BoardState.InCheck == Domains.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1 : null }, - Player1 = application.GetUsername(session.Player1), - Player2 = application.GetUsername(session.Player2), - SessionId = session.Id + Player1 = application.GetUsername(domainSession.Player1), + Player2 = application.GetUsername(domainSession.Player2), + SessionId = domainSession.Id }; } diff --git a/Shogi.Domain/Aggregates/Session.cs b/Shogi/BackEnd/Domains/Aggregates/Session.cs similarity index 86% rename from Shogi.Domain/Aggregates/Session.cs rename to Shogi/BackEnd/Domains/Aggregates/Session.cs index 4c76fcf..4cc1cb7 100644 --- a/Shogi.Domain/Aggregates/Session.cs +++ b/Shogi/BackEnd/Domains/Aggregates/Session.cs @@ -1,6 +1,7 @@ -using Shogi.Domain.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects.Rules; -namespace Shogi.Domain.Aggregates; +namespace Shogi.BackEnd.Domains.Aggregates; public class Session(Guid id, string player1Name) { diff --git a/Shogi.Domain/ValueObjects/BoardState.cs b/Shogi/BackEnd/Domains/ValueObjects/BoardState.cs similarity index 96% rename from Shogi.Domain/ValueObjects/BoardState.cs rename to Shogi/BackEnd/Domains/ValueObjects/BoardState.cs index 03a51a6..ff0b982 100644 --- a/Shogi.Domain/ValueObjects/BoardState.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/BoardState.cs @@ -1,8 +1,11 @@ -using System.Collections.ObjectModel; -using Shogi.Domain.YetToBeAssimilatedIntoDDD; -using BoardTile = System.Collections.Generic.KeyValuePair; +using System.Collections.ObjectModel; +using System.Numerics; +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Pieces; +using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; +using BoardTile = System.Collections.Generic.KeyValuePair; -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects; public class BoardState { diff --git a/Shogi.Domain/ValueObjects/Enums.cs b/Shogi/BackEnd/Domains/ValueObjects/Enums.cs similarity index 91% rename from Shogi.Domain/ValueObjects/Enums.cs rename to Shogi/BackEnd/Domains/ValueObjects/Enums.cs index e259d0c..a7781d0 100644 --- a/Shogi.Domain/ValueObjects/Enums.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Enums.cs @@ -1,4 +1,4 @@ -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects; [Flags] internal enum InCheckResult diff --git a/Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs b/Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs new file mode 100644 index 0000000..f1a3427 --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs @@ -0,0 +1,17 @@ +using System.Numerics; + +namespace Shogi.BackEnd.Domains.ValueObjects.Movement; + +public static class Direction +{ + public static readonly Vector2 Forward = new(0, 1); + public static readonly Vector2 Backward = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 ForwardLeft = new(-1, 1); + public static readonly Vector2 ForwardRight = new(1, 1); + public static readonly Vector2 BackwardLeft = new(-1, -1); + public static readonly Vector2 BackwardRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); +} diff --git a/Shogi.Domain/ValueObjects/Movement/Distance.cs b/Shogi/BackEnd/Domains/ValueObjects/Movement/Distance.cs similarity index 81% rename from Shogi.Domain/ValueObjects/Movement/Distance.cs rename to Shogi/BackEnd/Domains/ValueObjects/Movement/Distance.cs index dfa7362..1ad6fdf 100644 --- a/Shogi.Domain/ValueObjects/Movement/Distance.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Movement/Distance.cs @@ -1,4 +1,4 @@ -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects.Movement; public enum Distance { @@ -10,4 +10,4 @@ public enum Distance /// Signifies that a piece can move multiple tiles/positions in a single move. /// MultiStep -} \ No newline at end of file +} diff --git a/Shogi.Domain/ValueObjects/Movement/Move.cs b/Shogi/BackEnd/Domains/ValueObjects/Movement/Move.cs similarity index 86% rename from Shogi.Domain/ValueObjects/Movement/Move.cs rename to Shogi/BackEnd/Domains/ValueObjects/Movement/Move.cs index a9d7e45..b5cd595 100644 --- a/Shogi.Domain/ValueObjects/Movement/Move.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Movement/Move.cs @@ -1,4 +1,6 @@ -namespace Shogi.Domain.ValueObjects; +using System.Numerics; + +namespace Shogi.BackEnd.Domains.ValueObjects.Movement; /// /// Represents a single piece being moved by a player from to . diff --git a/Shogi/BackEnd/Domains/ValueObjects/Movement/MoveResult.cs b/Shogi/BackEnd/Domains/ValueObjects/Movement/MoveResult.cs new file mode 100644 index 0000000..06362dc --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Movement/MoveResult.cs @@ -0,0 +1,5 @@ +namespace Shogi.BackEnd.Domains.ValueObjects.Movement; + +public record MoveResult(bool IsSuccess, string Reason = "") +{ +} diff --git a/Shogi.Domain/ValueObjects/Movement/Path.cs b/Shogi/BackEnd/Domains/ValueObjects/Movement/Path.cs similarity index 83% rename from Shogi.Domain/ValueObjects/Movement/Path.cs rename to Shogi/BackEnd/Domains/ValueObjects/Movement/Path.cs index 14ee1a5..65868e7 100644 --- a/Shogi.Domain/ValueObjects/Movement/Path.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Movement/Path.cs @@ -1,6 +1,7 @@ -using System.Diagnostics; +using System.Diagnostics; +using System.Numerics; -namespace Shogi.Domain.ValueObjects.Movement; +namespace Shogi.BackEnd.Domains.ValueObjects.Movement; [DebuggerDisplay("{Step} - {Distance}")] public record Path @@ -21,4 +22,4 @@ public record Path } public Path Invert() => new(Vector2.Negate(Step), Distance); -} \ No newline at end of file +} diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs new file mode 100644 index 0000000..3751e4d --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs @@ -0,0 +1,47 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +internal record class Bishop : Piece +{ + private static readonly ReadOnlyCollection BishopPaths = new(new List(4) + { + new Path(Direction.ForwardLeft, Distance.MultiStep), + new Path(Direction.ForwardRight, Distance.MultiStep), + new Path(Direction.BackwardLeft, Distance.MultiStep), + new Path(Direction.BackwardRight, Distance.MultiStep) + }); + + public static readonly ReadOnlyCollection PromotedBishopPaths = new(new List(8) + { + new Path(Direction.Forward), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Backward), + new Path(Direction.ForwardLeft, Distance.MultiStep), + new Path(Direction.ForwardRight, Distance.MultiStep), + new Path(Direction.BackwardLeft, Distance.MultiStep), + new Path(Direction.BackwardRight, Distance.MultiStep) + }); + + public static readonly ReadOnlyCollection Player2Paths = + BishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public static readonly ReadOnlyCollection Player2PromotedPaths = + PromotedBishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Bishop(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Bishop, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths; +} diff --git a/Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/GoldGeneral.cs similarity index 81% rename from Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs rename to Shogi/BackEnd/Domains/ValueObjects/Pieces/GoldGeneral.cs index a45a0b2..c6f35fe 100644 --- a/Shogi.Domain/ValueObjects/Pieces/GoldGeneral.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/GoldGeneral.cs @@ -1,7 +1,8 @@ -using Shogi.Domain.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Movement; using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; internal record class GoldGeneral : Piece { diff --git a/Shogi.Domain/ValueObjects/Pieces/King.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/King.cs similarity index 82% rename from Shogi.Domain/ValueObjects/Pieces/King.cs rename to Shogi/BackEnd/Domains/ValueObjects/Pieces/King.cs index 6f66cf5..e0e40c2 100644 --- a/Shogi.Domain/ValueObjects/Pieces/King.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/King.cs @@ -1,7 +1,8 @@ -using Shogi.Domain.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Movement; using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; internal record class King : Piece { diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs new file mode 100644 index 0000000..f0c35a9 --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs @@ -0,0 +1,32 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +internal record class Knight : Piece +{ + public static readonly ReadOnlyCollection Player1Paths = new(new List(2) + { + new Path(Direction.KnightLeft), + new Path(Direction.KnightRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Knight(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Knight, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; +} diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs new file mode 100644 index 0000000..3d1be3d --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs @@ -0,0 +1,31 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +internal record class Lance : Piece +{ + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Forward, Distance.MultiStep), + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Lance(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Lance, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; +} diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs new file mode 100644 index 0000000..5f9de3a --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs @@ -0,0 +1,31 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +internal record class Pawn : Piece +{ + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Forward) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Pawn(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Pawn, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; +} diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs new file mode 100644 index 0000000..c18daf3 --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs @@ -0,0 +1,100 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Diagnostics; +using System.Numerics; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +[DebuggerDisplay("{WhichPiece} {Owner}")] +public abstract record class Piece +{ + 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)}.") + }; + } + public abstract IEnumerable MoveSet { get; } + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = isPromoted; + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void Promote() => IsPromoted = CanPromote; + + /// + /// Prep the piece for capture by changing ownership and demoting. + /// + public void Capture(WhichPlayer newOwner) + { + Owner = newOwner; + IsPromoted = false; + } + + /// + /// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end. + /// Useful if you need to iterate a move-set. + /// + /// + /// + /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions. + public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) + { + var steps = new List(10); + + var path = GetNearestPath(MoveSet, start, end); + var position = start; + while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) + { + position += path.Step; + steps.Add(position); + + if (path.Distance == Distance.OneStep) break; + } + + if (position == end) + { + return steps; + } + + return []; + } + + private static Path GetNearestPath(IEnumerable paths, Vector2 start, Vector2 end) + { + if (!paths.DefaultIfEmpty().Any()) + { + throw new ArgumentException("No paths to get nearest path from."); + } + + var shortestPath = paths.First(); + foreach (var path in paths.Skip(1)) + { + var distance = Vector2.Distance(start + path.Step, end); + var shortestDistance = Vector2.Distance(start + shortestPath.Step, end); + if (distance < shortestDistance) + { + shortestPath = path; + } + } + return shortestPath; + } +} diff --git a/Shogi.Domain/ValueObjects/Pieces/Rook.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Rook.cs similarity index 90% rename from Shogi.Domain/ValueObjects/Pieces/Rook.cs rename to Shogi/BackEnd/Domains/ValueObjects/Pieces/Rook.cs index 5624600..5a65f12 100644 --- a/Shogi.Domain/ValueObjects/Pieces/Rook.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/Rook.cs @@ -1,7 +1,8 @@ -using Shogi.Domain.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Movement; using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; -namespace Shogi.Domain.ValueObjects; +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; public record class Rook : Piece { diff --git a/Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs b/Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs new file mode 100644 index 0000000..9d23a03 --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs @@ -0,0 +1,35 @@ +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using System.Collections.ObjectModel; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Pieces; + +internal record class SilverGeneral : Piece +{ + public static readonly ReadOnlyCollection Player1Paths = new(new List(4) + { + new Path(Direction.Forward), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), + new Path(Direction.BackwardLeft), + new Path(Direction.BackwardRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public SilverGeneral(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.SilverGeneral, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; +} diff --git a/Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs b/Shogi/BackEnd/Domains/ValueObjects/Rules/ShogiBoard.cs similarity index 97% rename from Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs rename to Shogi/BackEnd/Domains/ValueObjects/Rules/ShogiBoard.cs index b139566..0d6e944 100644 --- a/Shogi.Domain/ValueObjects/Rules/ShogiBoard.cs +++ b/Shogi/BackEnd/Domains/ValueObjects/Rules/ShogiBoard.cs @@ -1,6 +1,10 @@ -using Shogi.Domain.ValueObjects.Movement; -using Shogi.Domain.YetToBeAssimilatedIntoDDD; -namespace Shogi.Domain.ValueObjects; +using System.Numerics; +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Pieces; +using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; +using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path; + +namespace Shogi.BackEnd.Domains.ValueObjects.Rules; /// /// Facilitates Shogi board state transitions, cognisant of Shogi rules. diff --git a/Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs b/Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs new file mode 100644 index 0000000..fc47ceb --- /dev/null +++ b/Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs @@ -0,0 +1,166 @@ +using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; +using System.Numerics; +using BoardTile = System.Collections.Generic.KeyValuePair; + +namespace Shogi.BackEnd.Domains.ValueObjects.Rules; + +internal class StandardRules +{ + private readonly BoardState boardState; + + internal StandardRules(BoardState board) + { + boardState = board; + } + + /// + /// Determines if the last move put the player who moved in check. + /// + /// + /// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance. + /// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check. + /// + internal bool DidPlayerPutThemselfInCheck() + { + if (boardState.PreviousMove.From == null) + { + // You can't place yourself in check by placing a piece from your hand. + return false; + } + + var previousMovedPiece = boardState[boardState.PreviousMove.To]; + if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}."); + var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition; + + + var isDiscoverCheck = false; + // Get line equation from king through the now-unoccupied location. + var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From.Value); + var slope = Math.Abs(direction.Y / direction.X); + var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From.Value, Vector2.Normalize(direction)); + var threat = boardState.QueryFirstPieceInPath(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)) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Lance => !threat.IsPromoted, + WhichPiece.Rook => true, + _ => false + }; + } + else if (slope == 1) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Bishop => true, + _ => false + }; + } + else if (slope == 0) + { + isDiscoverCheck = threat.WhichPiece switch + { + WhichPiece.Rook => true, + _ => false + }; + } + return isDiscoverCheck; + } + + internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To); + + internal bool IsOpposingKingThreatenedByPosition(Vector2 position) + { + 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.QueryFirstPieceInPath(path); + if (!path.Any() || threatenedPiece == null) return false; + + return threatenedPiece.WhichPiece == WhichPiece.King; + } + + internal bool IsOpponentInCheckMate() + { + // Assume checkmate, then try to disprove. + if (!boardState.InCheck.HasValue) return false; + // Get all pieces from opponent who threaten the king in question. + var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent); + var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1 + ? boardState.Player1KingPosition + : boardState.Player2KingPosition; + var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList(); + if (threats.Count == 1) + { + /* 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 = threats.Single(); + var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition); + var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn); + foreach (var threatBlockingPosition in pathFromThreatToKing) + { + var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat + .Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition)) + .ToList(); + + if (tilesThatDoBlockThreat.Any()) + { + return false; // Cannot be check-mate if a piece can intercept the threat. + } + } + } + else + { + /* + * If no ability to block the check, maybe the king can evade check by moving. + */ + + foreach (var maybeSafePosition in GetPossiblePositionsForKing(boardState.WhoseTurn)) + { + threats = tilesOccupiedByOpponent + .Where(tile => PieceHasLineOfSight(tile, maybeSafePosition)) + .ToList(); + if (!threats.Any()) + { + return false; + } + } + + } + + return true; + } + + private List GetPossiblePositionsForKing(WhichPlayer whichPlayer) + { + var kingPosition = whichPlayer == WhichPlayer.Player1 + ? boardState.Player1KingPosition + : boardState.Player2KingPosition; + + var paths = boardState[kingPosition]!.MoveSet; + return paths + .Select(path => path.Step + kingPosition) + // Because the king could be on the edge of the board, where some of its paths do not make sense. + .Where(newPosition => newPosition.IsInsideBoardBoundary()) + // Where tile at position is empty, meaning the king could move there. + .Where(newPosition => boardState[newPosition] == null) + .ToList(); + } + + private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget) + { + var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget); + return path + .SkipLast(1) + .All(position => boardState[Notation.ToBoardNotation(position)] == null); + } +} diff --git a/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/DomainExtensions.cs b/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/DomainExtensions.cs new file mode 100644 index 0000000..5860191 --- /dev/null +++ b/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/DomainExtensions.cs @@ -0,0 +1,19 @@ +using Shogi.BackEnd.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects.Pieces; + +namespace Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; + +internal static class DomainExtensions +{ + public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; + + public static bool IsBetween(this float self, float min, float max) + { + return self >= min && self <= max; + } + + public static bool IsInsideBoardBoundary(this System.Numerics.Vector2 self) + { + return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8); + } +} diff --git a/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs b/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs new file mode 100644 index 0000000..3fee9cd --- /dev/null +++ b/Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs @@ -0,0 +1,33 @@ +using System.Numerics; +using System.Text.RegularExpressions; + +namespace Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; + +public static class Notation +{ + private static readonly string BoardNotationRegex = @"(?[A-I])(?[1-9])"; + private static readonly char A = 'A'; + + public static string ToBoardNotation(Vector2 vector) + { + return ToBoardNotation((int)vector.X, (int)vector.Y); + } + + public static string ToBoardNotation(int x, int y) + { + var file = (char)(x + A); + var rank = y + 1; + return $"{file}{rank}"; + } + public static Vector2 FromBoardNotation(string notation) + { + if (Regex.IsMatch(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); + } + throw new ArgumentException($"Board notation not recognized. Notation given: {notation}"); + } +} diff --git a/Shogi/BackEnd/Extensions/ContractsExtensions.cs b/Shogi/BackEnd/Extensions/ContractsExtensions.cs new file mode 100644 index 0000000..fffba31 --- /dev/null +++ b/Shogi/BackEnd/Extensions/ContractsExtensions.cs @@ -0,0 +1,68 @@ +using Shogi.BackEnd.Types; +using System.Collections.ObjectModel; +using DomainValueObjects = Shogi.BackEnd.Domains.ValueObjects; + +namespace Shogi.BackEnd.Extensions; + +public static class ContractsExtensions +{ + public static WhichPlayer ToContract(this DomainValueObjects.WhichPlayer player) + { + return player switch + { + DomainValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1, + DomainValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2, + _ => throw new NotImplementedException(), + }; + } + + public static WhichPiece ToContract(this DomainValueObjects.WhichPiece piece) + { + return piece switch + { + DomainValueObjects.WhichPiece.King => WhichPiece.King, + DomainValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + DomainValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + DomainValueObjects.WhichPiece.Bishop => WhichPiece.Bishop, + DomainValueObjects.WhichPiece.Rook => WhichPiece.Rook, + DomainValueObjects.WhichPiece.Knight => WhichPiece.Knight, + DomainValueObjects.WhichPiece.Lance => WhichPiece.Lance, + DomainValueObjects.WhichPiece.Pawn => WhichPiece.Pawn, + _ => throw new NotImplementedException(), + }; + } + + public static Piece ToContract(this DomainValueObjects.Pieces.Piece piece) => new() + { + IsPromoted = piece.IsPromoted, + Owner = piece.Owner.ToContract(), + WhichPiece = piece.WhichPiece.ToContract() + }; + + public static IReadOnlyList ToContract(this List pieces) + { + return pieces + .Select(ToContract) + .ToList() + .AsReadOnly(); + } + + public static Dictionary ToContract(this ReadOnlyDictionary boardState) => + boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract()); + + public static DomainValueObjects.WhichPiece ToDomain(this WhichPiece piece) + { + return piece switch + { + WhichPiece.King => DomainValueObjects.WhichPiece.King, + WhichPiece.GoldGeneral => DomainValueObjects.WhichPiece.GoldGeneral, + WhichPiece.SilverGeneral => DomainValueObjects.WhichPiece.SilverGeneral, + WhichPiece.Bishop => DomainValueObjects.WhichPiece.Bishop, + WhichPiece.Rook => DomainValueObjects.WhichPiece.Rook, + WhichPiece.Knight => DomainValueObjects.WhichPiece.Knight, + WhichPiece.Lance => DomainValueObjects.WhichPiece.Lance, + WhichPiece.Pawn => DomainValueObjects.WhichPiece.Pawn, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Api/Identity/ApplicationDbContext.cs b/Shogi/BackEnd/Identity/ApplicationDbContext.cs similarity index 86% rename from Shogi.Api/Identity/ApplicationDbContext.cs rename to Shogi/BackEnd/Identity/ApplicationDbContext.cs index 13be2bb..c5dcd86 100644 --- a/Shogi.Api/Identity/ApplicationDbContext.cs +++ b/Shogi/BackEnd/Identity/ApplicationDbContext.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace Shogi.Api.Identity; +namespace Shogi.BackEnd.Identity; public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) { diff --git a/Shogi.Api/Identity/ShogiUser.cs b/Shogi/BackEnd/Identity/ShogiUser.cs similarity index 71% rename from Shogi.Api/Identity/ShogiUser.cs rename to Shogi/BackEnd/Identity/ShogiUser.cs index 22eb31d..4bbe2da 100644 --- a/Shogi.Api/Identity/ShogiUser.cs +++ b/Shogi/BackEnd/Identity/ShogiUser.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Shogi.Api.Identity; +namespace Shogi.BackEnd.Identity; public class ShogiUser : IdentityUser { diff --git a/Shogi.Api/Migrations/20240816002834_InitialCreate.Designer.cs b/Shogi/BackEnd/Migrations/20240816002834_InitialCreate.Designer.cs similarity index 96% rename from Shogi.Api/Migrations/20240816002834_InitialCreate.Designer.cs rename to Shogi/BackEnd/Migrations/20240816002834_InitialCreate.Designer.cs index 24c2203..cf87bba 100644 --- a/Shogi.Api/Migrations/20240816002834_InitialCreate.Designer.cs +++ b/Shogi/BackEnd/Migrations/20240816002834_InitialCreate.Designer.cs @@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Shogi.Api.Identity; +using Shogi.BackEnd.Identity; #nullable disable -namespace Shogi.Api.Migrations +namespace Shogi.BackEnd.Migrations { [DbContext(typeof(ApplicationDbContext))] [Migration("20240816002834_InitialCreate")] @@ -158,7 +158,7 @@ namespace Shogi.Api.Migrations b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Shogi.Api.Models.User", b => + modelBuilder.Entity("Shogi.Models.User", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -234,7 +234,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -243,7 +243,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -258,7 +258,7 @@ namespace Shogi.Api.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -267,7 +267,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/Shogi.Api/Migrations/20240816002834_InitialCreate.cs b/Shogi/BackEnd/Migrations/20240816002834_InitialCreate.cs similarity index 99% rename from Shogi.Api/Migrations/20240816002834_InitialCreate.cs rename to Shogi/BackEnd/Migrations/20240816002834_InitialCreate.cs index 9ddf303..e405349 100644 --- a/Shogi.Api/Migrations/20240816002834_InitialCreate.cs +++ b/Shogi/BackEnd/Migrations/20240816002834_InitialCreate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Shogi.Api.Migrations +namespace Shogi.BackEnd.Migrations { /// public partial class InitialCreate : Migration diff --git a/Shogi.Api/Migrations/ApplicationDbContextModelSnapshot.cs b/Shogi/BackEnd/Migrations/ApplicationDbContextModelSnapshot.cs similarity index 96% rename from Shogi.Api/Migrations/ApplicationDbContextModelSnapshot.cs rename to Shogi/BackEnd/Migrations/ApplicationDbContextModelSnapshot.cs index bc2412f..8ae44b3 100644 --- a/Shogi.Api/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Shogi/BackEnd/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Shogi.Api.Identity; +using Shogi.BackEnd.Identity; #nullable disable -namespace Shogi.Api.Migrations +namespace Shogi.BackEnd.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -155,7 +155,7 @@ namespace Shogi.Api.Migrations b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Shogi.Api.Models.User", b => + modelBuilder.Entity("Shogi.Models.User", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -231,7 +231,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -240,7 +240,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -255,7 +255,7 @@ namespace Shogi.Api.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -264,7 +264,7 @@ namespace Shogi.Api.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Shogi.Api.Models.User", null) + b.HasOne("Shogi.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/Shogi.Api/Repositories/CouchModels/BoardStateDocument.cs b/Shogi/BackEnd/Repositories/CouchModels/BoardStateDocument.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/BoardStateDocument.cs rename to Shogi/BackEnd/Repositories/CouchModels/BoardStateDocument.cs diff --git a/Shogi.Api/Repositories/CouchModels/CouchCreatedResult.cs b/Shogi/BackEnd/Repositories/CouchModels/CouchCreatedResult.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/CouchCreatedResult.cs rename to Shogi/BackEnd/Repositories/CouchModels/CouchCreatedResult.cs diff --git a/Shogi.Api/Repositories/CouchModels/CouchDocument.cs b/Shogi/BackEnd/Repositories/CouchModels/CouchDocument.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/CouchDocument.cs rename to Shogi/BackEnd/Repositories/CouchModels/CouchDocument.cs diff --git a/Shogi.Api/Repositories/CouchModels/CouchFindResult.cs b/Shogi/BackEnd/Repositories/CouchModels/CouchFindResult.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/CouchFindResult.cs rename to Shogi/BackEnd/Repositories/CouchModels/CouchFindResult.cs diff --git a/Shogi.Api/Repositories/CouchModels/CouchViewResult.cs b/Shogi/BackEnd/Repositories/CouchModels/CouchViewResult.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/CouchViewResult.cs rename to Shogi/BackEnd/Repositories/CouchModels/CouchViewResult.cs diff --git a/Shogi.Api/Repositories/CouchModels/Move.cs b/Shogi/BackEnd/Repositories/CouchModels/Move.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/Move.cs rename to Shogi/BackEnd/Repositories/CouchModels/Move.cs diff --git a/Shogi.Api/Repositories/CouchModels/Piece.cs b/Shogi/BackEnd/Repositories/CouchModels/Piece.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/Piece.cs rename to Shogi/BackEnd/Repositories/CouchModels/Piece.cs diff --git a/Shogi.Api/Repositories/CouchModels/SessionDocument.cs b/Shogi/BackEnd/Repositories/CouchModels/SessionDocument.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/SessionDocument.cs rename to Shogi/BackEnd/Repositories/CouchModels/SessionDocument.cs diff --git a/Shogi.Api/Repositories/CouchModels/UserDocument.cs b/Shogi/BackEnd/Repositories/CouchModels/UserDocument.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/UserDocument.cs rename to Shogi/BackEnd/Repositories/CouchModels/UserDocument.cs diff --git a/Shogi.Api/Repositories/CouchModels/WhichDocumentType.cs b/Shogi/BackEnd/Repositories/CouchModels/WhichDocumentType.cs similarity index 100% rename from Shogi.Api/Repositories/CouchModels/WhichDocumentType.cs rename to Shogi/BackEnd/Repositories/CouchModels/WhichDocumentType.cs diff --git a/Shogi.Api/Repositories/Dto/MoveDto.cs b/Shogi/BackEnd/Repositories/Dto/MoveDto.cs similarity index 68% rename from Shogi.Api/Repositories/Dto/MoveDto.cs rename to Shogi/BackEnd/Repositories/Dto/MoveDto.cs index 2c6b086..02809e5 100644 --- a/Shogi.Api/Repositories/Dto/MoveDto.cs +++ b/Shogi/BackEnd/Repositories/Dto/MoveDto.cs @@ -1,6 +1,6 @@ -using Shogi.Domain.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects; -namespace Shogi.Api.Repositories.Dto; +namespace Shogi.BackEnd.Repositories.Dto; /// /// Useful with Dapper to read from database. diff --git a/Shogi.Api/Repositories/Dto/SessionDto.cs b/Shogi/BackEnd/Repositories/Dto/SessionDto.cs similarity index 67% rename from Shogi.Api/Repositories/Dto/SessionDto.cs rename to Shogi/BackEnd/Repositories/Dto/SessionDto.cs index bec4145..406e7d2 100644 --- a/Shogi.Api/Repositories/Dto/SessionDto.cs +++ b/Shogi/BackEnd/Repositories/Dto/SessionDto.cs @@ -1,4 +1,4 @@ -namespace Shogi.Api.Repositories.Dto; +namespace Shogi.BackEnd.Repositories.Dto; public readonly record struct SessionDto(string Id, string Player1Id, string Player2Id) { diff --git a/Shogi.Api/Repositories/EmailSender.cs b/Shogi/BackEnd/Repositories/EmailSender.cs similarity index 97% rename from Shogi.Api/Repositories/EmailSender.cs rename to Shogi/BackEnd/Repositories/EmailSender.cs index 84c1e69..4e14364 100644 --- a/Shogi.Api/Repositories/EmailSender.cs +++ b/Shogi/BackEnd/Repositories/EmailSender.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; using System.Net.Http.Headers; using System.Text.Json; -namespace Shogi.Api.Repositories; +namespace Shogi.BackEnd.Repositories; // https://app-smtp.brevo.com/real-time diff --git a/Shogi.Api/Repositories/GameboardRepository.cs b/Shogi/BackEnd/Repositories/GameboardRepository.cs similarity index 100% rename from Shogi.Api/Repositories/GameboardRepository.cs rename to Shogi/BackEnd/Repositories/GameboardRepository.cs diff --git a/Shogi.Api/Repositories/QueryRepository.cs b/Shogi/BackEnd/Repositories/QueryRepository.cs similarity index 90% rename from Shogi.Api/Repositories/QueryRepository.cs rename to Shogi/BackEnd/Repositories/QueryRepository.cs index 6b489fb..c8a163f 100644 --- a/Shogi.Api/Repositories/QueryRepository.cs +++ b/Shogi/BackEnd/Repositories/QueryRepository.cs @@ -1,9 +1,9 @@ using Dapper; -using Shogi.Api.Repositories.Dto; +using Shogi.BackEnd.Repositories.Dto; using System.Data; using System.Data.SqlClient; -namespace Shogi.Api.Repositories; +namespace Shogi.BackEnd.Repositories; public class QueryRepository(IConfiguration configuration) { diff --git a/Shogi.Api/Repositories/SessionRepository.cs b/Shogi/BackEnd/Repositories/SessionRepository.cs similarity index 90% rename from Shogi.Api/Repositories/SessionRepository.cs rename to Shogi/BackEnd/Repositories/SessionRepository.cs index 7ccc6e5..65c8d11 100644 --- a/Shogi.Api/Repositories/SessionRepository.cs +++ b/Shogi/BackEnd/Repositories/SessionRepository.cs @@ -1,11 +1,10 @@ using Dapper; -using Shogi.Api.Repositories.Dto; -using Shogi.Contracts.Api.Commands; -using Shogi.Domain.Aggregates; +using Shogi.BackEnd.Repositories.Dto; +using Shogi.BackEnd.Domains.Aggregates; using System.Data; using System.Data.SqlClient; -namespace Shogi.Api.Repositories; +namespace Shogi.BackEnd.Repositories; public class SessionRepository(IConfiguration configuration) { @@ -53,7 +52,7 @@ public class SessionRepository(IConfiguration configuration) return new(sessionDtos.First(), moveDtos); } - public async Task CreateMove(string sessionId, MovePieceCommand command) + public async Task CreateMove(string sessionId, Types.MovePieceCommand command) { using var connection = new SqlConnection(this.connectionString); await connection.ExecuteAsync( diff --git a/Shogi.Contracts/Types/BoardState.cs b/Shogi/BackEnd/Types/BoardState.cs similarity index 83% rename from Shogi.Contracts/Types/BoardState.cs rename to Shogi/BackEnd/Types/BoardState.cs index ef9b987..ef834db 100644 --- a/Shogi.Contracts/Types/BoardState.cs +++ b/Shogi/BackEnd/Types/BoardState.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Shogi.Contracts.Types; +namespace Shogi.BackEnd.Types; public class BoardState { diff --git a/Shogi.Contracts/Api/Commands/MovePieceCommand.cs b/Shogi/BackEnd/Types/MovePieceCommand.cs similarity index 95% rename from Shogi.Contracts/Api/Commands/MovePieceCommand.cs rename to Shogi/BackEnd/Types/MovePieceCommand.cs index 741fd69..a8ab1b2 100644 --- a/Shogi.Contracts/Api/Commands/MovePieceCommand.cs +++ b/Shogi/BackEnd/Types/MovePieceCommand.cs @@ -1,9 +1,7 @@ -using Shogi.Contracts.Types; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; -namespace Shogi.Contracts.Api.Commands; +namespace Shogi.BackEnd.Types; public partial class MovePieceCommand : IValidatableObject { diff --git a/Shogi/BackEnd/Types/Piece.cs b/Shogi/BackEnd/Types/Piece.cs new file mode 100644 index 0000000..2c7c41c --- /dev/null +++ b/Shogi/BackEnd/Types/Piece.cs @@ -0,0 +1,8 @@ +namespace Shogi.BackEnd.Types; + +public class Piece +{ + public bool IsPromoted { get; set; } + public WhichPiece WhichPiece { get; set; } + public WhichPlayer Owner { get; set; } +} diff --git a/Shogi.Contracts/Types/Session.cs b/Shogi/BackEnd/Types/Session.cs similarity index 74% rename from Shogi.Contracts/Types/Session.cs rename to Shogi/BackEnd/Types/Session.cs index bc67642..e418097 100644 --- a/Shogi.Contracts/Types/Session.cs +++ b/Shogi/BackEnd/Types/Session.cs @@ -1,6 +1,4 @@ -using System; - -namespace Shogi.Contracts.Types; +namespace Shogi.BackEnd.Types; public class Session { @@ -16,5 +14,5 @@ public class Session public Guid SessionId { get; set; } - public BoardState BoardState { get; set; } + public BoardState BoardState { get; set; } = new(); } diff --git a/Shogi.Contracts/Types/SessionMetadata.cs b/Shogi/BackEnd/Types/SessionMetadata.cs similarity index 77% rename from Shogi.Contracts/Types/SessionMetadata.cs rename to Shogi/BackEnd/Types/SessionMetadata.cs index 8b3c2b3..e242ea7 100644 --- a/Shogi.Contracts/Types/SessionMetadata.cs +++ b/Shogi/BackEnd/Types/SessionMetadata.cs @@ -1,6 +1,4 @@ -using System; - -namespace Shogi.Contracts.Types; +namespace Shogi.BackEnd.Types; public class SessionMetadata { diff --git a/Shogi.Contracts/Types/WhichPiece.cs b/Shogi/BackEnd/Types/WhichPiece.cs similarity index 74% rename from Shogi.Contracts/Types/WhichPiece.cs rename to Shogi/BackEnd/Types/WhichPiece.cs index 2a13589..5b174a6 100644 --- a/Shogi.Contracts/Types/WhichPiece.cs +++ b/Shogi/BackEnd/Types/WhichPiece.cs @@ -1,4 +1,4 @@ -namespace Shogi.Contracts.Types; +namespace Shogi.BackEnd.Types; public enum WhichPiece { diff --git a/Shogi.Contracts/Types/WhichPerspective.cs b/Shogi/BackEnd/Types/WhichPlayer.cs similarity index 57% rename from Shogi.Contracts/Types/WhichPerspective.cs rename to Shogi/BackEnd/Types/WhichPlayer.cs index 6016326..6b52b93 100644 --- a/Shogi.Contracts/Types/WhichPerspective.cs +++ b/Shogi/BackEnd/Types/WhichPlayer.cs @@ -1,4 +1,4 @@ -namespace Shogi.Contracts.Types; +namespace Shogi.BackEnd.Types; public enum WhichPlayer { diff --git a/Shogi.UI/Identity/CookieAuthenticationStateProvider.cs b/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs similarity index 99% rename from Shogi.UI/Identity/CookieAuthenticationStateProvider.cs rename to Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs index 648de91..fb5f1ac 100644 --- a/Shogi.UI/Identity/CookieAuthenticationStateProvider.cs +++ b/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs @@ -1,4 +1,4 @@ -namespace Shogi.UI.Identity; +namespace Shogi.FrontEnd.Client; using Microsoft.AspNetCore.Components.Authorization; using System.Net.Http; diff --git a/Shogi.UI/Identity/FormResult.cs b/Shogi/FrontEnd/Client/FormResult.cs similarity index 89% rename from Shogi.UI/Identity/FormResult.cs rename to Shogi/FrontEnd/Client/FormResult.cs index c2bf8ac..6cc9253 100644 --- a/Shogi.UI/Identity/FormResult.cs +++ b/Shogi/FrontEnd/Client/FormResult.cs @@ -1,4 +1,4 @@ -namespace Shogi.UI.Identity; +namespace Shogi.FrontEnd.Client; public class FormResult { diff --git a/Shogi.UI/Shared/GameHubNode.cs b/Shogi/FrontEnd/Client/GameHubNode.cs similarity index 64% rename from Shogi.UI/Shared/GameHubNode.cs rename to Shogi/FrontEnd/Client/GameHubNode.cs index 6d78365..899bd73 100644 --- a/Shogi.UI/Shared/GameHubNode.cs +++ b/Shogi/FrontEnd/Client/GameHubNode.cs @@ -1,28 +1,19 @@ -using Microsoft.AspNetCore.SignalR.Client; -using Shogi.UI.Identity; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.SignalR.Client; -namespace Shogi.UI.Shared; +namespace Shogi.FrontEnd.Client; public class GameHubNode : IAsyncDisposable { private readonly HubConnection hubConnection; - public GameHubNode(IConfiguration configuration) + public GameHubNode(NavigationManager navigationManager) { - var baseUrl = configuration["ShogiApiUrl"]; - if (string.IsNullOrWhiteSpace(baseUrl)) - { - throw new InvalidOperationException("ShogiApiUrl configuration is missing."); - } + var hubUrl = navigationManager.ToAbsoluteUri("/gamehub"); this.hubConnection = new HubConnectionBuilder() - .WithUrl(new Uri(new Uri(baseUrl, UriKind.Absolute), "gamehub"), options => - { - options.HttpMessageHandlerFactory = handler => new CookieCredentialsMessageHandler { InnerHandler = handler }; - options.SkipNegotiation = true; - options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; - }) - .Build(); + .WithUrl(hubUrl) + .Build(); this.hubConnection.Closed += this.HubConnection_Closed; } diff --git a/Shogi.UI/Identity/IAccountManagement.cs b/Shogi/FrontEnd/Client/IAccountManagement.cs similarity index 96% rename from Shogi.UI/Identity/IAccountManagement.cs rename to Shogi/FrontEnd/Client/IAccountManagement.cs index 3f92acc..76bb5d8 100644 --- a/Shogi.UI/Identity/IAccountManagement.cs +++ b/Shogi/FrontEnd/Client/IAccountManagement.cs @@ -1,4 +1,4 @@ -namespace Shogi.UI.Identity; +namespace Shogi.FrontEnd.Client; /// /// Account management services. diff --git a/Shogi.UI/Shared/LocalStorage.cs b/Shogi/FrontEnd/Client/LocalStorage.cs similarity index 97% rename from Shogi.UI/Shared/LocalStorage.cs rename to Shogi/FrontEnd/Client/LocalStorage.cs index 5acbc3b..8439036 100644 --- a/Shogi.UI/Shared/LocalStorage.cs +++ b/Shogi/FrontEnd/Client/LocalStorage.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Shogi.UI.Shared; +namespace Shogi.FrontEnd.Client; public class LocalStorage : ILocalStorage { diff --git a/Shogi/FrontEnd/Client/ShogiService.cs b/Shogi/FrontEnd/Client/ShogiService.cs new file mode 100644 index 0000000..0a2eab5 --- /dev/null +++ b/Shogi/FrontEnd/Client/ShogiService.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Identity; +using Shogi.BackEnd.Application; +using Shogi.BackEnd.Extensions; +using Shogi.BackEnd.Identity; +using Shogi.BackEnd.Repositories; +using Shogi.BackEnd.Types; + +namespace Shogi.FrontEnd.Client; + +/// +/// Service layer for Blazor components to access application functionality directly, +/// without going through HTTP. +/// +public class ShogiService( + ShogiApplication application, + SessionRepository sessionRepository, + QueryRepository queryRepository, + UserManager userManager, + GameHubContext gameHubContext) +{ + public async Task CreateSession(string playerId) + { + var sessionId = Guid.NewGuid(); + var session = new BackEnd.Domains.Aggregates.Session(sessionId, playerId); + + try + { + await sessionRepository.CreateSession(session); + return sessionId; + } + catch + { + return null; + } + } + + public async Task GetSession(string sessionId) + { + var domainSession = await application.ReadSession(sessionId); + if (domainSession is null) return null; + + return new Session + { + BoardState = new BoardState + { + Board = domainSession.Board.BoardState.State.ToContract(), + Player1Hand = domainSession.Board.BoardState.Player1Hand.ToContract(), + Player2Hand = domainSession.Board.BoardState.Player2Hand.ToContract(), + PlayerInCheck = domainSession.Board.BoardState.InCheck?.ToContract(), + WhoseTurn = domainSession.Board.BoardState.WhoseTurn.ToContract(), + Victor = domainSession.Board.BoardState.IsCheckmate + ? domainSession.Board.BoardState.InCheck == BackEnd.Domains.ValueObjects.WhichPlayer.Player1 + ? WhichPlayer.Player2 + : WhichPlayer.Player1 + : null + }, + Player1 = GetUsername(domainSession.Player1), + Player2 = GetUsername(domainSession.Player2), + SessionId = domainSession.Id + }; + } + + public async Task GetAllSessionsMetadata(string? playerId) + { + var dtos = await application.ReadAllSessionMetadatas(playerId ?? string.Empty); + return dtos + .Select(dto => new SessionMetadata + { + Player1 = GetUsername(dto.Player1Id), + Player2 = GetUsername(dto.Player2Id), + SessionId = Guid.Parse(dto.Id), + }) + .ToArray(); + } + + public async Task Move(string playerId, Guid sessionId, MovePieceCommand command) + { + var result = await application.MovePiece(playerId, sessionId.ToString(), command); + return result is Microsoft.AspNetCore.Mvc.NoContentResult; + } + + public async Task JoinSession(string sessionId, string playerId) + { + var result = await application.JoinSession(sessionId, playerId); + return result is Microsoft.AspNetCore.Mvc.OkResult; + } + + public async Task DeleteSession(Guid sessionId, string playerId) + { + var (session, _) = await sessionRepository.ReadSessionAndMoves(sessionId.ToString()); + if (!session.HasValue) return true; + + if (session.Value.Player1Id == playerId) + { + await sessionRepository.DeleteSession(sessionId.ToString()); + return true; + } + + return false; + } + + private string GetUsername(string? userId) + { + if (string.IsNullOrEmpty(userId)) + { + return string.Empty; + } + + return userManager.Users.FirstOrDefault(u => u.Id == userId)?.UserName ?? string.Empty; + } +} diff --git a/Shogi.UI/Identity/UserInfo.cs b/Shogi/FrontEnd/Client/UserInfo.cs similarity index 93% rename from Shogi.UI/Identity/UserInfo.cs rename to Shogi/FrontEnd/Client/UserInfo.cs index 01e7809..ff78ef4 100644 --- a/Shogi.UI/Identity/UserInfo.cs +++ b/Shogi/FrontEnd/Client/UserInfo.cs @@ -1,4 +1,4 @@ -namespace Shogi.UI.Identity; +namespace Shogi.FrontEnd.Client; /// /// User info from identity endpoint to establish claims. diff --git a/Shogi/FrontEnd/Components/App.razor b/Shogi/FrontEnd/Components/App.razor new file mode 100644 index 0000000..df9f838 --- /dev/null +++ b/Shogi/FrontEnd/Components/App.razor @@ -0,0 +1,20 @@ + + + + + + + + Shogi + + + + + + + + + + + + diff --git a/Shogi.UI/Shared/IconButton.razor b/Shogi/FrontEnd/Components/IconButton.razor similarity index 100% rename from Shogi.UI/Shared/IconButton.razor rename to Shogi/FrontEnd/Components/IconButton.razor diff --git a/Shogi.UI/Shared/IconButton.razor.css b/Shogi/FrontEnd/Components/IconButton.razor.css similarity index 100% rename from Shogi.UI/Shared/IconButton.razor.css rename to Shogi/FrontEnd/Components/IconButton.razor.css diff --git a/Shogi.UI/Shared/Icons/ChevronDownIcon.razor b/Shogi/FrontEnd/Components/Icons/ChevronDownIcon.razor similarity index 100% rename from Shogi.UI/Shared/Icons/ChevronDownIcon.razor rename to Shogi/FrontEnd/Components/Icons/ChevronDownIcon.razor diff --git a/Shogi.UI/Shared/Icons/ChevronUpIcon.razor b/Shogi/FrontEnd/Components/Icons/ChevronUpIcon.razor similarity index 100% rename from Shogi.UI/Shared/Icons/ChevronUpIcon.razor rename to Shogi/FrontEnd/Components/Icons/ChevronUpIcon.razor diff --git a/Shogi.UI/Shared/Icons/TrashCanIcon.razor b/Shogi/FrontEnd/Components/Icons/TrashCanIcon.razor similarity index 100% rename from Shogi.UI/Shared/Icons/TrashCanIcon.razor rename to Shogi/FrontEnd/Components/Icons/TrashCanIcon.razor diff --git a/Shogi.UI/Layout/MainLayout.razor b/Shogi/FrontEnd/Components/Layout/MainLayout.razor similarity index 56% rename from Shogi.UI/Layout/MainLayout.razor rename to Shogi/FrontEnd/Components/Layout/MainLayout.razor index 35169e1..bac1397 100644 --- a/Shogi.UI/Layout/MainLayout.razor +++ b/Shogi/FrontEnd/Components/Layout/MainLayout.razor @@ -1,6 +1,6 @@ @inherits LayoutComponentBase -
+
@Body
diff --git a/Shogi.UI/Layout/MainLayout.razor.css b/Shogi/FrontEnd/Components/Layout/MainLayout.razor.css similarity index 100% rename from Shogi.UI/Layout/MainLayout.razor.css rename to Shogi/FrontEnd/Components/Layout/MainLayout.razor.css diff --git a/Shogi.UI/Layout/NavMenu.razor b/Shogi/FrontEnd/Components/Layout/NavMenu.razor similarity index 79% rename from Shogi.UI/Layout/NavMenu.razor rename to Shogi/FrontEnd/Components/Layout/NavMenu.razor index febff03..2086597 100644 --- a/Shogi.UI/Layout/NavMenu.razor +++ b/Shogi/FrontEnd/Components/Layout/NavMenu.razor @@ -1,5 +1,5 @@ @inject NavigationManager navigator -@inject ShogiApi Api +@inject ShogiService Service @* Desktop view *@ @code { + [CascadingParameter] + private Task AuthState { get; set; } = default!; + private bool isExpanded = false; async Task CreateSession() { - var sessionId = await Api.PostSession(); - if (!string.IsNullOrEmpty(sessionId)) + var state = await AuthState; + var userId = state.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (userId is null) return; + + var sessionId = await Service.CreateSession(userId); + if (sessionId.HasValue) { - navigator.NavigateTo($"play/{sessionId}"); + navigator.NavigateTo($"play/{sessionId.Value}"); } } diff --git a/Shogi.UI/Layout/NavMenu.razor.css b/Shogi/FrontEnd/Components/Layout/NavMenu.razor.css similarity index 100% rename from Shogi.UI/Layout/NavMenu.razor.css rename to Shogi/FrontEnd/Components/Layout/NavMenu.razor.css diff --git a/Shogi.UI/Pages/FancyErrorPage.razor b/Shogi/FrontEnd/Components/Pages/FancyErrorPage.razor similarity index 100% rename from Shogi.UI/Pages/FancyErrorPage.razor rename to Shogi/FrontEnd/Components/Pages/FancyErrorPage.razor diff --git a/Shogi.UI/Pages/FancyErrorPage.razor.css b/Shogi/FrontEnd/Components/Pages/FancyErrorPage.razor.css similarity index 100% rename from Shogi.UI/Pages/FancyErrorPage.razor.css rename to Shogi/FrontEnd/Components/Pages/FancyErrorPage.razor.css diff --git a/Shogi.UI/Pages/Home/HomePage.razor b/Shogi/FrontEnd/Components/Pages/Home/HomePage.razor similarity index 98% rename from Shogi.UI/Pages/Home/HomePage.razor rename to Shogi/FrontEnd/Components/Pages/Home/HomePage.razor index 4185bc4..5b9dcdd 100644 --- a/Shogi.UI/Pages/Home/HomePage.razor +++ b/Shogi/FrontEnd/Components/Pages/Home/HomePage.razor @@ -1,6 +1,5 @@ @page "/" -@using Shogi.Contracts.Types @using System.Net.WebSockets @using System.Text diff --git a/Shogi.UI/Pages/Home/HomePage.razor.css b/Shogi/FrontEnd/Components/Pages/Home/HomePage.razor.css similarity index 100% rename from Shogi.UI/Pages/Home/HomePage.razor.css rename to Shogi/FrontEnd/Components/Pages/Home/HomePage.razor.css diff --git a/Shogi.UI/Pages/Home/VisualAids/BishopMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/BishopMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/BishopMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/BishopMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/BoardSetupVisualAid.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/BoardSetupVisualAid.razor similarity index 98% rename from Shogi.UI/Pages/Home/VisualAids/BoardSetupVisualAid.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/BoardSetupVisualAid.razor index 8ad5251..9dc4db1 100644 --- a/Shogi.UI/Pages/Home/VisualAids/BoardSetupVisualAid.razor +++ b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/BoardSetupVisualAid.razor @@ -1,6 +1,4 @@ -@using Shogi.Contracts.Types - -
+
diff --git a/Shogi.UI/Pages/Home/VisualAids/BoardSetupVisualAid.razor.css b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/BoardSetupVisualAid.razor.css similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/BoardSetupVisualAid.razor.css rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/BoardSetupVisualAid.razor.css diff --git a/Shogi.UI/Pages/Home/VisualAids/DragonMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/DragonMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/DragonMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/DragonMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/GoldGeneralMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/GoldGeneralMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/GoldGeneralMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/GoldGeneralMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/HorseMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/HorseMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/HorseMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/HorseMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/KingMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/KingMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/KingMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/KingMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/KnightMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/KnightMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/KnightMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/KnightMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/LanceMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/LanceMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/LanceMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/LanceMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PawnMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PawnMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PawnMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PawnMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PieceMovesVisualAid.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PieceMovesVisualAid.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PieceMovesVisualAid.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PieceMovesVisualAid.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PieceMovesVisualAid.razor.css b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PieceMovesVisualAid.razor.css similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PieceMovesVisualAid.razor.css rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PieceMovesVisualAid.razor.css diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedKnightMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedKnightMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedKnightMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedKnightMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedLanceMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedLanceMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedLanceMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedLanceMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedPawnMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPawnMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedPawnMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPawnMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedPieceVisualAid.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedPieceVisualAid.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedPieceVisualAid.razor.css b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor.css similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedPieceVisualAid.razor.css rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor.css diff --git a/Shogi.UI/Pages/Home/VisualAids/PromotedSilverMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedSilverMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/PromotedSilverMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedSilverMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/RookMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/RookMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/RookMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/RookMoves.razor diff --git a/Shogi.UI/Pages/Home/VisualAids/SilverMoves.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/SilverMoves.razor similarity index 100% rename from Shogi.UI/Pages/Home/VisualAids/SilverMoves.razor rename to Shogi/FrontEnd/Components/Pages/Home/VisualAids/SilverMoves.razor diff --git a/Shogi/FrontEnd/Components/Pages/Home/_Imports.razor b/Shogi/FrontEnd/Components/Pages/Home/_Imports.razor new file mode 100644 index 0000000..b57aff1 --- /dev/null +++ b/Shogi/FrontEnd/Components/Pages/Home/_Imports.razor @@ -0,0 +1 @@ +@using Shogi.FrontEnd.Components.Pages.Home.VisualAids \ No newline at end of file diff --git a/Shogi.UI/Pages/Identity/ForgotPassword.razor b/Shogi/FrontEnd/Components/Pages/Identity/ForgotPassword.razor similarity index 100% rename from Shogi.UI/Pages/Identity/ForgotPassword.razor rename to Shogi/FrontEnd/Components/Pages/Identity/ForgotPassword.razor diff --git a/Shogi.UI/Pages/Identity/ForgotPassword.razor.css b/Shogi/FrontEnd/Components/Pages/Identity/ForgotPassword.razor.css similarity index 100% rename from Shogi.UI/Pages/Identity/ForgotPassword.razor.css rename to Shogi/FrontEnd/Components/Pages/Identity/ForgotPassword.razor.css diff --git a/Shogi.UI/Pages/Identity/LoginPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor similarity index 100% rename from Shogi.UI/Pages/Identity/LoginPage.razor rename to Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor diff --git a/Shogi.UI/Pages/Identity/LoginPage.razor.css b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css similarity index 100% rename from Shogi.UI/Pages/Identity/LoginPage.razor.css rename to Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css diff --git a/Shogi.UI/Pages/Identity/LogoutPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LogoutPage.razor similarity index 100% rename from Shogi.UI/Pages/Identity/LogoutPage.razor rename to Shogi/FrontEnd/Components/Pages/Identity/LogoutPage.razor diff --git a/Shogi.UI/Pages/Identity/RegisterPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/RegisterPage.razor similarity index 100% rename from Shogi.UI/Pages/Identity/RegisterPage.razor rename to Shogi/FrontEnd/Components/Pages/Identity/RegisterPage.razor diff --git a/Shogi.UI/Pages/Identity/RegisterPage.razor.css b/Shogi/FrontEnd/Components/Pages/Identity/RegisterPage.razor.css similarity index 100% rename from Shogi.UI/Pages/Identity/RegisterPage.razor.css rename to Shogi/FrontEnd/Components/Pages/Identity/RegisterPage.razor.css diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBoard/EmptyGameBoard.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/EmptyGameBoard.razor new file mode 100644 index 0000000..b24e7d0 --- /dev/null +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/EmptyGameBoard.razor @@ -0,0 +1,4 @@ + + +@code { +} diff --git a/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoard.razor similarity index 90% rename from Shogi.UI/Pages/Play/GameBoard/GameBoard.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoard.razor index 1724e1b..a9d722e 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameBoard.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoard.razor @@ -1,10 +1,8 @@ -@using Shogi.Contracts.Api -@using Shogi.Contracts.Types -@using System.Text.RegularExpressions +@using System.Text.RegularExpressions @using System.Security.Claims @implements IDisposable -@inject ShogiApi ShogiApi +@inject ShogiService Service @inject GameHubNode hubNode @inject NavigationManager navigator @@ -52,7 +50,7 @@ else { if (!string.IsNullOrWhiteSpace(SessionId)) { - this.session = await ShogiApi.GetSession(SessionId); + this.session = await Service.GetSession(SessionId); if (this.session != null) { var state = await authenticationState; diff --git a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoardPresentation.razor similarity index 99% rename from Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoardPresentation.razor index 21679ab..8bd7339 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameBoardPresentation.razor @@ -1,5 +1,4 @@ -@using Shogi.Contracts.Types; -@using System.Text.Json; +@using System.Text.Json;
diff --git a/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameboardPresentation.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/GameboardPresentation.razor.css diff --git a/Shogi.UI/Pages/Play/GameBoard/OpponentName.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/OpponentName.razor similarity index 90% rename from Shogi.UI/Pages/Play/GameBoard/OpponentName.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/OpponentName.razor index da3fc0c..ce82dd6 100644 --- a/Shogi.UI/Pages/Play/GameBoard/OpponentName.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/OpponentName.razor @@ -1,5 +1,4 @@ -@using Shogi.Contracts.Types -
+
@if (IsTurn) { diff --git a/Shogi.UI/Pages/Play/GameBoard/OpponentName.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/OpponentName.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBoard/OpponentName.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/OpponentName.razor.css diff --git a/Shogi.UI/Pages/Play/GameBoard/PlayerName.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/PlayerName.razor similarity index 89% rename from Shogi.UI/Pages/Play/GameBoard/PlayerName.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/PlayerName.razor index 05d961b..615a8dd 100644 --- a/Shogi.UI/Pages/Play/GameBoard/PlayerName.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/PlayerName.razor @@ -1,5 +1,4 @@ -@using Shogi.Contracts.Types -
+
@if (string.IsNullOrEmpty(Name)) { diff --git a/Shogi.UI/Pages/Play/GameBoard/PlayerName.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/PlayerName.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBoard/PlayerName.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/PlayerName.razor.css diff --git a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/SeatedGameBoard.razor similarity index 86% rename from Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/SeatedGameBoard.razor index fc66d1c..198706b 100644 --- a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/SeatedGameBoard.razor @@ -1,8 +1,7 @@ -@using Shogi.Contracts.Api.Commands -@using Shogi.Contracts.Types; @using System.Text.RegularExpressions; @using System.Net; -@inject ShogiApi ShogiApi; +@using System.Security.Claims +@inject ShogiService Service @code { + [CascadingParameter] + private Task AuthState { get; set; } = default!; + [Parameter, EditorRequired] public WhichPlayer Perspective { get; set; } [Parameter, EditorRequired] @@ -43,16 +45,18 @@ private bool showPromotePrompt; private string? moveTo; private bool showError = false; + private string? userId; - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { - base.OnParametersSet(); selectedBoardPosition = null; selectedPieceFromHand = null; if (Session == null) { throw new ArgumentException($"{nameof(Session)} cannot be null.", nameof(Session)); } + var state = await AuthState; + userId = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; } bool IsWithinPromoteArea(string position) @@ -96,8 +100,7 @@ if (pieceAtPosition is null) { // Placing a piece from the hand to an empty space. - var success = await ShogiApi.Move( - Session.SessionId, + var success = await Service.Move(userId!, Session.SessionId, new MovePieceCommand(selectedPieceFromHand.Value, position)); if (!success) { @@ -123,7 +126,7 @@ } else { - var success = await ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedBoardPosition, position, false)); + var success = await Service.Move(userId!, Session.SessionId, new MovePieceCommand(selectedBoardPosition, position, false)); if (!success) { selectedBoardPosition = null; @@ -155,7 +158,7 @@ { if (selectedBoardPosition != null && moveTo != null) { - showError = await ShogiApi.Move(Session.SessionId, new MovePieceCommand(selectedBoardPosition, moveTo, shouldPromote)); + showError = await Service.Move(userId!, Session.SessionId, new MovePieceCommand(selectedBoardPosition, moveTo, shouldPromote)); showPromotePrompt = false; return; } @@ -168,3 +171,4 @@ showError = false; } } + diff --git a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/SeatedGameBoard.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/SeatedGameBoard.razor.css diff --git a/Shogi.UI/Pages/Play/GameBoard/SpectatorGameBoard.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/SpectatorGameBoard.razor similarity index 59% rename from Shogi.UI/Pages/Play/GameBoard/SpectatorGameBoard.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBoard/SpectatorGameBoard.razor index fcc8dd2..7a6e941 100644 --- a/Shogi.UI/Pages/Play/GameBoard/SpectatorGameBoard.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBoard/SpectatorGameBoard.razor @@ -1,6 +1,6 @@ -@using Contracts.Types -@using System.Net -@inject ShogiApi ShogiApi +@using System.Net +@using System.Security.Claims +@inject ShogiService Service @code { + [CascadingParameter] + private Task AuthState { get; set; } = default!; + [Parameter] [EditorRequired] public Session Session { get; set; } = default!; @@ -25,7 +28,10 @@ async Task OnClickJoinGame() { - var response = await ShogiApi.PatchJoinGame(Session.SessionId.ToString()); - response.EnsureSuccessStatusCode(); + var state = await AuthState; + var userId = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (userId is null) return; + + await Service.JoinSession(Session.SessionId.ToString(), userId); } } diff --git a/Shogi.UI/Pages/Play/GameBrowser.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor similarity index 83% rename from Shogi.UI/Pages/Play/GameBrowser.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor index 81d47fd..7866211 100644 --- a/Shogi.UI/Pages/Play/GameBrowser.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor @@ -1,9 +1,8 @@ -@using Shogi.Contracts.Types; -@using System.ComponentModel.DataAnnotations; +@using System.ComponentModel.DataAnnotations; @using System.Net; @using System.Text.Json; -@inject ShogiApi ShogiApi +@inject ShogiService Service
@@ -37,7 +36,7 @@ async Task FetchSessions() { - var sessions = await ShogiApi.GetAllSessionsMetadata(); + var sessions = await Service.GetAllSessionsMetadata(null); Console.WriteLine("Session count {0}", sessions.Length); if (sessions != null) { diff --git a/Shogi.UI/Pages/Play/GameBrowser.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBrowser.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor.css diff --git a/Shogi.UI/Pages/Play/GameBrowserEntry.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor similarity index 79% rename from Shogi.UI/Pages/Play/GameBrowserEntry.razor rename to Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor index 389db93..ab69b0a 100644 --- a/Shogi.UI/Pages/Play/GameBrowserEntry.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor @@ -1,6 +1,5 @@ -@using Shogi.Contracts.Types - -@inject ShogiApi Api +@inject ShogiService Service +@using System.Security.Claims @if (showDeletePrompt) @@ -46,6 +45,9 @@ @code { + [CascadingParameter] + private Task AuthState { get; set; } = default!; + [Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!; [Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; } private bool showDeletePrompt = false; @@ -59,8 +61,12 @@ async Task DeleteSession() { - var response = await Api.DeleteSession(Session.SessionId); - if (response.IsSuccessStatusCode) + var state = await AuthState; + var userId = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (userId is null) return; + + var success = await Service.DeleteSession(Session.SessionId, userId); + if (success) { showDeletePrompt = false; showDeleteError = false; diff --git a/Shogi.UI/Pages/Play/GameBrowserEntry.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GameBrowserEntry.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css diff --git a/Shogi.UI/Pages/Play/GamePiece.razor b/Shogi/FrontEnd/Components/Pages/Play/GamePiece.razor similarity index 94% rename from Shogi.UI/Pages/Play/GamePiece.razor rename to Shogi/FrontEnd/Components/Pages/Play/GamePiece.razor index b4ce0ae..6c09228 100644 --- a/Shogi.UI/Pages/Play/GamePiece.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GamePiece.razor @@ -1,6 +1,4 @@ -@using Shogi.Contracts.Types - -
+
@switch (Piece) { case WhichPiece.Bishop: diff --git a/Shogi.UI/Pages/Play/GamePiece.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GamePiece.razor.css similarity index 100% rename from Shogi.UI/Pages/Play/GamePiece.razor.css rename to Shogi/FrontEnd/Components/Pages/Play/GamePiece.razor.css diff --git a/Shogi.UI/Pages/Play/Pieces/Bishop.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/Bishop.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/Bishop.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/Bishop.razor diff --git a/Shogi.UI/Pages/Play/Pieces/ChallengingKing.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/ChallengingKing.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/ChallengingKing.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/ChallengingKing.razor diff --git a/Shogi.UI/Pages/Play/Pieces/GoldGeneral.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/GoldGeneral.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/GoldGeneral.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/GoldGeneral.razor diff --git a/Shogi.UI/Pages/Play/Pieces/Knight.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/Knight.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/Knight.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/Knight.razor diff --git a/Shogi.UI/Pages/Play/Pieces/Lance.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/Lance.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/Lance.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/Lance.razor diff --git a/Shogi.UI/Pages/Play/Pieces/Pawn.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/Pawn.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/Pawn.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/Pawn.razor diff --git a/Shogi.UI/Pages/Play/Pieces/ReigningKing.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/ReigningKing.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/ReigningKing.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/ReigningKing.razor diff --git a/Shogi.UI/Pages/Play/Pieces/Rook.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/Rook.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/Rook.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/Rook.razor diff --git a/Shogi.UI/Pages/Play/Pieces/SilverGeneral.razor b/Shogi/FrontEnd/Components/Pages/Play/Pieces/SilverGeneral.razor similarity index 100% rename from Shogi.UI/Pages/Play/Pieces/SilverGeneral.razor rename to Shogi/FrontEnd/Components/Pages/Play/Pieces/SilverGeneral.razor diff --git a/Shogi.UI/Pages/Play/PlayPage.razor b/Shogi/FrontEnd/Components/Pages/Play/PlayPage.razor similarity index 100% rename from Shogi.UI/Pages/Play/PlayPage.razor rename to Shogi/FrontEnd/Components/Pages/Play/PlayPage.razor diff --git a/Shogi.UI/Pages/SearchPage.razor b/Shogi/FrontEnd/Components/Pages/SearchPage.razor similarity index 100% rename from Shogi.UI/Pages/SearchPage.razor rename to Shogi/FrontEnd/Components/Pages/SearchPage.razor diff --git a/Shogi.UI/Pages/SearchPage.razor.css b/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css similarity index 100% rename from Shogi.UI/Pages/SearchPage.razor.css rename to Shogi/FrontEnd/Components/Pages/SearchPage.razor.css diff --git a/Shogi.UI/Shared/RotatingCogsSvg.razor b/Shogi/FrontEnd/Components/RotatingCogsSvg.razor similarity index 100% rename from Shogi.UI/Shared/RotatingCogsSvg.razor rename to Shogi/FrontEnd/Components/RotatingCogsSvg.razor diff --git a/Shogi.UI/Shared/RotatingCogsSvg.razor.css b/Shogi/FrontEnd/Components/RotatingCogsSvg.razor.css similarity index 100% rename from Shogi.UI/Shared/RotatingCogsSvg.razor.css rename to Shogi/FrontEnd/Components/RotatingCogsSvg.razor.css diff --git a/Shogi.UI/App.razor b/Shogi/FrontEnd/Components/Routes.razor similarity index 74% rename from Shogi.UI/App.razor rename to Shogi/FrontEnd/Components/Routes.razor index e7359b7..71ddacf 100644 --- a/Shogi.UI/App.razor +++ b/Shogi/FrontEnd/Components/Routes.razor @@ -1,8 +1,8 @@ - - - + + + Not found @@ -11,5 +11,4 @@ - diff --git a/Shogi.UI/Shared/Stretch.razor b/Shogi/FrontEnd/Components/Stretch.razor similarity index 100% rename from Shogi.UI/Shared/Stretch.razor rename to Shogi/FrontEnd/Components/Stretch.razor diff --git a/Shogi.UI/Shared/Stretch.razor.css b/Shogi/FrontEnd/Components/Stretch.razor.css similarity index 100% rename from Shogi.UI/Shared/Stretch.razor.css rename to Shogi/FrontEnd/Components/Stretch.razor.css diff --git a/Shogi.UI/Shared/TemporaryModal.razor b/Shogi/FrontEnd/Components/TemporaryModal.razor similarity index 100% rename from Shogi.UI/Shared/TemporaryModal.razor rename to Shogi/FrontEnd/Components/TemporaryModal.razor diff --git a/Shogi.UI/Shared/TemporaryModal.razor.css b/Shogi/FrontEnd/Components/TemporaryModal.razor.css similarity index 100% rename from Shogi.UI/Shared/TemporaryModal.razor.css rename to Shogi/FrontEnd/Components/TemporaryModal.razor.css diff --git a/Shogi.UI/_Imports.razor b/Shogi/FrontEnd/Components/_Imports.razor similarity index 50% rename from Shogi.UI/_Imports.razor rename to Shogi/FrontEnd/Components/_Imports.razor index 2833a05..efec8a7 100644 --- a/Shogi.UI/_Imports.razor +++ b/Shogi/FrontEnd/Components/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @@ -6,12 +6,12 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop -@using Shogi.UI.Identity -@using Shogi.UI.Layout -@using Shogi.UI.Pages.Play -@using Shogi.UI.Pages.Play.GameBoard -@using Shogi.UI.Pages.Play.Pieces -@using Shogi.UI.Shared -@using Shogi.UI.Shared.Icons \ No newline at end of file +@using Shogi.FrontEnd.Client +@using Shogi.FrontEnd.Components +@using Shogi.FrontEnd.Components.Layout +@using Shogi.FrontEnd.Components.Pages.Play +@using Shogi.FrontEnd.Components.Pages.Play.GameBoard +@using Shogi.FrontEnd.Components.Pages.Play.Pieces +@using Shogi.FrontEnd.Components.Icons +@using Shogi.BackEnd.Types diff --git a/Shogi/Program.cs b/Shogi/Program.cs new file mode 100644 index 0000000..972a664 --- /dev/null +++ b/Shogi/Program.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.EntityFrameworkCore; +using Shogi; +using Shogi.BackEnd.Application; +using Shogi.FrontEnd.Components; +using Shogi.BackEnd.Controllers; +using Shogi.BackEnd.Identity; +using Shogi.BackEnd.Repositories; +using Shogi.FrontEnd.Client; + +var builder = WebApplication.CreateBuilder(args); + +// Add Blazor components +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +// Add API controllers +builder.Services + .AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.WriteIndented = true; + }); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Application services +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddHttpClient(); +builder.Services.Configure(builder.Configuration.GetSection("ApiKeys")); + +// Client services for Blazor components +builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => sp.GetRequiredService()); +builder.Services.AddScoped(sp => sp.GetRequiredService()); + +AddIdentity(builder, builder.Configuration); +builder.Services.AddSignalR(); +builder.Services.AddResponseCompression(opts => +{ + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); +}); + +var app = builder.Build(); + +app.MyMapIdentityApi(builder.Environment); + +if (app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} +else +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); + app.UseResponseCompression(); +} + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.UseSwagger(); +app.UseSwaggerUI(options => options.DocumentTitle = "Shogi API"); +app.UseAuthorization(); + +app.MapControllers(); +app.MapHub("/gamehub"); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); + +static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration) +{ + builder.Services + .AddAuthorizationBuilder() + .AddPolicy("Admin", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireAssertion(context => context.User?.Identity?.Name switch + { + "Hauth@live.com" => true, + "aat-account" => true, + _ => false + }); + }); + + builder.Services + .AddDbContext(options => + { + var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured."); + options.UseSqlServer(cs); + }) + .AddIdentityApiEndpoints(options => + { + options.SignIn.RequireConfirmedEmail = true; + options.User.RequireUniqueEmail = true; + }) + .AddEntityFrameworkStores(); + + builder.Services.ConfigureApplicationCookie(options => + { + options.SlidingExpiration = true; + options.ExpireTimeSpan = TimeSpan.FromDays(3); + }); +} + +// Make Program accessible for WebApplicationFactory in integration tests +public partial class Program { } diff --git a/Shogi.Api/Properties/PublishProfiles/FolderProfile.pubxml b/Shogi/Properties/PublishProfiles/FolderProfile.pubxml similarity index 100% rename from Shogi.Api/Properties/PublishProfiles/FolderProfile.pubxml rename to Shogi/Properties/PublishProfiles/FolderProfile.pubxml diff --git a/Shogi.Api/Properties/ServiceDependencies/local/secrets1.arm.json b/Shogi/Properties/ServiceDependencies/local/secrets1.arm.json similarity index 100% rename from Shogi.Api/Properties/ServiceDependencies/local/secrets1.arm.json rename to Shogi/Properties/ServiceDependencies/local/secrets1.arm.json diff --git a/Shogi/Properties/launchSettings.json b/Shogi/Properties/launchSettings.json new file mode 100644 index 0000000..98dbd51 --- /dev/null +++ b/Shogi/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ +"profiles": { + "Kestrel": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/", + "AZURE_USERNAME": "Hauth@live.com" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "Swagger": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/", + "AZURE_USERNAME": "Hauth@live.com" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } +}, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50728/", + "sslPort": 44315 + } + } +} \ No newline at end of file diff --git a/Shogi.Api/Properties/serviceDependencies.json b/Shogi/Properties/serviceDependencies.json similarity index 100% rename from Shogi.Api/Properties/serviceDependencies.json rename to Shogi/Properties/serviceDependencies.json diff --git a/Shogi.Api/Properties/serviceDependencies.local.json b/Shogi/Properties/serviceDependencies.local.json similarity index 100% rename from Shogi.Api/Properties/serviceDependencies.local.json rename to Shogi/Properties/serviceDependencies.local.json diff --git a/Shogi.Api/Readme.md b/Shogi/Readme.md similarity index 100% rename from Shogi.Api/Readme.md rename to Shogi/Readme.md diff --git a/Shogi/Shogi.csproj b/Shogi/Shogi.csproj new file mode 100644 index 0000000..f20c609 --- /dev/null +++ b/Shogi/Shogi.csproj @@ -0,0 +1,90 @@ + + + + net10.0 + true + 5 + enable + False + False + enable + 973a1f5f-ef25-4f1c-a24d-b0fc7d016ab8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Shogi.Api/appsettings.Development.json b/Shogi/appsettings.Development.json similarity index 100% rename from Shogi.Api/appsettings.Development.json rename to Shogi/appsettings.Development.json diff --git a/Shogi.Api/appsettings.json b/Shogi/appsettings.json similarity index 90% rename from Shogi.Api/appsettings.json rename to Shogi/appsettings.json index 1946ca8..6108118 100644 --- a/Shogi.Api/appsettings.json +++ b/Shogi/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "ShogiDatabase": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Shogi;Integrated Security=True;Application Name=Shogi.Api" + "ShogiDatabase": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Shogi;Integrated Security=True;Application Name=Shogi" }, "Logging": { "LogLevel": { diff --git a/Shogi.UI/wwwroot/css/app.css b/Shogi/wwwroot/css/app.css similarity index 100% rename from Shogi.UI/wwwroot/css/app.css rename to Shogi/wwwroot/css/app.css diff --git a/Shogi.UI/wwwroot/css/themes.css b/Shogi/wwwroot/css/themes.css similarity index 100% rename from Shogi.UI/wwwroot/css/themes.css rename to Shogi/wwwroot/css/themes.css diff --git a/Shogi.UI/wwwroot/favicon.ico b/Shogi/wwwroot/favicon.ico similarity index 100% rename from Shogi.UI/wwwroot/favicon.ico rename to Shogi/wwwroot/favicon.ico diff --git a/Shogi.UI/wwwroot/svgs/camera-reels.svg b/Shogi/wwwroot/svgs/camera-reels.svg similarity index 100% rename from Shogi.UI/wwwroot/svgs/camera-reels.svg rename to Shogi/wwwroot/svgs/camera-reels.svg diff --git a/Tests/AcceptanceTests/ApiTests.cs b/Tests/AcceptanceTests/ApiTests.cs index b069bb4..8d4824e 100644 --- a/Tests/AcceptanceTests/ApiTests.cs +++ b/Tests/AcceptanceTests/ApiTests.cs @@ -1,16 +1,11 @@ -using FluentAssertions.Execution; using Shogi.AcceptanceTests.TestSetup; -using Shogi.Contracts.Api.Commands; -using Shogi.Contracts.Types; +using Shogi.Types; using System.Net; using System.Net.Http.Json; -using Xunit.Abstractions; namespace Shogi.AcceptanceTests; -//#pragma warning disable xUnit1033 // There is a bug which provides a false positive of xUnit1033. -//#pragma warning restore xUnit1033 -public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClassFixture +public class ApiTests(AatTestFixture fixture) : IClassFixture { private readonly HttpClient httpClient = fixture.HttpClient; private readonly HttpClient player2HttpClient = fixture.OtherHttpClient; @@ -28,16 +23,16 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas var response = await this.ReadTestSession(); // Assert - response.Should().NotBeNull(); - response!.BoardState.Board.Should().NotBeEmpty(); + Assert.NotNull(response); + Assert.NotEmpty(response!.BoardState.Board); ValidateBoard(response.BoardState.Board); - response.BoardState.Player1Hand.Should().BeEmpty(); - response.BoardState.Player2Hand.Should().BeEmpty(); - response.BoardState.PlayerInCheck.Should().BeNull(); - response.BoardState.WhoseTurn.Should().Be(WhichPlayer.Player1); - response.Player1.Should().NotBeNull(); - response.Player2.Should().BeNullOrEmpty(); - response.SessionId.Should().NotBeEmpty(); + Assert.Empty(response.BoardState.Player1Hand); + Assert.Empty(response.BoardState.Player2Hand); + Assert.Null(response.BoardState.PlayerInCheck); + Assert.Equal(WhichPlayer.Player1, response.BoardState.WhoseTurn); + Assert.NotNull(response.Player1); + Assert.True(string.IsNullOrEmpty(response.Player2)); + Assert.NotEqual(Guid.Empty, response.SessionId); } finally { @@ -47,176 +42,175 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas static void ValidateBoard(Dictionary board) { - using var scope = new AssertionScope(); - board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["A1"]!.IsPromoted.Should().Be(false); - board["B1"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["B1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B1"]!.IsPromoted.Should().Be(false); - board["C1"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["C1"]!.IsPromoted.Should().Be(false); - board["D1"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["D1"]!.IsPromoted.Should().Be(false); - board["E1"]!.WhichPiece.Should().Be(WhichPiece.King); - board["E1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["E1"]!.IsPromoted.Should().Be(false); - board["F1"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["F1"]!.IsPromoted.Should().Be(false); - board["G1"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G1"]!.IsPromoted.Should().Be(false); - board["H1"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["H1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H1"]!.IsPromoted.Should().Be(false); - board["I1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["I1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["I1"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Lance, board["A1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["A1"]!.Owner); + Assert.False(board["A1"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["B1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B1"]!.Owner); + Assert.False(board["B1"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["C1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["C1"]!.Owner); + Assert.False(board["C1"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["D1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["D1"]!.Owner); + Assert.False(board["D1"]!.IsPromoted); + Assert.Equal(WhichPiece.King, board["E1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["E1"]!.Owner); + Assert.False(board["E1"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["F1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["F1"]!.Owner); + Assert.False(board["F1"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["G1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["G1"]!.Owner); + Assert.False(board["G1"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["H1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H1"]!.Owner); + Assert.False(board["H1"]!.IsPromoted); + Assert.Equal(WhichPiece.Lance, board["I1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["I1"]!.Owner); + Assert.False(board["I1"]!.IsPromoted); - board["A2"].Should().BeNull(); - board["B2"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["B2"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B2"]!.IsPromoted.Should().Be(false); - board["C2"].Should().BeNull(); - board["D2"].Should().BeNull(); - board["E2"].Should().BeNull(); - board["F2"].Should().BeNull(); - board["G2"].Should().BeNull(); - board["H2"]!.WhichPiece.Should().Be(WhichPiece.Rook); - board["H2"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H2"]!.IsPromoted.Should().Be(false); - board["I2"].Should().BeNull(); + Assert.Null(board["A2"]); + Assert.Equal(WhichPiece.Bishop, board["B2"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B2"]!.Owner); + Assert.False(board["B2"]!.IsPromoted); + Assert.Null(board["C2"]); + Assert.Null(board["D2"]); + Assert.Null(board["E2"]); + Assert.Null(board["F2"]); + Assert.Null(board["G2"]); + Assert.Equal(WhichPiece.Rook, board["H2"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H2"]!.Owner); + Assert.False(board["H2"]!.IsPromoted); + Assert.Null(board["I2"]); - board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["A3"]!.IsPromoted.Should().Be(false); - board["B3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["B3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B3"]!.IsPromoted.Should().Be(false); - board["C3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["C3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["C3"]!.IsPromoted.Should().Be(false); - board["D3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["D3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["D3"]!.IsPromoted.Should().Be(false); - board["E3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["E3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["E3"]!.IsPromoted.Should().Be(false); - board["F3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["F3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["F3"]!.IsPromoted.Should().Be(false); - board["G3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["G3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G3"]!.IsPromoted.Should().Be(false); - board["H3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["H3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H3"]!.IsPromoted.Should().Be(false); - board["I3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["I3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["I3"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Pawn, board["A3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["A3"]!.Owner); + Assert.False(board["A3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["B3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B3"]!.Owner); + Assert.False(board["B3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["C3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["C3"]!.Owner); + Assert.False(board["C3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["D3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["D3"]!.Owner); + Assert.False(board["D3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["E3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["E3"]!.Owner); + Assert.False(board["E3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["F3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["F3"]!.Owner); + Assert.False(board["F3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["G3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["G3"]!.Owner); + Assert.False(board["G3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["H3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H3"]!.Owner); + Assert.False(board["H3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["I3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["I3"]!.Owner); + Assert.False(board["I3"]!.IsPromoted); - board["A4"].Should().BeNull(); - board["B4"].Should().BeNull(); - board["C4"].Should().BeNull(); - board["D4"].Should().BeNull(); - board["E4"].Should().BeNull(); - board["F4"].Should().BeNull(); - board["G4"].Should().BeNull(); - board["H4"].Should().BeNull(); - board["I4"].Should().BeNull(); + Assert.Null(board["A4"]); + Assert.Null(board["B4"]); + Assert.Null(board["C4"]); + Assert.Null(board["D4"]); + Assert.Null(board["E4"]); + Assert.Null(board["F4"]); + Assert.Null(board["G4"]); + Assert.Null(board["H4"]); + Assert.Null(board["I4"]); - board["A5"].Should().BeNull(); - board["B5"].Should().BeNull(); - board["C5"].Should().BeNull(); - board["D5"].Should().BeNull(); - board["E5"].Should().BeNull(); - board["F5"].Should().BeNull(); - board["G5"].Should().BeNull(); - board["H5"].Should().BeNull(); - board["I5"].Should().BeNull(); + Assert.Null(board["A5"]); + Assert.Null(board["B5"]); + Assert.Null(board["C5"]); + Assert.Null(board["D5"]); + Assert.Null(board["E5"]); + Assert.Null(board["F5"]); + Assert.Null(board["G5"]); + Assert.Null(board["H5"]); + Assert.Null(board["I5"]); - board["A6"].Should().BeNull(); - board["B6"].Should().BeNull(); - board["C6"].Should().BeNull(); - board["D6"].Should().BeNull(); - board["E6"].Should().BeNull(); - board["F6"].Should().BeNull(); - board["G6"].Should().BeNull(); - board["H6"].Should().BeNull(); - board["I6"].Should().BeNull(); + Assert.Null(board["A6"]); + Assert.Null(board["B6"]); + Assert.Null(board["C6"]); + Assert.Null(board["D6"]); + Assert.Null(board["E6"]); + Assert.Null(board["F6"]); + Assert.Null(board["G6"]); + Assert.Null(board["H6"]); + Assert.Null(board["I6"]); - board["A7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["A7"]!.IsPromoted.Should().Be(false); - board["B7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["B7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B7"]!.IsPromoted.Should().Be(false); - board["C7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["C7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["C7"]!.IsPromoted.Should().Be(false); - board["D7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["D7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["D7"]!.IsPromoted.Should().Be(false); - board["E7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["E7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["E7"]!.IsPromoted.Should().Be(false); - board["F7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["F7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["F7"]!.IsPromoted.Should().Be(false); - board["G7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["G7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["G7"]!.IsPromoted.Should().Be(false); - board["H7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["H7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H7"]!.IsPromoted.Should().Be(false); - board["I7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["I7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["I7"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Pawn, board["A7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["A7"]!.Owner); + Assert.False(board["A7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["B7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B7"]!.Owner); + Assert.False(board["B7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["C7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["C7"]!.Owner); + Assert.False(board["C7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["D7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["D7"]!.Owner); + Assert.False(board["D7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["E7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["E7"]!.Owner); + Assert.False(board["E7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["F7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["F7"]!.Owner); + Assert.False(board["F7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["G7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["G7"]!.Owner); + Assert.False(board["G7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["H7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H7"]!.Owner); + Assert.False(board["H7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["I7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["I7"]!.Owner); + Assert.False(board["I7"]!.IsPromoted); - board["A8"].Should().BeNull(); - board["B8"]!.WhichPiece.Should().Be(WhichPiece.Rook); - board["B8"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B8"]!.IsPromoted.Should().Be(false); - board["C8"].Should().BeNull(); - board["D8"].Should().BeNull(); - board["E8"].Should().BeNull(); - board["F8"].Should().BeNull(); - board["G8"].Should().BeNull(); - board["H8"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["H8"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H8"]!.IsPromoted.Should().Be(false); - board["I8"].Should().BeNull(); + Assert.Null(board["A8"]); + Assert.Equal(WhichPiece.Rook, board["B8"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B8"]!.Owner); + Assert.False(board["B8"]!.IsPromoted); + Assert.Null(board["C8"]); + Assert.Null(board["D8"]); + Assert.Null(board["E8"]); + Assert.Null(board["F8"]); + Assert.Null(board["G8"]); + Assert.Equal(WhichPiece.Bishop, board["H8"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H8"]!.Owner); + Assert.False(board["H8"]!.IsPromoted); + Assert.Null(board["I8"]); - board["A9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["A9"]!.IsPromoted.Should().Be(false); - board["B9"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["B9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B9"]!.IsPromoted.Should().Be(false); - board["C9"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["C9"]!.IsPromoted.Should().Be(false); - board["D9"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["D9"]!.IsPromoted.Should().Be(false); - board["E9"]!.WhichPiece.Should().Be(WhichPiece.King); - board["E9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["E9"]!.IsPromoted.Should().Be(false); - board["F9"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["F9"]!.IsPromoted.Should().Be(false); - board["G9"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["G9"]!.IsPromoted.Should().Be(false); - board["H9"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["H9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H9"]!.IsPromoted.Should().Be(false); - board["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["I9"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Lance, board["A9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["A9"]!.Owner); + Assert.False(board["A9"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["B9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B9"]!.Owner); + Assert.False(board["B9"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["C9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["C9"]!.Owner); + Assert.False(board["C9"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["D9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["D9"]!.Owner); + Assert.False(board["D9"]!.IsPromoted); + Assert.Equal(WhichPiece.King, board["E9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["E9"]!.Owner); + Assert.False(board["E9"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["F9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["F9"]!.Owner); + Assert.False(board["F9"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["G9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["G9"]!.Owner); + Assert.False(board["G9"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["H9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H9"]!.Owner); + Assert.False(board["H9"]!.IsPromoted); + Assert.Equal(WhichPiece.Lance, board["I9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["I9"]!.Owner); + Assert.False(board["I9"]!.IsPromoted); } } @@ -231,12 +225,11 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas // Act var readAllResponse = await this.httpClient.GetFromJsonAsync( - new Uri("Sessions", UriKind.Relative), - Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions); + new Uri("Sessions", UriKind.Relative)); // Assert - readAllResponse.Should().NotBeNull(); - readAllResponse!.First().SessionId.Should().Be(testSession.SessionId); + Assert.NotNull(readAllResponse); + Assert.Equal(testSession.SessionId, readAllResponse!.First().SessionId); } finally { @@ -257,9 +250,9 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas var joinResponse = await this.player2HttpClient.PatchAsync(new Uri($"Sessions/{this.sessionId}/Join", UriKind.Relative), null); // Assert - joinResponse.StatusCode.Should().Be(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, joinResponse.StatusCode); var readSessionResponse = await this.ReadTestSession(); - readSessionResponse.Player2.Should().NotBeNullOrEmpty(); + Assert.False(string.IsNullOrEmpty(readSessionResponse.Player2)); } finally { @@ -275,15 +268,15 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas // Arrange await this.SetupTestSession(); var joinResponse = await this.player2HttpClient.PatchAsync(new Uri($"Sessions/{this.sessionId}/Join", UriKind.Relative), null); - joinResponse.StatusCode.Should().Be(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, joinResponse.StatusCode); var readSessionResponse = await this.ReadTestSession(); - readSessionResponse.Player2.Should().NotBeNull(); + Assert.NotNull(readSessionResponse.Player2); // Act joinResponse = await this.player2HttpClient.PatchAsync(new Uri($"Sessions/{this.sessionId}/Join", UriKind.Relative), null); // Assert - joinResponse.StatusCode.Should().Be(HttpStatusCode.Conflict); + Assert.Equal(HttpStatusCode.Conflict, joinResponse.StatusCode); } finally { @@ -306,16 +299,16 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas // Act var response = await this.httpClient.PatchAsync(new Uri($"Sessions/{this.sessionId}/Move", UriKind.Relative), JsonContent.Create(movePawnCommand)); - response.StatusCode.Should().Be(HttpStatusCode.NoContent, because: await response.Content.ReadAsStringAsync()); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); // Assert var session = await this.ReadTestSession(); - session.BoardState.Board.Should().ContainKey("A3"); - session.BoardState.Board["A3"].Should().BeNull(); - session.BoardState.Board["A4"].Should().NotBeNull(); - session.BoardState.Board["A4"]!.IsPromoted.Should().BeFalse(); - session.BoardState.Board["A4"]!.Owner.Should().Be(WhichPlayer.Player1); - session.BoardState.Board["A4"]!.WhichPiece.Should().Be(WhichPiece.Pawn); + Assert.Contains("A3", session.BoardState.Board.Keys); + Assert.Null(session.BoardState.Board["A3"]); + Assert.NotNull(session.BoardState.Board["A4"]); + Assert.False(session.BoardState.Board["A4"]!.IsPromoted); + Assert.Equal(WhichPlayer.Player1, session.BoardState.Board["A4"]!.Owner); + Assert.Equal(WhichPiece.Pawn, session.BoardState.Board["A4"]!.WhichPiece); } finally { @@ -327,7 +320,7 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas private async Task SetupTestSession() { var createResponse = await this.httpClient.PostAsync(new Uri("Sessions", UriKind.Relative), null); - createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); this.sessionId = await createResponse.Content.ReadAsStringAsync(); } @@ -339,7 +332,6 @@ public class ApiTests(AatTestFixture fixture, ITestOutputHelper console) : IClas private async Task DeleteTestSession() { var response = await this.httpClient.DeleteAsync(new Uri($"Sessions/{Uri.EscapeDataString(this.sessionId)}", UriKind.Relative)); - response.StatusCode.Should().Be(HttpStatusCode.NoContent, because: await response.Content.ReadAsStringAsync()); - + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } -} \ No newline at end of file +} diff --git a/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj index 6df2195..76f300d 100644 --- a/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj +++ b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj @@ -1,51 +1,31 @@  - net10.0 - enable - enable + net10.0 + enable + enable - false - 96d6281d-a75b-4181-b535-ea34b26dc8a2 - true + false + true - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj.Backup.tmp b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj.Backup.tmp new file mode 100644 index 0000000..60683ba --- /dev/null +++ b/Tests/AcceptanceTests/Shogi.AcceptanceTests.csproj.Backup.tmp @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + + false + 96d6281d-a75b-4181-b535-ea34b26dc8a2 + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs b/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs index 205b92a..c428c29 100644 --- a/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs +++ b/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs @@ -1,114 +1,105 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shogi.Identity; using System.Net.Http.Json; using System.Text.Json; namespace Shogi.AcceptanceTests.TestSetup; /// -/// Acceptance Test fixture for tests which assert features for Microsoft accounts. +/// Integration test fixture using WebApplicationFactory for in-process testing. /// -public class AatTestFixture : IAsyncLifetime, IDisposable +public class AatTestFixture : WebApplicationFactory, IAsyncLifetime { protected static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); - private readonly string testAccountPassword; - private bool disposedValue; + private const string TestPassword = "TestPassword123!"; - public AatTestFixture() + public HttpClient HttpClient { get; private set; } = null!; + public HttpClient OtherHttpClient { get; private set; } = null!; + + protected override void ConfigureWebHost(IWebHostBuilder builder) { - this.Configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .AddJsonFile("appsettings.Development.json", optional: true) - .Build(); + builder.UseEnvironment("Development"); - var baseUrl = this.Configuration["ServiceUrl"] ?? throw new InvalidOperationException(); - this.HttpClient = new HttpClient + builder.ConfigureServices(services => { - BaseAddress = new Uri(baseUrl, UriKind.Absolute) - }; - this.OtherHttpClient = new HttpClient - { - BaseAddress = new Uri(baseUrl, UriKind.Absolute) - }; + // Remove the existing DbContext registration + var descriptor = services.SingleOrDefault( + d => d.ServiceType == typeof(DbContextOptions)); - this.testAccountPassword = this.Configuration["TestUserPassword"]!; - if (string.IsNullOrWhiteSpace(this.testAccountPassword)) - { - throw new InvalidOperationException("TestUserPassword is not configured."); - } - - } - - public IConfiguration Configuration { get; private set; } - public HttpClient HttpClient { get; } - public HttpClient OtherHttpClient { get; } - - protected async Task LoginToTestAccounts() - { - var response = await this.HttpClient.PostAsJsonAsync( - RelativeUri("login"), - new + if (descriptor != null) { - email = "aat-account", - password = this.testAccountPassword, - }, - options: SerializerOptions); + services.Remove(descriptor); + } - response.IsSuccessStatusCode.Should().BeTrue(because: "The test account should exist. If it does not, use the /Account/TestAccount route to create it."); - - var bearerToken = (await response.Content.ReadFromJsonAsync())?.AccessToken; - this.HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bearerToken); - - response = await this.HttpClient.PostAsJsonAsync( - RelativeUri("login"), - new + // Add in-memory database for testing + services.AddDbContext(options => { - email = "aat-account-2", - password = this.testAccountPassword, - }, - options: SerializerOptions); + options.UseInMemoryDatabase("IntegrationTestDb_" + Guid.NewGuid().ToString()); + }); - response.IsSuccessStatusCode.Should().BeTrue(because: "The test account should exist. If it does not, use the /Account/TestAccount route to create it."); - - bearerToken = (await response.Content.ReadFromJsonAsync())?.AccessToken; - this.OtherHttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bearerToken); + // Ensure the database is created and seeded + var sp = services.BuildServiceProvider(); + using var scope = sp.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + }); } public async Task InitializeAsync() { - await this.LoginToTestAccounts(); + this.HttpClient = this.CreateClient(); + this.OtherHttpClient = this.CreateClient(); + + await this.SetupTestAccountsAndLogin(); } - protected virtual void Dispose(bool disposing) + private async Task SetupTestAccountsAndLogin() { - if (!this.disposedValue) - { - if (disposing) - { - this.HttpClient.Dispose(); - } + // Register and login first test account + await RegisterAndLogin(this.HttpClient, "aat-account@test.com", TestPassword); - this.disposedValue = true; + // Register and login second test account + await RegisterAndLogin(this.OtherHttpClient, "aat-account-2@test.com", TestPassword); + } + + private static async Task RegisterAndLogin(HttpClient client, string email, string password) + { + // Try to register (may already exist) + await client.PostAsJsonAsync( + new Uri("register", UriKind.Relative), + new { email, password }, + options: SerializerOptions); + + // Login + var loginResponse = await client.PostAsJsonAsync( + new Uri("login", UriKind.Relative), + new { email, password }, + options: SerializerOptions); + + if (loginResponse.IsSuccessStatusCode) + { + var tokenResponse = await loginResponse.Content.ReadFromJsonAsync(SerializerOptions); + if (tokenResponse?.AccessToken != null) + { + client.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken); + } } } - public Task DisposeAsync() + async Task IAsyncLifetime.DisposeAsync() { - this.Dispose(true); - return Task.CompletedTask; + this.HttpClient?.Dispose(); + this.OtherHttpClient?.Dispose(); + await base.DisposeAsync(); } - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected static Uri RelativeUri(string s) => new(s, UriKind.Relative); - private class LoginResponse { public string AccessToken { get; set; } = string.Empty; } - } diff --git a/Tests/AcceptanceTests/Usings.cs b/Tests/AcceptanceTests/Usings.cs index 7fef4b0..8c927eb 100644 --- a/Tests/AcceptanceTests/Usings.cs +++ b/Tests/AcceptanceTests/Usings.cs @@ -1,2 +1 @@ -global using Xunit; -global using FluentAssertions; \ No newline at end of file +global using Xunit; \ No newline at end of file diff --git a/Tests/AcceptanceTests/appsettings.Development.json b/Tests/AcceptanceTests/appsettings.Development.json deleted file mode 100644 index f6aa32c..0000000 --- a/Tests/AcceptanceTests/appsettings.Development.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TestUserPassword": "I'mAToysRUsK1d" -} diff --git a/Tests/AcceptanceTests/appsettings.json b/Tests/AcceptanceTests/appsettings.json deleted file mode 100644 index f217a91..0000000 --- a/Tests/AcceptanceTests/appsettings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "ServiceUrl": "https://localhost:5001", - "TestUserPassword": "" -} \ No newline at end of file diff --git a/Tests/E2ETests/E2ETests.csproj b/Tests/E2ETests/E2ETests.csproj deleted file mode 100644 index 74fdf80..0000000 --- a/Tests/E2ETests/E2ETests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net10.0 - enable - enable - - false - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/Tests/E2ETests/MicrosoftLoginTests.cs b/Tests/E2ETests/MicrosoftLoginTests.cs deleted file mode 100644 index cb17a53..0000000 --- a/Tests/E2ETests/MicrosoftLoginTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace E2ETests; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -public class MicrosoftLoginTests : PageTest -{ - - [SetUp] - public async void Init() - { - - } - - [Test] - public async Task Test1() - { - await Page.GotoAsync("https://lucaserver.space/shogi", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); - var loginButton = Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "Log in" }); - await loginButton.ClickAsync(); - //await Page.WaitForURLAsync() - } -} \ No newline at end of file diff --git a/Tests/E2ETests/PlaywriteExample.cs b/Tests/E2ETests/PlaywriteExample.cs deleted file mode 100644 index d71891d..0000000 --- a/Tests/E2ETests/PlaywriteExample.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; -using System.Text.RegularExpressions; - -namespace E2ETests; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -public class PlaywriteExample : PageTest -{ - [Test] - public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() - { - await Page.GotoAsync("https://playwright.dev"); - - // Expect a title "to contain" a substring. - await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); - - // create a locator - var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); - - // Expect an attribute "to be strictly equal" to the value. - await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro"); - - // Click the get started link. - await getStarted.ClickAsync(); - - // Expects the URL to contain intro. - await Expect(Page).ToHaveURLAsync(new Regex(".*intro")); - } -} \ No newline at end of file diff --git a/Tests/E2ETests/Usings.cs b/Tests/E2ETests/Usings.cs deleted file mode 100644 index cefced4..0000000 --- a/Tests/E2ETests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/Tests/E2ETests/appsettings.json b/Tests/E2ETests/appsettings.json deleted file mode 100644 index 0db3279..0000000 --- a/Tests/E2ETests/appsettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/Tests/UnitTests/Extensions.cs b/Tests/UnitTests/Extensions.cs index f9486c1..faa6f70 100644 --- a/Tests/UnitTests/Extensions.cs +++ b/Tests/UnitTests/Extensions.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.ValueObjects; +using Shogi.Domains.ValueObjects; using System; using System.Text; diff --git a/Tests/UnitTests/NotationShould.cs b/Tests/UnitTests/NotationShould.cs index 577b8a4..2d213fc 100644 --- a/Tests/UnitTests/NotationShould.cs +++ b/Tests/UnitTests/NotationShould.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.YetToBeAssimilatedIntoDDD; +using Shogi.Domains.YetToBeAssimilatedIntoDDD; using System.Numerics; namespace UnitTests; @@ -8,18 +8,18 @@ public class NotationShould [Fact] public void ConvertFromNotationToVector() { - Notation.FromBoardNotation("A1").Should().Be(new Vector2(0, 0)); - Notation.FromBoardNotation("E5").Should().Be(new Vector2(4, 4)); - Notation.FromBoardNotation("I9").Should().Be(new Vector2(8, 8)); - Notation.FromBoardNotation("C3").Should().Be(new Vector2(2, 2)); + Assert.Equal(new Vector2(0, 0), Notation.FromBoardNotation("A1")); + Assert.Equal(new Vector2(4, 4), Notation.FromBoardNotation("E5")); + Assert.Equal(new Vector2(8, 8), Notation.FromBoardNotation("I9")); + Assert.Equal(new Vector2(2, 2), Notation.FromBoardNotation("C3")); } [Fact] public void ConvertFromVectorToNotation() { - Notation.ToBoardNotation(new Vector2(0, 0)).Should().Be("A1"); - Notation.ToBoardNotation(new Vector2(4, 4)).Should().Be("E5"); - Notation.ToBoardNotation(new Vector2(8, 8)).Should().Be("I9"); - Notation.ToBoardNotation(new Vector2(2, 2)).Should().Be("C3"); + Assert.Equal("A1", Notation.ToBoardNotation(new Vector2(0, 0))); + Assert.Equal("E5", Notation.ToBoardNotation(new Vector2(4, 4))); + Assert.Equal("I9", Notation.ToBoardNotation(new Vector2(8, 8))); + Assert.Equal("C3", Notation.ToBoardNotation(new Vector2(2, 2))); } } diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index 5d0ab72..89ce99a 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -1,5 +1,6 @@ -using Shogi.Domain.ValueObjects; -using Shogi.Domain.ValueObjects.Movement; +using Shogi.Domains.ValueObjects; +using Shogi.Domains.ValueObjects.Movement; +using System.Linq; using System.Numerics; namespace UnitTests; @@ -21,11 +22,11 @@ public class RookShould public void Player1_HasCorrectMoveSet() { var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, 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.Backward, Distance.MultiStep)); + Assert.Equal(4, moveSet.Count); + Assert.Contains(new Path(Direction.Forward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Left, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Right, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Backward, Distance.MultiStep), moveSet); } [Fact] @@ -33,30 +34,30 @@ public class RookShould { // Arrange rook1.Promote(); - rook1.IsPromoted.Should().BeTrue(); + Assert.True(rook1.IsPromoted); // Assert var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, 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.Backward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); + Assert.Equal(8, moveSet.Count); + Assert.Contains(new Path(Direction.Forward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Left, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Right, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Backward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.ForwardLeft, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.BackwardLeft, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.ForwardRight, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.BackwardRight, Distance.OneStep), moveSet); } [Fact] public void Player2_HasCorrectMoveSet() { var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, 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.Backward, Distance.MultiStep)); + Assert.Equal(4, moveSet.Count); + Assert.Contains(new Path(Direction.Forward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Left, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Right, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Backward, Distance.MultiStep), moveSet); } [Fact] @@ -64,23 +65,22 @@ public class RookShould { // Arrange rook2.Promote(); - rook2.IsPromoted.Should().BeTrue(); + Assert.True(rook2.IsPromoted); // Assert var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, 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.Backward, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); + Assert.Equal(8, moveSet.Count); + Assert.Contains(new Path(Direction.Forward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Left, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Right, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.Backward, Distance.MultiStep), moveSet); + Assert.Contains(new Path(Direction.ForwardLeft, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.BackwardLeft, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.ForwardRight, Distance.OneStep), moveSet); + Assert.Contains(new Path(Direction.BackwardRight, Distance.OneStep), moveSet); } - - } + private readonly Rook rookPlayer1; public RookShould() @@ -91,11 +91,11 @@ public class RookShould [Fact] public void Promote() { - rookPlayer1.IsPromoted.Should().BeFalse(); - rookPlayer1.CanPromote.Should().BeTrue(); + Assert.False(rookPlayer1.IsPromoted); + Assert.True(rookPlayer1.CanPromote); rookPlayer1.Promote(); - rookPlayer1.IsPromoted.Should().BeTrue(); - rookPlayer1.CanPromote.Should().BeFalse(); + Assert.True(rookPlayer1.IsPromoted); + Assert.False(rookPlayer1.CanPromote); } [Fact] @@ -104,15 +104,15 @@ public class RookShould Vector2 start = new(0, 0); Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - 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)); + Assert.False(rookPlayer1.IsPromoted); + Assert.Equal(5, steps.Count); + Assert.Contains(new Vector2(0, 1), steps); + Assert.Contains(new Vector2(0, 2), steps); + Assert.Contains(new Vector2(0, 3), steps); + Assert.Contains(new Vector2(0, 4), steps); + Assert.Contains(new Vector2(0, 5), steps); } [Fact] @@ -121,10 +121,10 @@ public class RookShould Vector2 start = new(0, 0); Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); + Assert.False(rookPlayer1.IsPromoted); + Assert.Empty(steps); } [Fact] @@ -134,15 +134,15 @@ public class RookShould Vector2 end = new(0, 5); rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - 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)); + Assert.True(rookPlayer1.IsPromoted); + Assert.Equal(5, steps.Count); + Assert.Contains(new Vector2(0, 1), steps); + Assert.Contains(new Vector2(0, 2), steps); + Assert.Contains(new Vector2(0, 3), steps); + Assert.Contains(new Vector2(0, 4), steps); + Assert.Contains(new Vector2(0, 5), steps); } [Fact] @@ -152,11 +152,11 @@ public class RookShould Vector2 end = new(1, 1); rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); + Assert.True(rookPlayer1.IsPromoted); + Assert.Single(steps); + Assert.Contains(new Vector2(1, 1), steps); } [Fact] @@ -165,15 +165,15 @@ public class RookShould Vector2 start = new(0, 0); Vector2 end = new(0, 5); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - 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)); + Assert.False(rookPlayer1.IsPromoted); + Assert.Equal(5, steps.Count); + Assert.Contains(new Vector2(0, 1), steps); + Assert.Contains(new Vector2(0, 2), steps); + Assert.Contains(new Vector2(0, 3), steps); + Assert.Contains(new Vector2(0, 4), steps); + Assert.Contains(new Vector2(0, 5), steps); } [Fact] @@ -182,10 +182,10 @@ public class RookShould Vector2 start = new(0, 0); Vector2 end = new(1, 1); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); + Assert.False(rookPlayer1.IsPromoted); + Assert.Empty(steps); } [Fact] @@ -195,15 +195,15 @@ public class RookShould Vector2 end = new(0, 5); rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - 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)); + Assert.True(rookPlayer1.IsPromoted); + Assert.Equal(5, steps.Count); + Assert.Contains(new Vector2(0, 1), steps); + Assert.Contains(new Vector2(0, 2), steps); + Assert.Contains(new Vector2(0, 3), steps); + Assert.Contains(new Vector2(0, 4), steps); + Assert.Contains(new Vector2(0, 5), steps); } [Fact] @@ -213,10 +213,10 @@ public class RookShould Vector2 end = new(1, 1); rookPlayer1.Promote(); - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); + var steps = rookPlayer1.GetPathFromStartToEnd(start, end).ToList(); - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); + Assert.True(rookPlayer1.IsPromoted); + Assert.Single(steps); + Assert.Contains(new Vector2(1, 1), steps); } } diff --git a/Tests/UnitTests/ShogiBoardStateShould.cs b/Tests/UnitTests/ShogiBoardStateShould.cs index a4488e8..9541e75 100644 --- a/Tests/UnitTests/ShogiBoardStateShould.cs +++ b/Tests/UnitTests/ShogiBoardStateShould.cs @@ -1,4 +1,4 @@ -using Shogi.Domain.ValueObjects; +using Shogi.Domains.ValueObjects; namespace UnitTests; @@ -11,174 +11,174 @@ public class ShogiBoardStateShould var board = BoardState.StandardStarting; // Assert - board["A1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["A1"]!.IsPromoted.Should().Be(false); - board["B1"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["B1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B1"]!.IsPromoted.Should().Be(false); - board["C1"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["C1"]!.IsPromoted.Should().Be(false); - board["D1"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["D1"]!.IsPromoted.Should().Be(false); - board["E1"]!.WhichPiece.Should().Be(WhichPiece.King); - board["E1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["E1"]!.IsPromoted.Should().Be(false); - board["F1"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["F1"]!.IsPromoted.Should().Be(false); - board["G1"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G1"]!.IsPromoted.Should().Be(false); - board["H1"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["H1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H1"]!.IsPromoted.Should().Be(false); - board["I1"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["I1"]!.Owner.Should().Be(WhichPlayer.Player1); - board["I1"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Lance, board["A1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["A1"]!.Owner); + Assert.False(board["A1"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["B1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B1"]!.Owner); + Assert.False(board["B1"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["C1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["C1"]!.Owner); + Assert.False(board["C1"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["D1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["D1"]!.Owner); + Assert.False(board["D1"]!.IsPromoted); + Assert.Equal(WhichPiece.King, board["E1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["E1"]!.Owner); + Assert.False(board["E1"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["F1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["F1"]!.Owner); + Assert.False(board["F1"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["G1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["G1"]!.Owner); + Assert.False(board["G1"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["H1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H1"]!.Owner); + Assert.False(board["H1"]!.IsPromoted); + Assert.Equal(WhichPiece.Lance, board["I1"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["I1"]!.Owner); + Assert.False(board["I1"]!.IsPromoted); - board["A2"].Should().BeNull(); - board["B2"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["B2"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B2"]!.IsPromoted.Should().Be(false); - board["C2"].Should().BeNull(); - board["D2"].Should().BeNull(); - board["E2"].Should().BeNull(); - board["F2"].Should().BeNull(); - board["G2"].Should().BeNull(); - board["H2"]!.WhichPiece.Should().Be(WhichPiece.Rook); - board["H2"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H2"]!.IsPromoted.Should().Be(false); - board["I2"].Should().BeNull(); + Assert.Null(board["A2"]); + Assert.Equal(WhichPiece.Bishop, board["B2"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B2"]!.Owner); + Assert.False(board["B2"]!.IsPromoted); + Assert.Null(board["C2"]); + Assert.Null(board["D2"]); + Assert.Null(board["E2"]); + Assert.Null(board["F2"]); + Assert.Null(board["G2"]); + Assert.Equal(WhichPiece.Rook, board["H2"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H2"]!.Owner); + Assert.False(board["H2"]!.IsPromoted); + Assert.Null(board["I2"]); - board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["A3"]!.IsPromoted.Should().Be(false); - board["B3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["B3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["B3"]!.IsPromoted.Should().Be(false); - board["C3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["C3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["C3"]!.IsPromoted.Should().Be(false); - board["D3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["D3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["D3"]!.IsPromoted.Should().Be(false); - board["E3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["E3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["E3"]!.IsPromoted.Should().Be(false); - board["F3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["F3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["F3"]!.IsPromoted.Should().Be(false); - board["G3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["G3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G3"]!.IsPromoted.Should().Be(false); - board["H3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["H3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["H3"]!.IsPromoted.Should().Be(false); - board["I3"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["I3"]!.Owner.Should().Be(WhichPlayer.Player1); - board["I3"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Pawn, board["A3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["A3"]!.Owner); + Assert.False(board["A3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["B3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["B3"]!.Owner); + Assert.False(board["B3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["C3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["C3"]!.Owner); + Assert.False(board["C3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["D3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["D3"]!.Owner); + Assert.False(board["D3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["E3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["E3"]!.Owner); + Assert.False(board["E3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["F3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["F3"]!.Owner); + Assert.False(board["F3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["G3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["G3"]!.Owner); + Assert.False(board["G3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["H3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["H3"]!.Owner); + Assert.False(board["H3"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["I3"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["I3"]!.Owner); + Assert.False(board["I3"]!.IsPromoted); - board["A4"].Should().BeNull(); - board["B4"].Should().BeNull(); - board["C4"].Should().BeNull(); - board["D4"].Should().BeNull(); - board["E4"].Should().BeNull(); - board["F4"].Should().BeNull(); - board["G4"].Should().BeNull(); - board["H4"].Should().BeNull(); - board["I4"].Should().BeNull(); + Assert.Null(board["A4"]); + Assert.Null(board["B4"]); + Assert.Null(board["C4"]); + Assert.Null(board["D4"]); + Assert.Null(board["E4"]); + Assert.Null(board["F4"]); + Assert.Null(board["G4"]); + Assert.Null(board["H4"]); + Assert.Null(board["I4"]); - board["A5"].Should().BeNull(); - board["B5"].Should().BeNull(); - board["C5"].Should().BeNull(); - board["D5"].Should().BeNull(); - board["E5"].Should().BeNull(); - board["F5"].Should().BeNull(); - board["G5"].Should().BeNull(); - board["H5"].Should().BeNull(); - board["I5"].Should().BeNull(); + Assert.Null(board["A5"]); + Assert.Null(board["B5"]); + Assert.Null(board["C5"]); + Assert.Null(board["D5"]); + Assert.Null(board["E5"]); + Assert.Null(board["F5"]); + Assert.Null(board["G5"]); + Assert.Null(board["H5"]); + Assert.Null(board["I5"]); - board["A6"].Should().BeNull(); - board["B6"].Should().BeNull(); - board["C6"].Should().BeNull(); - board["D6"].Should().BeNull(); - board["E6"].Should().BeNull(); - board["F6"].Should().BeNull(); - board["G6"].Should().BeNull(); - board["H6"].Should().BeNull(); - board["I6"].Should().BeNull(); + Assert.Null(board["A6"]); + Assert.Null(board["B6"]); + Assert.Null(board["C6"]); + Assert.Null(board["D6"]); + Assert.Null(board["E6"]); + Assert.Null(board["F6"]); + Assert.Null(board["G6"]); + Assert.Null(board["H6"]); + Assert.Null(board["I6"]); - board["A7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["A7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["A7"]!.IsPromoted.Should().Be(false); - board["B7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["B7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B7"]!.IsPromoted.Should().Be(false); - board["C7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["C7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["C7"]!.IsPromoted.Should().Be(false); - board["D7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["D7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["D7"]!.IsPromoted.Should().Be(false); - board["E7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["E7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["E7"]!.IsPromoted.Should().Be(false); - board["F7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["F7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["F7"]!.IsPromoted.Should().Be(false); - board["G7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["G7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["G7"]!.IsPromoted.Should().Be(false); - board["H7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["H7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H7"]!.IsPromoted.Should().Be(false); - board["I7"]!.WhichPiece.Should().Be(WhichPiece.Pawn); - board["I7"]!.Owner.Should().Be(WhichPlayer.Player2); - board["I7"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Pawn, board["A7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["A7"]!.Owner); + Assert.False(board["A7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["B7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B7"]!.Owner); + Assert.False(board["B7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["C7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["C7"]!.Owner); + Assert.False(board["C7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["D7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["D7"]!.Owner); + Assert.False(board["D7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["E7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["E7"]!.Owner); + Assert.False(board["E7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["F7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["F7"]!.Owner); + Assert.False(board["F7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["G7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["G7"]!.Owner); + Assert.False(board["G7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["H7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H7"]!.Owner); + Assert.False(board["H7"]!.IsPromoted); + Assert.Equal(WhichPiece.Pawn, board["I7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["I7"]!.Owner); + Assert.False(board["I7"]!.IsPromoted); - board["A8"].Should().BeNull(); - board["B8"]!.WhichPiece.Should().Be(WhichPiece.Rook); - board["B8"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B8"]!.IsPromoted.Should().Be(false); - board["C8"].Should().BeNull(); - board["D8"].Should().BeNull(); - board["E8"].Should().BeNull(); - board["F8"].Should().BeNull(); - board["G8"].Should().BeNull(); - board["H8"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["H8"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H8"]!.IsPromoted.Should().Be(false); - board["I8"].Should().BeNull(); + Assert.Null(board["A8"]); + Assert.Equal(WhichPiece.Rook, board["B8"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B8"]!.Owner); + Assert.False(board["B8"]!.IsPromoted); + Assert.Null(board["C8"]); + Assert.Null(board["D8"]); + Assert.Null(board["E8"]); + Assert.Null(board["F8"]); + Assert.Null(board["G8"]); + Assert.Equal(WhichPiece.Bishop, board["H8"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H8"]!.Owner); + Assert.False(board["H8"]!.IsPromoted); + Assert.Null(board["I8"]); - board["A9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["A9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["A9"]!.IsPromoted.Should().Be(false); - board["B9"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["B9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["B9"]!.IsPromoted.Should().Be(false); - board["C9"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["C9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["C9"]!.IsPromoted.Should().Be(false); - board["D9"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["D9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["D9"]!.IsPromoted.Should().Be(false); - board["E9"]!.WhichPiece.Should().Be(WhichPiece.King); - board["E9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["E9"]!.IsPromoted.Should().Be(false); - board["F9"]!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); - board["F9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["F9"]!.IsPromoted.Should().Be(false); - board["G9"]!.WhichPiece.Should().Be(WhichPiece.SilverGeneral); - board["G9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["G9"]!.IsPromoted.Should().Be(false); - board["H9"]!.WhichPiece.Should().Be(WhichPiece.Knight); - board["H9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["H9"]!.IsPromoted.Should().Be(false); - board["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - board["I9"]!.Owner.Should().Be(WhichPlayer.Player2); - board["I9"]!.IsPromoted.Should().Be(false); + Assert.Equal(WhichPiece.Lance, board["A9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["A9"]!.Owner); + Assert.False(board["A9"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["B9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["B9"]!.Owner); + Assert.False(board["B9"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["C9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["C9"]!.Owner); + Assert.False(board["C9"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["D9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["D9"]!.Owner); + Assert.False(board["D9"]!.IsPromoted); + Assert.Equal(WhichPiece.King, board["E9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["E9"]!.Owner); + Assert.False(board["E9"]!.IsPromoted); + Assert.Equal(WhichPiece.GoldGeneral, board["F9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["F9"]!.Owner); + Assert.False(board["F9"]!.IsPromoted); + Assert.Equal(WhichPiece.SilverGeneral, board["G9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["G9"]!.Owner); + Assert.False(board["G9"]!.IsPromoted); + Assert.Equal(WhichPiece.Knight, board["H9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["H9"]!.Owner); + Assert.False(board["H9"]!.IsPromoted); + Assert.Equal(WhichPiece.Lance, board["I9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, board["I9"]!.Owner); + Assert.False(board["I9"]!.IsPromoted); } } diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index f35ee88..1fd21de 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -1,15 +1,9 @@ -using Shogi.Domain.ValueObjects; +using Shogi.Domains.ValueObjects; namespace UnitTests; public class ShogiShould { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) - { - this.console = console; - } - [Fact] public void MoveAPieceToAnEmptyPosition() { @@ -17,16 +11,16 @@ public class ShogiShould var shogi = MockShogiBoard(); var board = shogi.BoardState; - board["A4"].Should().BeNull(); + Assert.Null(board["A4"]); var expectedPiece = board["A3"]; - expectedPiece.Should().NotBeNull(); + Assert.NotNull(expectedPiece); // Act shogi.Move("A3", "A4", false); // Assert - board["A3"].Should().BeNull(); - board["A4"].Should().Be(expectedPiece); + Assert.Null(board["A3"]); + Assert.Equal(expectedPiece, board["A4"]); } [Fact] @@ -41,17 +35,14 @@ public class ShogiShould shogi.Move("G7", "G6", false); // P1 Bishop puts P2 in check shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); + Assert.Equal(WhichPlayer.Player2, board.InCheck); // Act - P2 is able to un-check theirself. /// P2 King moves out of check shogi.Move("E9", "E8", false); // Assert - using (new AssertionScope()) - { - board.InCheck.Should().BeNull(); - } + Assert.Null(board.InCheck); } [Fact] @@ -60,18 +51,18 @@ public class ShogiShould // Arrange var shogi = MockShogiBoard(); var board = shogi.BoardState; - board["D5"].Should().BeNull(); + Assert.Null(board["D5"]); // Act var moveResult = shogi.Move("D5", "D6", false); // Assert - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["D5"].Should().BeNull(); - board["D6"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(board["D5"]); + Assert.Null(board["D6"]); + Assert.Empty(board.Player1Hand); + Assert.Empty(board.Player2Hand); } [Fact] @@ -86,13 +77,11 @@ public class ShogiShould var moveResult = shogi.Move("A3", "A3", false); // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A3"].Should().Be(expectedPiece); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(expectedPiece, board["A3"]); + Assert.Empty(board.Player1Hand); + Assert.Empty(board.Player2Hand); } [Fact] @@ -102,21 +91,18 @@ public class ShogiShould var shogi = MockShogiBoard(); var board = shogi.BoardState; var expectedPiece = board["D1"]; - expectedPiece!.WhichPiece.Should().Be(WhichPiece.GoldGeneral); + Assert.Equal(WhichPiece.GoldGeneral, expectedPiece!.WhichPiece); // Act - Move General illegally var moveResult = shogi.Move("D1", "D5", false); // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["D1"].Should().Be(expectedPiece); - board["D5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(expectedPiece, board["D1"]); + Assert.Null(board["D5"]); + Assert.Empty(board.Player1Hand); + Assert.Empty(board.Player2Hand); } [Fact] @@ -126,19 +112,17 @@ public class ShogiShould var shogi = MockShogiBoard(); var board = shogi.BoardState; var expectedPiece = board["A7"]; - expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); + Assert.Equal(WhichPlayer.Player2, expectedPiece!.Owner); + Assert.Equal(WhichPlayer.Player1, board.WhoseTurn); // Act - Move Player2 Pawn when it is Player1 turn. var moveResult = shogi.Move("A7", "A6", false); // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A7"].Should().Be(expectedPiece); - board["A6"].Should().BeNull(); - } + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(expectedPiece, board["A7"]); + Assert.Null(board["A6"]); } [Fact] @@ -149,19 +133,17 @@ public class ShogiShould var board = shogi.BoardState; var lance = board["A1"]; var pawn = board["A3"]; - lance!.Owner.Should().Be(pawn!.Owner); + Assert.Equal(lance!.Owner, pawn!.Owner); // Act - Move P1 Lance through P1 Pawn. var moveResult = shogi.Move("A1", "A5", false); // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board["A1"].Should().Be(lance); - board["A3"].Should().Be(pawn); - board["A5"].Should().BeNull(); - } + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(lance, board["A1"]); + Assert.Equal(pawn, board["A3"]); + Assert.Null(board["A5"]); } [Fact] @@ -172,20 +154,18 @@ public class ShogiShould var board = shogi.BoardState; var knight = board["B1"]; var pawn = board["C3"]; - knight!.Owner.Should().Be(pawn!.Owner); + Assert.Equal(knight!.Owner, pawn!.Owner); // Act - P1 Knight tries to capture P1 Pawn. var moveResult = shogi.Move("B1", "C3", false); - // Arrange - using (new AssertionScope()) - { - 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(); - } + // Assert + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(knight, board["B1"]); + Assert.Equal(pawn, board["C3"]); + Assert.Empty(board.Player1Hand); + Assert.Empty(board.Player2Hand); } [Fact] @@ -200,20 +180,18 @@ public class ShogiShould shogi.Move("G7", "G6", false); // P1 Bishop puts P2 in check shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); + Assert.Equal(WhichPlayer.Player2, board.InCheck); var lance = board["I9"]; // Act - P2 moves Lance while in check. var moveResult = shogi.Move("I9", "I8", false); // Assert - using (new AssertionScope()) - { - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().Be(lance); - board["I8"].Should().BeNull(); - } + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Equal(WhichPlayer.Player2, board.InCheck); + Assert.Equal(lance, board["I9"]); + Assert.Null(board["I8"]); } [Fact] @@ -242,44 +220,44 @@ public class ShogiShould 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); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); + Assert.Equal(4, board.Player1Hand.Count); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Knight); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Lance); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Pawn); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Bishop); + Assert.Equal(WhichPlayer.Player1, board.WhoseTurn); // Act | Assert - Illegally placing Knight from the hand in farthest rank. - board["H9"].Should().BeNull(); + Assert.Null(board["H9"]); 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); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(board["H9"]); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally placing Knight from the hand in second farthest row. - board["H8"].Should().BeNull(); + Assert.Null(board["H8"]); moveResult = shogi.Move(WhichPiece.Knight, "H8"); - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["H8"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(board["H8"]); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Knight); // Act | Assert - Illegally place Lance from the hand. - board["H9"].Should().BeNull(); + Assert.Null(board["H9"]); moveResult = shogi.Move(WhichPiece.Knight, "H9"); - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(board["H9"]); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Lance); // Act | Assert - Illegally place Pawn from the hand. - board["H9"].Should().BeNull(); + Assert.Null(board["H9"]); moveResult = shogi.Move(WhichPiece.Pawn, "H9"); - moveResult.Should().NotBeNull(); - moveResult.IsSuccess.Should().BeFalse(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(board["H9"]); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Pawn); // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. // // TODO @@ -291,33 +269,32 @@ public class ShogiShould // Arrange var shogi = MockShogiBoard(); // P1 Pawn - shogi.Move("C3", "C4", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("C3", "C4", false).IsSuccess); // P2 Pawn - shogi.Move("G7", "G6", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("G7", "G6", false).IsSuccess); // P1 Pawn, arbitrary move. - shogi.Move("A3", "A4", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("A3", "A4", false).IsSuccess); // P2 Bishop takes P1 Bishop - shogi.Move("H8", "B2", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("H8", "B2", false).IsSuccess); // P1 Silver takes P2 Bishop - shogi.Move("C1", "B2", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("C1", "B2", false).IsSuccess); // P2 Pawn, arbtrary move - shogi.Move("A7", "A6", false).IsSuccess.Should().BeTrue(); + Assert.True(shogi.Move("A7", "A6", false).IsSuccess); // P1 drop Bishop, place P2 in check - shogi.Move(WhichPiece.Bishop, "G7").IsSuccess.Should().BeTrue(); - shogi.BoardState.InCheck.Should().Be(WhichPlayer.Player2); - shogi.BoardState.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["E5"].Should().BeNull(); + Assert.True(shogi.Move(WhichPiece.Bishop, "G7").IsSuccess); + Assert.Equal(WhichPlayer.Player2, shogi.BoardState.InCheck); + Assert.Single(shogi.BoardState.Player2Hand, p => p.WhichPiece == WhichPiece.Bishop); + Assert.Null(shogi.BoardState["E5"]); // Act - P2 places a Bishop while in check. var moveResult = shogi.Move(WhichPiece.Bishop, "E5"); // Assert - using var scope = new AssertionScope(); - 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); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Null(shogi.BoardState["E5"]); + Assert.Equal(WhichPlayer.Player2, shogi.BoardState.InCheck); + Assert.Single(shogi.BoardState.Player2Hand, p => p.WhichPiece == WhichPiece.Bishop); } [Fact] @@ -333,22 +310,21 @@ public class ShogiShould shogi.Move("B2", "H8", false); // P2 Pawn shogi.Move("G6", "G5", false); - shogi.BoardState.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - shogi.BoardState["I9"].Should().NotBeNull(); - shogi.BoardState["I9"]!.WhichPiece.Should().Be(WhichPiece.Lance); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + Assert.Single(shogi.BoardState.Player1Hand, p => p.WhichPiece == WhichPiece.Bishop); + Assert.NotNull(shogi.BoardState["I9"]); + Assert.Equal(WhichPiece.Lance, shogi.BoardState["I9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, shogi.BoardState["I9"]!.Owner); // Act - P1 tries to place a piece where an opponent's piece resides. var moveResult = shogi.Move(WhichPiece.Bishop, "I9"); // Assert - using var scope = new AssertionScope(); - 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); - shogi.BoardState["I9"]!.Owner.Should().Be(WhichPlayer.Player2); + Assert.NotNull(moveResult); + Assert.False(moveResult.IsSuccess); + Assert.Single(shogi.BoardState.Player1Hand, p => p.WhichPiece == WhichPiece.Bishop); + Assert.NotNull(shogi.BoardState["I9"]); + Assert.Equal(WhichPiece.Lance, shogi.BoardState["I9"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player2, shogi.BoardState["I9"]!.Owner); } [Fact] @@ -366,7 +342,7 @@ public class ShogiShould shogi.Move("B2", "G7", false); // Assert - board.InCheck.Should().Be(WhichPlayer.Player2); + Assert.Equal(WhichPlayer.Player2, board.InCheck); } [Fact] @@ -384,14 +360,11 @@ public class ShogiShould shogi.Move("B2", "G7", true); // Assert - using (new AssertionScope()) - { - board["B2"].Should().BeNull(); - board["G7"].Should().NotBeNull(); - board["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - board["G7"]!.Owner.Should().Be(WhichPlayer.Player1); - board["G7"]!.IsPromoted.Should().BeTrue(); - } + Assert.Null(board["B2"]); + Assert.NotNull(board["G7"]); + Assert.Equal(WhichPiece.Bishop, board["G7"]!.WhichPiece); + Assert.Equal(WhichPlayer.Player1, board["G7"]!.Owner); + Assert.True(board["G7"]!.IsPromoted); } [Fact] @@ -401,7 +374,7 @@ public class ShogiShould var shogi = MockShogiBoard(); var board = shogi.BoardState; var p1Bishop = board["B2"]; - p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); + Assert.Equal(WhichPiece.Bishop, p1Bishop!.WhichPiece); shogi.Move("C3", "C4", false); shogi.Move("G7", "G6", false); @@ -409,13 +382,9 @@ public class ShogiShould shogi.Move("B2", "H8", false); // Assert - board["B2"].Should().BeNull(); - board["H8"].Should().Be(p1Bishop); - - board - .Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); + Assert.Null(board["B2"]); + Assert.Equal(p1Bishop, board["H8"]); + Assert.Single(board.Player1Hand, p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); } [Fact] @@ -449,8 +418,8 @@ public class ShogiShould shogi.Move("E7", "E8", false); // Assert - checkmate - board.IsCheckmate.Should().BeTrue(); - board.InCheck.Should().Be(WhichPlayer.Player2); + Assert.True(board.IsCheckmate); + Assert.Equal(WhichPlayer.Player2, board.InCheck); } private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 639f4c4..121b2e1 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -8,29 +8,24 @@ - - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + - - + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cbef4a5..15a0c90 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,7 +13,7 @@ variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' - apiProjectName: 'Shogi.Api' + apiProjectName: 'Shogi' uiProjectName: 'Shogi.UI' steps: @@ -77,5 +77,5 @@ inputs: inputs: sshEndpoint: 'LucaServer' runOptions: 'commands' - commands: 'sudo systemctl restart kestrel-shogi.api.service' + commands: 'sudo systemctl restart kestrel-Shogi.service' readyTimeout: '20000' \ No newline at end of file From d9f48244aa050fdf7d822f740dd49d59187eb713 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 31 Dec 2025 09:03:05 -0600 Subject: [PATCH 04/15] yep --- Tests/UnitTests/Extensions.cs | 4 +++- Tests/UnitTests/NotationShould.cs | 2 +- Tests/UnitTests/RookShould.cs | 5 +++-- Tests/UnitTests/ShogiBoardStateShould.cs | 2 +- Tests/UnitTests/ShogiShould.cs | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/UnitTests/Extensions.cs b/Tests/UnitTests/Extensions.cs index faa6f70..1d2e500 100644 --- a/Tests/UnitTests/Extensions.cs +++ b/Tests/UnitTests/Extensions.cs @@ -1,4 +1,6 @@ -using Shogi.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects.Pieces; +using Shogi.BackEnd.Domains.ValueObjects.Rules; using System; using System.Text; diff --git a/Tests/UnitTests/NotationShould.cs b/Tests/UnitTests/NotationShould.cs index 2d213fc..41b64f1 100644 --- a/Tests/UnitTests/NotationShould.cs +++ b/Tests/UnitTests/NotationShould.cs @@ -1,4 +1,4 @@ -using Shogi.Domains.YetToBeAssimilatedIntoDDD; +using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD; using System.Numerics; namespace UnitTests; diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index 89ce99a..a759ef5 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -1,5 +1,6 @@ -using Shogi.Domains.ValueObjects; -using Shogi.Domains.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects.Movement; +using Shogi.BackEnd.Domains.ValueObjects.Pieces; using System.Linq; using System.Numerics; diff --git a/Tests/UnitTests/ShogiBoardStateShould.cs b/Tests/UnitTests/ShogiBoardStateShould.cs index 9541e75..404e01d 100644 --- a/Tests/UnitTests/ShogiBoardStateShould.cs +++ b/Tests/UnitTests/ShogiBoardStateShould.cs @@ -1,4 +1,4 @@ -using Shogi.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects; namespace UnitTests; diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 1fd21de..0ea9fa2 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -1,4 +1,5 @@ -using Shogi.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects; +using Shogi.BackEnd.Domains.ValueObjects.Rules; namespace UnitTests; From a4b08f4cf1c15dfea8516441dd0756988a65edab Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 13 Jan 2026 20:36:03 -0600 Subject: [PATCH 05/15] checkpoint --- .../Components/Pages/Identity/LoginPage.razor | 99 +++++++++++++++---- .../Pages/Identity/LoginPage.razor.css | 32 ++++++ .../Components/Pages/Play/GameBrowser.razor | 15 ++- .../Pages/Play/GameBrowserEntry.razor | 55 ++++++----- .../Pages/Play/GameBrowserEntry.razor.css | 10 ++ .../Components/Pages/SearchPage.razor | 48 ++++++++- .../Components/Pages/SearchPage.razor.css | 36 +++++++ Shogi/wwwroot/css/themes.css | 1 + 8 files changed, 250 insertions(+), 46 deletions(-) diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor index 24b1bbe..8befcab 100644 --- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor +++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor @@ -3,9 +3,9 @@ @inject NavigationManager navigator
-

Login

-
+
+

Login

You're logged in as @context.User.Identity?.Name.
@@ -13,26 +13,32 @@ @if (errorList.Length > 0) { -
    - @foreach (var error in errorList) - { -
  • @error
  • - } -
+
+
    + @foreach (var error in errorList) + { +
  • @error
  • + } +
+
+
} - + - - + @if (isEmailSubmitted) + { + + - Reset password + Reset password + } - +
-
+
@@ -41,21 +47,49 @@ private string email = string.Empty; private string password = string.Empty; private string[] errorList = []; + private System.Threading.CancellationTokenSource? _clearErrorCts; + private bool isEmailSubmitted = false; + private Guid errorKey; - public async Task DoLoginAsync() + private async Task HandleSubmit() { - errorList = []; + if (!isEmailSubmitted) + { + SubmitEmail(); + } + else + { + await DoLoginAsync(); + } + } + + private void SubmitEmail() + { + SetErrors([]); if (string.IsNullOrWhiteSpace(email)) { - errorList = ["Email is required."]; + SetErrors(["Email is required."]); + return; + } + + isEmailSubmitted = true; + } + + public async Task DoLoginAsync() + { + SetErrors([]); + + if (string.IsNullOrWhiteSpace(email)) + { + SetErrors(["Email is required."]); return; } if (string.IsNullOrWhiteSpace(password)) { - errorList = ["Password is required."]; + SetErrors(["Password is required."]); return; } @@ -70,7 +104,34 @@ } else { - errorList = result.ErrorList; + SetErrors(result.ErrorList); + } + } + + private void SetErrors(string[] errors) + { + _clearErrorCts?.Cancel(); + errorList = errors; + + if (errors.Length > 0) + { + errorKey = Guid.NewGuid(); + _clearErrorCts = new System.Threading.CancellationTokenSource(); + _ = ClearErrorsAfterDelay(_clearErrorCts.Token); + } + } + + private async Task ClearErrorsAfterDelay(System.Threading.CancellationToken token) + { + try + { + await Task.Delay(10000, token); + errorList = []; + await InvokeAsync(StateHasChanged); + } + catch (TaskCanceledException) + { + // Ignore } } } \ No newline at end of file diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css index 42ff5c7..f86dbcd 100644 --- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css +++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css @@ -1,20 +1,52 @@ main { padding: 1rem; + display: grid; + place-content: flex-start center; } .LoginForm { grid-area: form; display: inline-grid; grid-template-areas: + "h1 h1" "errors errors" "emailLabel emailControl" "passLabel passControl" ". resetLink" "button button"; gap: 0.5rem 3rem; + color: var(--backgroundColor); + background-color: var(--middlegroundColor); + padding: 2rem; +} +.LoginForm a { + color: var(--middlegroundHrefColor); } .LoginForm .Errors { color: darkred; background-color: var(--foregroundColor); + display: flex; + flex-direction: column; } + + .LoginForm .Errors ul { + margin: 0.5rem 0; + } + +.ErrorProgress { + height: 5px; + background-color: darkred; + width: 100%; + animation: deplete 10s linear forwards; +} + +@keyframes deplete { + from { + width: 100%; + } + + to { + width: 0%; + } +} diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor index 7866211..63028cf 100644 --- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor @@ -12,10 +12,13 @@
- @foreach (var session in allSessions) + @foreach (var session in allSessions) { - + }
@@ -29,6 +32,9 @@ @code { private SessionMetadata[] allSessions = Array.Empty(); + [Parameter] public SessionMetadata? SelectedSession { get; set; } + [Parameter] public EventCallback OnSessionSelected { get; set; } + protected override Task OnInitializedAsync() { return FetchSessions(); @@ -44,4 +50,9 @@ StateHasChanged(); } } + + private async Task HandleSessionSelected(SessionMetadata session) + { + await OnSessionSelected.InvokeAsync(session); + } } diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor index ab69b0a..b5b65d6 100644 --- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor @@ -1,31 +1,31 @@ @inject ShogiService Service @using System.Security.Claims - - @if (showDeletePrompt) - { - -
- @if (showDeleteError) - { -

An error occurred.

-
- - } - else - { -

Do you wish to delete this session?

-
- - - } -
- - } + +@if (showDeletePrompt) +{ + +
+@if (showDeleteError) +{ +

An error occurred.

+
+ +} +else +{ +

Do you wish to delete this session?

+
+ + +} +
+ +} - +
+@Session.Player1 +
@if (string.IsNullOrEmpty(Session.Player2)) { 1 / 2 @@ -50,9 +50,16 @@ [Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!; [Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; } + [Parameter] public EventCallback OnSessionSelected { get; set; } + [Parameter] public bool IsSelected { get; set; } private bool showDeletePrompt = false; private bool showDeleteError = false; + private async Task HandleSessionSelectedClick() + { + await OnSessionSelected.InvokeAsync(Session); + } + void HideModal() { diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css index 9c18c06..b00c83f 100644 --- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css +++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css @@ -5,8 +5,18 @@ padding-left: 5px; /* Matches box shadow on hover */ gap: 1rem; place-items: center start; + cursor: pointer; } + gameBrowserEntry:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + gameBrowserEntry[data-selected="true"] { + background-color: rgba(255, 255, 255, 0.2); + box-shadow: inset 3px 0 0 0 var(--primary-color, #fff); + } + modal { position: absolute; top: 0; diff --git a/Shogi/FrontEnd/Components/Pages/SearchPage.razor b/Shogi/FrontEnd/Components/Pages/SearchPage.razor index 9d3afd5..d957021 100644 --- a/Shogi/FrontEnd/Components/Pages/SearchPage.razor +++ b/Shogi/FrontEnd/Components/Pages/SearchPage.razor @@ -1,11 +1,57 @@ @page "/search" +@inject ShogiService Service +@inject NavigationManager Navigation +

Find Sessions

- +
+ + + +
@code { + private SessionMetadata? selectedSession; + private Session? previewSession; + private async Task HandleSessionSelected(SessionMetadata session) + { + selectedSession = session; + previewSession = null; + + previewSession = await Service.GetSession(session.SessionId.ToString()); + } + + private void JoinGame() + { + if (selectedSession is not null) + { + Navigation.NavigateTo($"/play/{selectedSession.SessionId}"); + } + } } diff --git a/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css b/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css index f14027d..58cbf6f 100644 --- a/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css +++ b/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css @@ -1,3 +1,39 @@ .SearchPage { padding: 0 0.5rem; + display: grid; + grid-template-columns: 100%; + grid-template-rows: auto 1fr; +} + +.search-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: start; +} + +.preview-panel { + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + min-height: 300px; +} + +.preview-panel .no-selection { + color: #888; + font-style: italic; +} + +.preview-actions { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1rem; +} + +.preview-actions button { + padding: 0.5rem 1.5rem; + font-size: 1rem; } diff --git a/Shogi/wwwroot/css/themes.css b/Shogi/wwwroot/css/themes.css index 216e2e7..ea72a42 100644 --- a/Shogi/wwwroot/css/themes.css +++ b/Shogi/wwwroot/css/themes.css @@ -3,6 +3,7 @@ --middlegroundColor: #D1D1D1; --foregroundColor: #eaeaea; --hrefColor: #99c3ff; + --middlegroundHrefColor: #0065be; --uniformBottomMargin: 0.5rem; background-color: var(--backgroundColor); color: var(--foregroundColor); From 334c2fecb5611b1e7fbae7447d2d7b1f848312df Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 13 Jan 2026 21:47:52 -0600 Subject: [PATCH 06/15] Commit changes before fixing global.json file(s). --- .../new-dotnet-version_a1d9f5/assessment.csv | 2 + .../new-dotnet-version_a1d9f5/assessment.json | 222 ++++ .../new-dotnet-version_a1d9f5/assessment.md | 339 ++++++ .../new-dotnet-version_a1d9f5/plan.md | 1028 +++++++++++++++++ .../new-dotnet-version_a1d9f5/scenario.json | 12 + .../new-dotnet-version_a1d9f5/tasks.md | 59 + 6 files changed, 1662 insertions(+) create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json create mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv new file mode 100644 index 0000000..65de2eb --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv @@ -0,0 +1,2 @@ +Issue ID,Description,State,Severity,Story Points,Project Path,Location Kind,Path,Line,Column,Incident ID,Help Link,Assembly Name,Assembly Version,Assembly Public Key,Snippet +Project.0002,Project's target framework(s) needs to be changed,Active,Mandatory,1,Shogi.Database\Shogi.Database.sqlproj,File,Shogi.Database\Shogi.Database.sqlproj,,,,,,,,"Current target framework: .NETFramework,Version=v4.7.2 Recommended target framework: net10.0" diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json new file mode 100644 index 0000000..58e8d5c --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json @@ -0,0 +1,222 @@ +{ + "settings": { + "components": { + "code": true, + "binaries": false + }, + "targetId": "net10.0", + "targetDisplayName": ".NETCoreApp,Version=v10.0" + }, + "analysisStartTime": "2026-01-14T03:11:27.1112869Z", + "analysisEndTime": "2026-01-14T03:11:27.3394814Z", + "privacyModeHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2270980", + "stats": { + "summary": { + "projects": 5, + "issues": 1, + "incidents": 1, + "effort": 1 + }, + "charts": { + "severity": { + "Mandatory": 1, + "Optional": 0, + "Potential": 0, + "Information": 0 + }, + "category": { + "Project": 1 + } + } + }, + "projects": [ + { + "path": "BoardRules\\BoardRules.csproj", + "startingProject": true, + "issues": 0, + "storyPoints": 0, + "properties": { + "appName": "BoardRules", + "projectKind": "ClassLibrary", + "frameworks": [ + "net10.0" + ], + "languages": [ + "C#" + ], + "tools": [ + "MSBuild" + ], + "isSdkStyle": true, + "numberOfFiles": 1, + "numberOfCodeFiles": 1, + "linesTotal": 120, + "linesOfCode": 120, + "totalApiScanned": 0, + "minLinesOfCodeToChange": 0, + "maxLinesOfCodeToChange": 0 + }, + "ruleInstances": [], + "features": [] + }, + { + "path": "Tests\\AcceptanceTests\\Shogi.AcceptanceTests.csproj", + "startingProject": true, + "issues": 0, + "storyPoints": 0, + "properties": { + "appName": "Shogi.AcceptanceTests", + "projectKind": "DotNetCoreApp", + "frameworks": [ + "net10.0" + ], + "languages": [ + "C#" + ], + "tools": [ + "MSBuild" + ], + "isSdkStyle": true, + "numberOfFiles": 5, + "numberOfCodeFiles": 3, + "linesTotal": 3059, + "linesOfCode": 443, + "totalApiScanned": 0, + "minLinesOfCodeToChange": 0, + "maxLinesOfCodeToChange": 0 + }, + "ruleInstances": [], + "features": [] + }, + { + "path": "Shogi\\Shogi.csproj", + "startingProject": true, + "issues": 0, + "storyPoints": 0, + "properties": { + "appName": "Shogi", + "projectKind": "AspNetCore", + "frameworks": [ + "net10.0" + ], + "languages": [ + "C#" + ], + "tools": [ + "MSBuild" + ], + "isSdkStyle": true, + "numberOfFiles": 118, + "numberOfCodeFiles": 55, + "linesTotal": 10759, + "linesOfCode": 4232, + "totalApiScanned": 0, + "minLinesOfCodeToChange": 0, + "maxLinesOfCodeToChange": 0 + }, + "ruleInstances": [], + "features": [] + }, + { + "path": "Shogi.Database\\Shogi.Database.sqlproj", + "startingProject": true, + "issues": 1, + "storyPoints": 1, + "properties": { + "appName": "Shogi.Database", + "projectKind": "ClassicClassLibrary", + "frameworks": [ + "net472" + ], + "languages": [ + "" + ], + "tools": [ + "MSBuild" + ], + "isSdkStyle": false, + "numberOfFiles": 0, + "numberOfCodeFiles": 0, + "linesTotal": 0, + "linesOfCode": 0, + "totalApiScanned": 0, + "minLinesOfCodeToChange": 0, + "maxLinesOfCodeToChange": 0 + }, + "ruleInstances": [ + { + "incidentId": "a43e59f8-9932-42d1-991f-9653db2e0408", + "ruleId": "Project.0002", + "projectPath": "Shogi.Database\\Shogi.Database.sqlproj", + "state": "Active", + "location": { + "snippetModel": { + "unrestricted": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", + "protected": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0" + }, + "kind": "File", + "path": "Shogi.Database\\Shogi.Database.sqlproj", + "snippet": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", + "protectedSnippet": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", + "properties": { + "CurrentTargetFramework": ".NETFramework,Version=v4.7.2", + "RecommendedTargetFramework": "net10.0" + } + } + } + ], + "features": [] + }, + { + "path": "Tests\\UnitTests\\UnitTests.csproj", + "startingProject": true, + "issues": 0, + "storyPoints": 0, + "properties": { + "appName": "UnitTests", + "projectKind": "DotNetCoreApp", + "frameworks": [ + "net10.0" + ], + "languages": [ + "C#" + ], + "tools": [ + "MSBuild" + ], + "isSdkStyle": true, + "numberOfFiles": 7, + "numberOfCodeFiles": 5, + "linesTotal": 3574, + "linesOfCode": 958, + "totalApiScanned": 0, + "minLinesOfCodeToChange": 0, + "maxLinesOfCodeToChange": 0 + }, + "ruleInstances": [], + "features": [] + } + ], + "rules": { + "Project.0002": { + "id": "Project.0002", + "isFeature": false, + "description": "Project\u0027s target framework(s) needs to be changed to the new target framework that you selected for this upgrade.\n\nDuring upgrade target framework will be adjusted to corresponding platform when applicable. In some cases project would result in multiple target frameworks after the upgrade if it was using features that now have their own platforms in modern .NET frameworks (windows, iOS, Android etc).", + "label": "Project\u0027s target framework(s) needs to be changed", + "severity": "Mandatory", + "effort": 1, + "links": [ + { + "title": "Overview of porting from .NET Framework to .NET", + "url": "https://go.microsoft.com/fwlink/?linkid=2265227", + "isCustom": false + }, + { + "title": ".NET project SDKs", + "url": "https://go.microsoft.com/fwlink/?linkid=2265226", + "isCustom": false + } + ] + } + } +} \ No newline at end of file diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md new file mode 100644 index 0000000..204da56 --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md @@ -0,0 +1,339 @@ +# Projects and dependencies analysis + +This document provides a comprehensive overview of the projects and their dependencies in the context of upgrading to .NETCoreApp,Version=v10.0. + +## Table of Contents + +- [Executive Summary](#executive-Summary) + - [Highlevel Metrics](#highlevel-metrics) + - [Projects Compatibility](#projects-compatibility) + - [Package Compatibility](#package-compatibility) + - [API Compatibility](#api-compatibility) +- [Aggregate NuGet packages details](#aggregate-nuget-packages-details) +- [Top API Migration Challenges](#top-api-migration-challenges) + - [Technologies and Features](#technologies-and-features) + - [Most Frequent API Issues](#most-frequent-api-issues) +- [Projects Relationship Graph](#projects-relationship-graph) +- [Project Details](#project-details) + + - [BoardRules\BoardRules.csproj](#boardrulesboardrulescsproj) + - [Shogi.Database\Shogi.Database.sqlproj](#shogidatabaseshogidatabasesqlproj) + - [Shogi\Shogi.csproj](#shogishogicsproj) + - [Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) + - [Tests\UnitTests\UnitTests.csproj](#testsunittestsunittestscsproj) + + +## Executive Summary + +### Highlevel Metrics + +| Metric | Count | Status | +| :--- | :---: | :--- | +| Total Projects | 5 | 1 require upgrade | +| Total NuGet Packages | 14 | All compatible | +| Total Code Files | 64 | | +| Total Code Files with Incidents | 1 | | +| Total Lines of Code | 5753 | | +| Total Number of Issues | 1 | | +| Estimated LOC to modify | 0+ | at least 0.0% of codebase | + +### Projects Compatibility + +| Project | Target Framework | Difficulty | Package Issues | API Issues | Est. LOC Impact | Description | +| :--- | :---: | :---: | :---: | :---: | :---: | :--- | +| [BoardRules\BoardRules.csproj](#boardrulesboardrulescsproj) | net10.0 | ✅ None | 0 | 0 | | ClassLibrary, Sdk Style = True | +| [Shogi.Database\Shogi.Database.sqlproj](#shogidatabaseshogidatabasesqlproj) | net472 | 🟢 Low | 0 | 0 | | ClassicClassLibrary, Sdk Style = False | +| [Shogi\Shogi.csproj](#shogishogicsproj) | net10.0 | ✅ None | 0 | 0 | | AspNetCore, Sdk Style = True | +| [Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) | net10.0 | ✅ None | 0 | 0 | | DotNetCoreApp, Sdk Style = True | +| [Tests\UnitTests\UnitTests.csproj](#testsunittestsunittestscsproj) | net10.0 | ✅ None | 0 | 0 | | DotNetCoreApp, Sdk Style = True | + +### Package Compatibility + +| Status | Count | Percentage | +| :--- | :---: | :---: | +| ✅ Compatible | 14 | 100.0% | +| ⚠️ Incompatible | 0 | 0.0% | +| 🔄 Upgrade Recommended | 0 | 0.0% | +| ***Total NuGet Packages*** | ***14*** | ***100%*** | + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + +## Aggregate NuGet packages details + +| Package | Current Version | Suggested Version | Projects | Description | +| :--- | :---: | :---: | :--- | :--- | +| coverlet.collector | 6.0.4 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | +| Dapper | 2.1.66 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| FluentValidation | 12.1.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) | ✅Compatible | +| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.EntityFrameworkCore.Design | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| Microsoft.NET.Test.Sdk | 18.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | +| Swashbuckle.AspNetCore | 10.1.0 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| System.Data.SqlClient | 4.9.0 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | +| xunit | 2.9.3 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | +| xunit.runner.visualstudio | 3.1.5 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | + +## Top API Migration Challenges + +### Technologies and Features + +| Technology | Issues | Percentage | Migration Path | +| :--- | :---: | :---: | :--- | + +### Most Frequent API Issues + +| API | Count | Percentage | Category | +| :--- | :---: | :---: | :--- | + +## Projects Relationship Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart LR + P1["⚙️ Shogi.Database.sqlproj
net472"] + P2["📦 BoardRules.csproj
net10.0"] + P3["📦 Shogi.AcceptanceTests.csproj
net10.0"] + P4["📦 UnitTests.csproj
net10.0"] + P5["📦 Shogi.csproj
net10.0"] + P3 --> P5 + P4 --> P5 + click P1 "#shogidatabaseshogidatabasesqlproj" + click P2 "#boardrulesboardrulescsproj" + click P3 "#testsacceptancetestsshogiacceptancetestscsproj" + click P4 "#testsunittestsunittestscsproj" + click P5 "#shogishogicsproj" + +``` + +## Project Details + + +### BoardRules\BoardRules.csproj + +#### Project Info + +- **Current Target Framework:** net10.0✅ +- **SDK-style**: True +- **Project Kind:** ClassLibrary +- **Dependencies**: 0 +- **Dependants**: 0 +- **Number of Files**: 1 +- **Lines of Code**: 120 +- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) + +#### Dependency Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart TB + subgraph current["BoardRules.csproj"] + MAIN["📦 BoardRules.csproj
net10.0"] + click MAIN "#boardrulesboardrulescsproj" + end + +``` + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + + +### Shogi.Database\Shogi.Database.sqlproj + +#### Project Info + +- **Current Target Framework:** net472 +- **Proposed Target Framework:** net10.0 +- **SDK-style**: False +- **Project Kind:** ClassicClassLibrary +- **Dependencies**: 0 +- **Dependants**: 0 +- **Number of Files**: 0 +- **Number of Files with Incidents**: 1 +- **Lines of Code**: 0 +- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) + +#### Dependency Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart TB + subgraph current["Shogi.Database.sqlproj"] + MAIN["⚙️ Shogi.Database.sqlproj
net472"] + click MAIN "#shogidatabaseshogidatabasesqlproj" + end + +``` + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + + +### Shogi\Shogi.csproj + +#### Project Info + +- **Current Target Framework:** net10.0✅ +- **SDK-style**: True +- **Project Kind:** AspNetCore +- **Dependencies**: 0 +- **Dependants**: 2 +- **Number of Files**: 118 +- **Lines of Code**: 4232 +- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) + +#### Dependency Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart TB + subgraph upstream["Dependants (2)"] + P3["📦 Shogi.AcceptanceTests.csproj
net10.0"] + P4["📦 UnitTests.csproj
net10.0"] + click P3 "#testsacceptancetestsshogiacceptancetestscsproj" + click P4 "#testsunittestsunittestscsproj" + end + subgraph current["Shogi.csproj"] + MAIN["📦 Shogi.csproj
net10.0"] + click MAIN "#shogishogicsproj" + end + P3 --> MAIN + P4 --> MAIN + +``` + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + + +### Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj + +#### Project Info + +- **Current Target Framework:** net10.0✅ +- **SDK-style**: True +- **Project Kind:** DotNetCoreApp +- **Dependencies**: 1 +- **Dependants**: 0 +- **Number of Files**: 5 +- **Lines of Code**: 443 +- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) + +#### Dependency Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart TB + subgraph current["Shogi.AcceptanceTests.csproj"] + MAIN["📦 Shogi.AcceptanceTests.csproj
net10.0"] + click MAIN "#testsacceptancetestsshogiacceptancetestscsproj" + end + subgraph downstream["Dependencies (1"] + P5["📦 Shogi.csproj
net10.0"] + click P5 "#shogishogicsproj" + end + MAIN --> P5 + +``` + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + + +### Tests\UnitTests\UnitTests.csproj + +#### Project Info + +- **Current Target Framework:** net10.0✅ +- **SDK-style**: True +- **Project Kind:** DotNetCoreApp +- **Dependencies**: 1 +- **Dependants**: 0 +- **Number of Files**: 7 +- **Lines of Code**: 958 +- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) + +#### Dependency Graph + +Legend: +📦 SDK-style project +⚙️ Classic project + +```mermaid +flowchart TB + subgraph current["UnitTests.csproj"] + MAIN["📦 UnitTests.csproj
net10.0"] + click MAIN "#testsunittestsunittestscsproj" + end + subgraph downstream["Dependencies (1"] + P5["📦 Shogi.csproj
net10.0"] + click P5 "#shogishogicsproj" + end + MAIN --> P5 + +``` + +### API Compatibility + +| Category | Count | Impact | +| :--- | :---: | :--- | +| 🔴 Binary Incompatible | 0 | High - Require code changes | +| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | +| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | +| ✅ Compatible | 0 | | +| ***Total APIs Analyzed*** | ***0*** | | + diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md new file mode 100644 index 0000000..f0e078e --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md @@ -0,0 +1,1028 @@ +# .NET 10.0 Upgrade Plan + +## Table of Contents + +- [Executive Summary](#executive-summary) +- [Migration Strategy](#migration-strategy) +- [Detailed Dependency Analysis](#detailed-dependency-analysis) +- [Project-by-Project Migration Plans](#project-by-project-migration-plans) +- [Package Update Reference](#package-update-reference) +- [Breaking Changes Catalog](#breaking-changes-catalog) +- [Testing & Validation Strategy](#testing--validation-strategy) +- [Risk Management](#risk-management) +- [Complexity & Effort Assessment](#complexity--effort-assessment) +- [Source Control Strategy](#source-control-strategy) +- [Success Criteria](#success-criteria) + +--- + +## Executive Summary + +### Scenario Description + +Upgrade Shogi solution from .NET 8.0 to **.NET 10.0 (Long Term Support)**. The solution consists of 5 projects, with 4 .NET projects already targeting net10.0 and 1 SQL database project remaining on .NET Framework 4.7.2. + +### Scope + +**Projects in Scope:** +- **BoardRules\BoardRules.csproj** - Class library, already on net10.0 ✅ +- **Shogi\Shogi.csproj** - ASP.NET Core web application, already on net10.0 ✅ +- **Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj** - Test project, already on net10.0 ✅ +- **Tests\UnitTests\UnitTests.csproj** - Test project, already on net10.0 ✅ +- **Shogi.Database\Shogi.Database.sqlproj** - SQL database project, remains on net472 (as expected) + +**Current State:** +- 4 of 5 .NET projects already targeting net10.0 +- All 14 NuGet packages already compatible with .NET 10.0 +- No security vulnerabilities detected +- Total codebase: 5,753 lines of code across 64 files + +**Target State:** +- Confirm all .NET projects are on net10.0 +- Verify all packages are compatible +- Validate solution builds and all tests pass + +### Selected Strategy + +**All-At-Once Strategy** - All projects are already upgraded simultaneously. + +**Rationale:** +- **Small solution** - Only 5 projects +- **Already migrated** - 4 projects already on net10.0 +- **Simple dependency structure** - Clear hierarchy with no circular dependencies +- **All packages compatible** - All 14 NuGet packages support .NET 10.0 +- **Zero risk** - No breaking changes, no security issues +- **Verification focus** - Primary task is to validate current state, not perform migration + +### Complexity Assessment + +**Discovered Metrics:** +- **Project count**: 5 (4 .NET projects + 1 SQL database project) +- **Dependency depth**: 1 (test projects depend on main Shogi project) +- **High-risk projects**: 0 +- **Security vulnerabilities**: 0 +- **Package updates required**: 0 (all already compatible) +- **Lines of code**: 5,753 +- **API compatibility issues**: 0 + +**Classification**: **Simple** - Verification scenario + +**Critical Issues**: None + +### Recommended Approach + +**Phase 1: Verification** - Confirm projects are on net10.0, packages are compatible +**Phase 2: Build & Test** - Ensure solution builds and all tests pass +**Phase 3: Documentation** - Update any remaining documentation references + +This is primarily a **validation exercise** rather than an active migration, as the solution appears to have already been upgraded to .NET 10.0. + +### Iteration Strategy + +**Fast Verification Approach** (2-3 iterations): +- One iteration for all project details (since they're already upgraded) +- One iteration for validation and success criteria + +--- + +## Migration Strategy + +### Approach Selection + +**Selected: All-At-Once Strategy (Verification Mode)** + +This scenario is unique - it's not a traditional migration but a **verification** of an already-completed upgrade. All .NET projects already target net10.0. + +### Strategy Rationale + +**Why All-At-Once (Already Complete):** + +1. **Small Solution** - Only 5 projects total (4 .NET projects) +2. **Already Migrated** - All .NET projects already on net10.0 +3. **All Packages Compatible** - All 14 NuGet packages already support .NET 10.0 +4. **No Breaking Changes** - Zero API compatibility issues detected +5. **Simple Dependencies** - Clear hierarchy, no circular dependencies +6. **Low Risk** - No security vulnerabilities, no code changes required + +### All-At-Once Strategy Considerations + +**Simultaneity**: Not applicable - upgrade already performed. Focus is on validation. + +**Atomic Operation**: The original upgrade appears to have been completed atomically. Our task is to verify this state. + +**No Intermediate States**: Already achieved - all projects are on target framework. + +### Dependency-Based Ordering + +While not needed for migration, validation will follow dependency order: + +**Validation Order:** +1. **Standalone projects first**: BoardRules (no dependencies) +2. **Main application**: Shogi.csproj (depends on nothing within solution) +3. **Test projects last**: AcceptanceTests, UnitTests (depend on Shogi.csproj) + +This ensures if issues exist, they're discovered at the lowest dependency level first. + +### Parallel vs Sequential Execution + +**Validation Approach**: Sequential validation following dependency order ensures clear error isolation. However, since all projects are already upgraded, parallel validation would also be safe. + +**Recommended**: Sequential for clarity, parallel acceptable for speed. + +### Phase Definitions + +**Phase 1: Project File Verification** +- Confirm all .NET projects have `net10.0` +- Verify Shogi.Database.sqlproj correctly remains on net472 +- Validate package references are compatible versions + +**Phase 2: Build Validation** +- Restore NuGet packages +- Build entire solution +- Confirm zero errors, zero warnings + +**Phase 3: Test Validation** +- Run acceptance tests +- Run unit tests +- Confirm all tests pass + +**Phase 4: Documentation** +- Update any deployment documentation +- Confirm README reflects .NET 10.0 +- Document verification results + +--- + +## Detailed Dependency Analysis + +### Dependency Graph Summary + +The solution has a simple, clean dependency structure: + +``` +BoardRules.csproj (standalone, no dependencies) + ↓ +Shogi.csproj (main ASP.NET Core application) + ↓ +├── Shogi.AcceptanceTests.csproj (acceptance tests) +└── UnitTests.csproj (unit tests) + +Shogi.Database.sqlproj (standalone SQL database project) +``` + +**Dependency Characteristics:** +- **Depth**: 1 level (test projects depend on main project) +- **Circular dependencies**: None +- **Standalone projects**: 2 (BoardRules, Shogi.Database) +- **Leaf nodes**: 2 (test projects) +- **Root node**: 1 (Shogi.csproj) + +### Project Groupings by Migration Phase + +Since all .NET projects are already on net10.0, grouping is for validation purposes only: + +**Group 1: Standalone Libraries** +- BoardRules\BoardRules.csproj (already net10.0) +- Shogi.Database\Shogi.Database.sqlproj (remains net472 - SQL database project) + +**Group 2: Main Application** +- Shogi\Shogi.csproj (already net10.0) + +**Group 3: Test Projects** +- Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj (already net10.0) +- Tests\UnitTests\UnitTests.csproj (already net10.0) + +### Critical Path Identification + +**No critical path** - All projects are already on target framework. The validation sequence is: + +1. Verify standalone libraries (BoardRules) +2. Verify main application (Shogi) +3. Verify test projects build and pass + +### Circular Dependency Analysis + +**No circular dependencies detected** - Clean dependency tree. + +--- + +## Project-by-Project Migration Plans + +### Project: BoardRules\BoardRules.csproj + +**Current State**: +- **Target Framework**: net10.0 ✅ +- **SDK-Style**: True +- **Project Type**: Class Library +- **Dependencies**: 0 project dependencies, 0 NuGet packages +- **Dependants**: 0 (standalone library) +- **Lines of Code**: 120 +- **Risk Level**: None + +**Target State**: +- **Target Framework**: net10.0 (already achieved) +- **Package Count**: 0 (unchanged) + +**Migration Steps**: ✅ Already Complete + +This project is already on net10.0. No migration actions required. + +**Validation Steps**: +1. ✅ Verify project file contains `net10.0` +2. ✅ Build project independently +3. ✅ Confirm zero build errors +4. ✅ Confirm zero build warnings + +**Expected Breaking Changes**: None - project already on target framework + +**Code Modifications**: None required + +**Testing Strategy**: +- Build verification only (no test project exists for BoardRules) +- Confirm no compilation errors +- Verify as part of solution-wide build + +**Validation Checklist**: +- [ ] Project builds successfully +- [ ] No build warnings +- [ ] No package dependency conflicts +- [ ] Compatible with dependent projects (none currently) + +--- + +### Project: Shogi\Shogi.csproj + +**Current State**: +- **Target Framework**: net10.0 ✅ +- **SDK-Style**: True +- **Project Type**: ASP.NET Core Web Application +- **Dependencies**: 0 project dependencies (within solution) +- **Dependants**: 2 (Shogi.AcceptanceTests, UnitTests) +- **NuGet Packages**: 12 packages, all compatible with net10.0 +- **Lines of Code**: 4,232 +- **Risk Level**: None + +**Target State**: +- **Target Framework**: net10.0 (already achieved) +- **Package Count**: 12 (all already compatible) + +**Migration Steps**: ✅ Already Complete + +This project is already on net10.0 with all packages at compatible versions. + +**Package Updates**: None required - all packages already compatible + +| Package | Current Version | Status | +|---------|----------------|---------| +| Dapper | 2.1.66 | ✅ Compatible | +| FluentValidation | 12.1.1 | ✅ Compatible | +| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | ✅ Compatible | +| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | ✅ Compatible | +| Microsoft.EntityFrameworkCore.Design | 10.0.1 | ✅ Compatible | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | ✅ Compatible | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | ✅ Compatible | +| Swashbuckle.AspNetCore | 10.1.0 | ✅ Compatible | +| System.Data.SqlClient | 4.9.0 | ✅ Compatible | + +**Expected Breaking Changes**: None - project already on target framework, packages already compatible + +**Code Modifications**: None required + +**Configuration Updates**: None required - ASP.NET Core configuration already compatible + +**Testing Strategy**: +- Build project successfully +- Execute acceptance tests (see Shogi.AcceptanceTests project) +- Execute unit tests (see UnitTests project) +- Verify application startup (if applicable) + +**Validation Checklist**: +- [ ] Project builds successfully +- [ ] No build warnings +- [ ] All package dependencies restore successfully +- [ ] Entity Framework migrations compatible (if any) +- [ ] Acceptance tests pass +- [ ] Unit tests pass +- [ ] SignalR functionality works (if used) +- [ ] Identity/authentication works (if used) + +--- + +### Project: Shogi.Database\Shogi.Database.sqlproj + +**Current State**: +- **Target Framework**: net472 +- **SDK-Style**: False +- **Project Type**: SQL Database Project (Classic) +- **Dependencies**: 0 project dependencies +- **Dependants**: 0 +- **Lines of Code**: 0 (SQL project) +- **Risk Level**: None + +**Target State**: +- **Target Framework**: net472 (unchanged - SQL database projects remain on .NET Framework) +- **No migration required** + +**Migration Steps**: ⚠️ No Migration Required + +SQL Database projects (.sqlproj) are build-time tools that generate database schema scripts. They typically remain on .NET Framework and do not need to be upgraded to .NET Core/.NET 10.0. + +**Rationale for Not Upgrading**: +- SQL database projects are MSBuild-based schema definition tools +- They generate T-SQL scripts, not runtime executables +- .NET Framework 4.7.2 is appropriate for these projects +- No compatibility issues with the rest of the solution being on .NET 10.0 + +**Validation Steps**: +1. ✅ Confirm project remains on net472 +2. ✅ Verify project builds successfully +3. ✅ Confirm database scripts generate correctly + +**Expected Breaking Changes**: None + +**Code Modifications**: None + +**Testing Strategy**: +- Build verification only +- Confirm SQL scripts generate without errors +- Verify compatibility with SQL Server deployment targets + +**Validation Checklist**: +- [ ] Project builds successfully on net472 +- [ ] No build warnings +- [ ] Database schema scripts generate correctly +- [ ] No impact on .NET 10.0 projects in solution + +--- + +### Project: Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj + +**Current State**: +- **Target Framework**: net10.0 ✅ +- **SDK-Style**: True +- **Project Type**: xUnit Test Project (.NET Core) +- **Dependencies**: 1 project (Shogi.csproj) +- **Dependants**: 0 +- **NuGet Packages**: 6 packages, all compatible with net10.0 +- **Lines of Code**: 443 +- **Risk Level**: None + +**Target State**: +- **Target Framework**: net10.0 (already achieved) +- **Package Count**: 6 (all already compatible) + +**Migration Steps**: ✅ Already Complete + +This project is already on net10.0 with all test packages at compatible versions. + +**Package Updates**: None required - all packages already compatible + +| Package | Current Version | Status | +|---------|----------------|---------| +| coverlet.collector | 6.0.4 | ✅ Compatible | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | ✅ Compatible | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | ✅ Compatible | +| Microsoft.NET.Test.Sdk | 18.0.1 | ✅ Compatible | +| xunit | 2.9.3 | ✅ Compatible | +| xunit.runner.visualstudio | 3.1.5 | ✅ Compatible | + +**Expected Breaking Changes**: None - project already on target framework + +**Code Modifications**: None required + +**Testing Strategy**: +- Build test project successfully +- Execute all acceptance tests +- Verify test discovery works correctly +- Confirm code coverage collection works (coverlet.collector) +- Validate ASP.NET Core test host compatibility (Microsoft.AspNetCore.Mvc.Testing) + +**Validation Checklist**: +- [ ] Test project builds successfully +- [ ] No build warnings +- [ ] All tests are discovered +- [ ] All tests execute +- [ ] All tests pass +- [ ] Code coverage reporting works +- [ ] In-memory database tests function correctly + +--- + +### Project: Tests\UnitTests\UnitTests.csproj + +**Current State**: +- **Target Framework**: net10.0 ✅ +- **SDK-Style**: True +- **Project Type**: xUnit Test Project (.NET Core) +- **Dependencies**: 1 project (Shogi.csproj) +- **Dependants**: 0 +- **NuGet Packages**: 4 packages, all compatible with net10.0 +- **Lines of Code**: 958 +- **Risk Level**: None + +**Target State**: +- **Target Framework**: net10.0 (already achieved) +- **Package Count**: 4 (all already compatible) + +**Migration Steps**: ✅ Already Complete + +This project is already on net10.0 with all test packages at compatible versions. + +**Package Updates**: None required - all packages already compatible + +| Package | Current Version | Status | +|---------|----------------|---------| +| coverlet.collector | 6.0.4 | ✅ Compatible | +| Microsoft.NET.Test.Sdk | 18.0.1 | ✅ Compatible | +| xunit | 2.9.3 | ✅ Compatible | +| xunit.runner.visualstudio | 3.1.5 | ✅ Compatible | + +**Expected Breaking Changes**: None - project already on target framework + +**Code Modifications**: None required + +**Testing Strategy**: +- Build test project successfully +- Execute all unit tests +- Verify test discovery works correctly +- Confirm code coverage collection works (coverlet.collector) + +**Validation Checklist**: +- [ ] Test project builds successfully +- [ ] No build warnings +- [ ] All tests are discovered +- [ ] All tests execute +- [ ] All tests pass +- [ ] Code coverage reporting works + +--- + +## Package Update Reference + +### Summary + +**All packages are already compatible with .NET 10.0.** No package updates are required as part of this verification exercise. + +### Complete Package Matrix + +| Package | Current Version | Target Version | Projects | Update Reason | +|---------|----------------|----------------|----------|---------------| +| coverlet.collector | 6.0.4 | 6.0.4 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | +| Dapper | 2.1.66 | 2.1.66 (no change) | Shogi | ✅ Already compatible | +| FluentValidation | 12.1.1 | 12.1.1 (no change) | Shogi | ✅ Already compatible | +| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | 10.0.1 (no change) | AcceptanceTests | ✅ Already compatible | +| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | +| Microsoft.EntityFrameworkCore.Design | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | 10.0.1 (no change) | AcceptanceTests, Shogi | ✅ Already compatible | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | +| Microsoft.NET.Test.Sdk | 18.0.1 | 18.0.1 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | +| Swashbuckle.AspNetCore | 10.1.0 | 10.1.0 (no change) | Shogi | ✅ Already compatible | +| System.Data.SqlClient | 4.9.0 | 4.9.0 (no change) | Shogi | ✅ Already compatible | +| xunit | 2.9.3 | 2.9.3 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | +| xunit.runner.visualstudio | 3.1.5 | 3.1.5 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | + +### Package Category Analysis + +**ASP.NET Core Packages** (already at .NET 10.0 versions): +- Microsoft.AspNetCore.Identity.EntityFrameworkCore: 10.0.1 +- Microsoft.AspNetCore.Mvc.Testing: 10.0.1 +- Microsoft.AspNetCore.SignalR.Client: 10.0.1 + +**Entity Framework Core Packages** (already at .NET 10.0 versions): +- Microsoft.EntityFrameworkCore.Design: 10.0.1 +- Microsoft.EntityFrameworkCore.InMemory: 10.0.1 +- Microsoft.EntityFrameworkCore.SqlServer: 10.0.1 + +**Testing Packages** (already at compatible versions): +- Microsoft.NET.Test.Sdk: 18.0.1 +- xunit: 2.9.3 +- xunit.runner.visualstudio: 3.1.5 +- coverlet.collector: 6.0.4 + +**Utility Packages** (already at compatible versions): +- Dapper: 2.1.66 +- FluentValidation: 12.1.1 +- Swashbuckle.AspNetCore: 10.1.0 +- System.Data.SqlClient: 4.9.0 + +### Security Assessment + +**No security vulnerabilities detected** in any packages. All packages are at secure, compatible versions for .NET 10.0. + +--- + +## Breaking Changes Catalog + +### Summary + +**No breaking changes detected.** All projects are already on the target framework (net10.0) and all packages are at compatible versions. + +### Framework Breaking Changes + +**From .NET 8.0 to .NET 10.0**: Not applicable - projects already on net10.0. + +If projects had been migrated from .NET 8.0 to .NET 10.0, the following categories would typically be reviewed: +- ASP.NET Core API changes (already handled) +- Entity Framework Core API changes (already handled) +- BCL (Base Class Library) changes (already handled) +- Runtime behavior changes (already handled) + +### Package-Specific Breaking Changes + +**No package breaking changes** - all packages are already at .NET 10.0-compatible versions. + +### Configuration Changes + +**No configuration changes required** - ASP.NET Core configuration is already compatible with .NET 10.0. + +### Known Issues + +**None identified** during assessment. All API compatibility checks passed. + +### Areas Requiring Special Attention + +Since this is a verification scenario, the following areas should be validated to ensure no issues were missed: + +1. **Entity Framework Migrations**: Verify all EF migrations are compatible and functional +2. **SignalR Implementation**: Confirm SignalR hubs and clients work correctly +3. **Identity/Authentication**: Validate authentication and authorization flows +4. **Dependency Injection**: Ensure all service registrations are correct +5. **Middleware Pipeline**: Verify middleware order and functionality + +### Breaking Changes by Project + +**BoardRules.csproj**: None +**Shogi.csproj**: None +**Shogi.Database.sqlproj**: None (not migrating) +**Shogi.AcceptanceTests.csproj**: None +**UnitTests.csproj**: None + +--- + +## Testing & Validation Strategy + +### Overview + +Testing strategy focuses on **verification** rather than migration validation, since all projects are already on the target framework. + +### Multi-Level Testing + +#### Phase 1: Project-Level Verification + +**For Each Project:** + +1. **BoardRules.csproj** + - [ ] Project file verification: `net10.0` exists + - [ ] Builds without errors + - [ ] Builds without warnings + - [ ] No package dependency conflicts + +2. **Shogi.csproj** + - [ ] Project file verification: `net10.0` exists + - [ ] All 12 packages restore successfully + - [ ] Builds without errors + - [ ] Builds without warnings + - [ ] Entity Framework migrations are intact + - [ ] Configuration files are valid + +3. **Shogi.Database.sqlproj** + - [ ] Project file verification: remains on net472 + - [ ] Builds without errors + - [ ] Database scripts generate successfully + +4. **Shogi.AcceptanceTests.csproj** + - [ ] Project file verification: `net10.0` exists + - [ ] All 6 test packages restore successfully + - [ ] Builds without errors + - [ ] All tests are discovered + - [ ] Tests can execute (actual pass/fail checked in Phase 3) + +5. **UnitTests.csproj** + - [ ] Project file verification: `net10.0` exists + - [ ] All 4 test packages restore successfully + - [ ] Builds without errors + - [ ] All tests are discovered + - [ ] Tests can execute (actual pass/fail checked in Phase 3) + +#### Phase 2: Solution-Level Verification + +**Complete Solution Build:** +- [ ] `dotnet restore` succeeds for entire solution +- [ ] `dotnet build` succeeds for entire solution +- [ ] Zero compilation errors across all projects +- [ ] Zero compilation warnings across all projects +- [ ] All project references resolve correctly +- [ ] All package dependencies resolve correctly + +#### Phase 3: Functional Testing + +**Acceptance Tests:** +- [ ] All acceptance tests execute +- [ ] All acceptance tests pass +- [ ] ASP.NET Core test host initializes correctly +- [ ] In-memory database tests function correctly +- [ ] Integration scenarios work as expected + +**Unit Tests:** +- [ ] All unit tests execute +- [ ] All unit tests pass +- [ ] Code coverage collection works (if enabled) +- [ ] Test isolation is maintained + +**Application Verification** (if applicable): +- [ ] Application starts successfully +- [ ] API endpoints are accessible +- [ ] Database connectivity works +- [ ] SignalR functionality operates correctly (if used) +- [ ] Identity/authentication flows work (if used) +- [ ] Swagger/OpenAPI documentation generates correctly + +#### Phase 4: Deployment Verification (if applicable) + +- [ ] Application can be published (`dotnet publish`) +- [ ] Published output is complete +- [ ] Published application runs in deployment environment +- [ ] Configuration transformations work correctly + +### Test Execution Order + +1. **Standalone projects first**: BoardRules, Shogi.Database +2. **Main application**: Shogi +3. **Test projects**: AcceptanceTests, UnitTests + +This order follows dependency hierarchy to catch issues early. + +### Success Criteria for Testing + +**Build Success:** +- All 5 projects build without errors +- Zero build warnings +- All package references restore successfully + +**Test Success:** +- All acceptance tests pass (100%) +- All unit tests pass (100%) +- Code coverage maintains or improves previous levels + +**Functional Success:** +- Application starts and runs correctly +- All major features function as expected +- No runtime exceptions related to framework version + +### Testing Tools + +- **Build**: `dotnet build` (or Visual Studio) +- **Test Execution**: `dotnet test` (or Visual Studio Test Explorer) +- **Code Coverage**: coverlet.collector (already configured) +- **Package Management**: `dotnet restore` or NuGet Package Manager + +### Regression Testing + +Since projects are already upgraded, regression testing ensures no functionality was lost: + +- Review test results history (if available) +- Compare current test pass rate with previous runs +- Validate critical business scenarios +- Check performance benchmarks (if established) + +--- + +## Risk Management + +### High-Level Risk Assessment + +**Overall Risk Level**: **Very Low** + +All .NET projects are already on the target framework (net10.0), all packages are compatible, and no security vulnerabilities were detected. This is a verification exercise, not an active migration. + +### Risk Analysis + +| Project | Risk Level | Description | Mitigation | +|---------|-----------|-------------|------------| +| BoardRules.csproj | ✅ None | Already on net10.0, standalone library, 120 LOC | Verify builds without errors | +| Shogi.csproj | ✅ None | Already on net10.0, all packages compatible, 4,232 LOC | Build and run acceptance/unit tests | +| Shogi.Database.sqlproj | ✅ None | Remains on net472 as expected for SQL database projects | No action required | +| Shogi.AcceptanceTests.csproj | ✅ None | Already on net10.0, test project, 443 LOC | Execute tests to confirm functionality | +| UnitTests.csproj | ✅ None | Already on net10.0, test project, 958 LOC | Execute tests to confirm functionality | + +### Security Vulnerabilities + +**No security vulnerabilities detected** across all NuGet packages. All packages are at compatible, secure versions for .NET 10.0. + +### Contingency Plans + +Since this is a verification scenario with minimal risk, contingency planning is straightforward: + +**If build fails:** +- Review build errors for any environmental issues (SDK version, tooling) +- Confirm .NET 10.0 SDK is properly installed +- Check for missing NuGet package sources + +**If tests fail:** +- Isolate failing tests to determine if related to framework version +- Review test execution environment +- Check for test infrastructure compatibility with .NET 10.0 + +**If package compatibility issues arise:** +- Re-assess package versions (assessment shows all compatible) +- Check for package source configuration issues +- Verify NuGet cache is not corrupted + +### Rollback Plan + +**Not applicable** - Since projects are already on net10.0, there is no migration to roll back. If issues are discovered, they would be addressed in-place rather than through rollback. + +--- + +## Complexity & Effort Assessment + +### Per-Project Complexity + +| Project | Complexity | Dependencies | Risk | LOC | Notes | +|---------|-----------|--------------|------|-----|-------| +| BoardRules.csproj | Low | 0 | None | 120 | Standalone library, already upgraded | +| Shogi.csproj | Low | 0 | None | 4,232 | Main app, already upgraded, all packages compatible | +| Shogi.Database.sqlproj | Low | 0 | None | 0 | SQL database project, no migration needed | +| Shogi.AcceptanceTests.csproj | Low | 1 | None | 443 | Test project, already upgraded | +| UnitTests.csproj | Low | 1 | None | 958 | Test project, already upgraded | + +**Overall Complexity**: **Low** - This is a verification scenario, not an active migration. + +### Phase Complexity Assessment + +**Phase 1: Project File Verification** +- **Complexity**: Low +- **Effort**: Minimal - automated verification of TargetFramework properties +- **Dependencies**: None + +**Phase 2: Build Validation** +- **Complexity**: Low +- **Effort**: Minimal - standard build process +- **Dependencies**: Phase 1 complete + +**Phase 3: Test Validation** +- **Complexity**: Low +- **Effort**: Minimal - execute existing test suites +- **Dependencies**: Phase 2 complete (solution must build) + +**Phase 4: Documentation** +- **Complexity**: Low +- **Effort**: Minimal - update deployment docs if needed +- **Dependencies**: Phase 3 complete (tests must pass) + +### Resource Requirements + +**Skill Levels Required:** +- **Basic .NET knowledge** - Sufficient for verification activities +- **Build/test execution** - Standard CI/CD skills +- **No advanced migration expertise needed** - Projects already upgraded + +**Parallel Capacity:** +- **Not applicable** - Verification is lightweight and can be done sequentially +- All validation activities can be performed by a single resource + +### Total Effort Estimate + +**Relative Complexity**: **Low** + +**Effort Distribution:** +- Project file verification: Low +- Build validation: Low +- Test validation: Low +- Documentation updates: Low + +**Note**: No real-time estimates provided. This is a verification exercise with minimal actual work required. The primary task is confirming that the existing state is correct and functional. + +--- + +## Source Control Strategy + +### Overview + +Since this is a **verification exercise** rather than an active migration, source control strategy focuses on documenting the current state and ensuring any validation findings are properly tracked. + +### Branching Strategy + +**Current Branch**: `ssr` (as requested by user) + +**Approach**: Work directly on the `ssr` branch, as no destructive changes are anticipated. + +**Rationale**: +- Projects are already upgraded to net10.0 +- Verification activities do not modify code +- Any documentation updates are additive, not breaking +- Low risk of needing to revert changes + +**Alternative Approach** (if preferred): +- Create a verification branch from `ssr` +- Perform all validation activities +- Merge back to `ssr` after successful verification +- This provides isolation but adds overhead for minimal benefit + +### Commit Strategy + +**Approach**: Single commit after successful verification + +**Recommended Commit Message**: +``` +Verify .NET 10.0 upgrade - all projects confirmed on target framework + +- Confirmed all .NET projects targeting net10.0 +- Verified all 14 packages compatible with .NET 10.0 +- Built solution successfully with zero errors/warnings +- Executed all tests (acceptance + unit) - all passing +- Validated Shogi.Database.sqlproj remains on net472 as expected +- No security vulnerabilities detected +- All-At-Once strategy already completed + +Assessment: Low complexity, verification complete +``` + +**Alternative**: Multiple commits if validation reveals issues requiring fixes: +- Commit 1: "Verify project files and target frameworks" +- Commit 2: "Fix [specific issue] discovered during validation" +- Commit 3: "Confirm all tests pass on .NET 10.0" + +### Code Review and Merge Process + +**Review Requirements**: Minimal, since no code changes are expected + +**Checklist for Review** (if formal review required): +- [ ] All projects verified on correct target framework +- [ ] Build succeeds with zero errors/warnings +- [ ] All tests pass (acceptance + unit) +- [ ] Documentation updated (if applicable) +- [ ] No new security vulnerabilities introduced +- [ ] Verification results documented + +**Merge Criteria**: +- All validation checkpoints passed +- Tests are green +- No blocking issues discovered + +### Repository State + +**Current State** (as noted by user): +- **Branch**: `ssr` +- **Pending Changes**: Exist (user requested to proceed without git operations) + +**Recommendation**: +Since user requested to skip git operations, work can proceed directly. However, best practice would be to: +1. Commit or stash pending changes before verification +2. Perform verification activities +3. Document results +4. Commit verification results separately + +**User Preference**: Proceed without git operations - verification can be performed on current state. + +### Documentation Updates + +**Files to Update** (if applicable): +- README.md: Confirm .NET 10.0 is documented as target framework +- CHANGELOG.md: Add entry for .NET 10.0 verification +- Deployment documentation: Update any .NET version references +- CI/CD configuration: Ensure build pipelines target .NET 10.0 SDK + +### All-At-Once Strategy Source Control Guidance + +Since the All-At-Once upgrade appears to have already been completed (all projects simultaneously updated to net10.0), the source control strategy is retrospective: + +**Original Migration** (already completed): +- Appears to have been done atomically (all projects upgraded together) +- Single-commit approach was likely used +- All package updates applied simultaneously +- Clean result with no intermediate states + +**Current Verification**: +- Confirm the atomic upgrade was successful +- Validate no issues remain from the original migration +- Document the current verified state + +--- + +## Success Criteria + +### Technical Criteria + +**Project Framework Verification**: ✅ Target Met (Pending Validation) +- [ ] All .NET projects target net10.0: + - [ ] BoardRules\BoardRules.csproj: `net10.0` + - [ ] Shogi\Shogi.csproj: `net10.0` + - [ ] Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj: `net10.0` + - [ ] Tests\UnitTests\UnitTests.csproj: `net10.0` +- [ ] Shogi.Database\Shogi.Database.sqlproj correctly remains on net472 + +**Package Compatibility**: ✅ Target Met (Pending Validation) +- [ ] All 14 NuGet packages verified at .NET 10.0-compatible versions +- [ ] Zero packages with security vulnerabilities +- [ ] All package references restore successfully +- [ ] No package dependency conflicts + +**Build Success**: 🔄 Requires Validation +- [ ] `dotnet restore` succeeds for entire solution +- [ ] `dotnet build` succeeds for entire solution (exit code 0) +- [ ] Zero compilation errors across all 5 projects +- [ ] Zero compilation warnings across all 5 projects +- [ ] All project references resolve correctly + +**Test Success**: 🔄 Requires Validation +- [ ] All acceptance tests execute +- [ ] All acceptance tests pass (100% pass rate) +- [ ] All unit tests execute +- [ ] All unit tests pass (100% pass rate) +- [ ] Code coverage collection works (coverlet.collector) + +**Runtime Success**: 🔄 Requires Validation (if applicable) +- [ ] Application starts successfully +- [ ] No runtime exceptions related to .NET version +- [ ] All major features functional +- [ ] Database connectivity works +- [ ] SignalR functionality operates (if used) +- [ ] Identity/authentication works (if used) + +### Quality Criteria + +**Code Quality**: ✅ Already Maintained +- [ ] No new code quality issues introduced +- [ ] Existing code quality maintained +- [ ] No deprecated API usage warnings + +**Test Coverage**: ✅ Already Maintained +- [ ] Test coverage maintained at previous levels +- [ ] No loss of test coverage +- [ ] All existing tests still relevant + +**Documentation**: 🔄 Requires Update (if applicable) +- [ ] README updated to reference .NET 10.0 (if not already) +- [ ] Deployment documentation reflects .NET 10.0 +- [ ] CI/CD pipeline documentation updated +- [ ] CHANGELOG includes .NET 10.0 verification entry + +### Process Criteria + +**All-At-Once Strategy Compliance**: ✅ Already Applied +- [ ] All-At-Once approach was used (all projects upgraded simultaneously) +- [ ] No intermediate states exist +- [ ] All projects on same target framework (net10.0) +- [ ] Atomic upgrade principle followed + +**Verification Methodology**: 🔄 Requires Execution +- [ ] Systematic project-by-project verification performed +- [ ] Solution-level build verification performed +- [ ] Comprehensive test execution completed +- [ ] Results documented + +**Source Control**: 🔄 Conditional +- [ ] Verification results committed (if git operations performed) +- [ ] Documentation updates committed (if applicable) +- [ ] Clean working directory (or documented pending changes) + +### Completion Definition + +**The verification is complete and successful when:** + +1. ✅ **All .NET projects confirmed on net10.0** + - Project files inspected and verified + - Shogi.Database.sqlproj confirmed on net472 (as expected) + +2. ✅ **All packages confirmed compatible** + - 14 packages verified at compatible versions + - Zero security vulnerabilities + - No deprecated or obsolete packages + +3. ✅ **Solution builds successfully** + - Restore succeeds + - Build succeeds with zero errors + - Zero build warnings + +4. ✅ **All tests pass** + - Acceptance tests: 100% pass + - Unit tests: 100% pass + - No skipped or failing tests + +5. ✅ **Application runs correctly** (if applicable) + - Starts without errors + - Core functionality verified + - No runtime exceptions + +6. ✅ **Documentation updated** (if applicable) + - Version references corrected + - Deployment guides updated + - Verification documented + +### Acceptance Sign-Off + +**Criteria for Final Acceptance:** + +- All technical criteria marked complete ✅ +- All quality criteria marked complete ✅ +- All process criteria marked complete ✅ +- No blocking issues remain +- Verification results documented and committed + +**Final Deliverable**: Verified .NET 10.0 solution with documented validation results, all projects on target framework, all tests passing, ready for deployment. + +### Notes + +This is a **verification scenario**, not an active migration. Success is measured by confirming the current state (net10.0) is correct and functional, not by completing migration activities. The assessment indicates the upgrade has already been completed successfully; verification activities will confirm this assessment is accurate. diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json new file mode 100644 index 0000000..6c714ec --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json @@ -0,0 +1,12 @@ +{ + "scenarioId": "New-DotNet-Version", + "operationId": "a1d9f56b-ad01-4c78-bbfa-17a76f5681a3", + "description": "Upgrade solution or project to new version of .NET", + "startTime": "2026-01-14T02:57:44.7961447Z", + "lastUpdateTime": "2026-01-14T03:37:51.3730402Z", + "stage": "Execution", + "properties": { + "SelectedScenarioStrategy": "AllAtOnce", + "upgradeTargetFramework": "net10.0" + } +} \ No newline at end of file diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md new file mode 100644 index 0000000..b22a0d6 --- /dev/null +++ b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md @@ -0,0 +1,59 @@ +# Shogi .NET 10.0 Upgrade Verification Tasks + +## Overview + +This document tracks the verification of the Shogi solution upgrade to .NET 10.0. All projects are already targeting net10.0, and this verification confirms the upgrade is complete and functional. + +**Progress**: 0/4 tasks complete (0%) ![0%](https://progress-bar.xyz/0) + +--- + +## Tasks + +### [▶] TASK-001: Verify .NET 10.0 SDK and configuration prerequisites +**References**: Plan §Migration Strategy - Phase 0 + +- [✓] (1) Verify .NET 10.0 SDK is installed and available +- [✓] (2) .NET 10.0 SDK version meets minimum requirements (**Verify**) +- [▶] (3) Check for global.json file and verify compatibility if present +- [ ] (4) global.json compatible with .NET 10.0 or no global.json present (**Verify**) + +--- + +### [ ] TASK-002: Verify all projects target net10.0 and build solution +**References**: Plan §Migration Strategy - Phase 1, Plan §Migration Strategy - Phase 2, Plan §Project-by-Project Migration Plans, Plan §Package Update Reference + +- [ ] (1) Verify TargetFramework is net10.0 in BoardRules\BoardRules.csproj +- [ ] (2) Verify TargetFramework is net10.0 in Shogi\Shogi.csproj +- [ ] (3) Verify TargetFramework is net10.0 in Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj +- [ ] (4) Verify TargetFramework is net10.0 in Tests\UnitTests\UnitTests.csproj +- [ ] (5) Verify TargetFramework is net472 in Shogi.Database\Shogi.Database.sqlproj (SQL database project remains on .NET Framework) +- [ ] (6) All .NET projects correctly targeting net10.0 and SQL project correctly on net472 (**Verify**) +- [ ] (7) Verify all 14 package references are at compatible versions per Plan §Package Update Reference +- [ ] (8) All packages are at .NET 10.0-compatible versions (**Verify**) +- [ ] (9) Restore dependencies for entire solution +- [ ] (10) All dependencies restore successfully (**Verify**) +- [ ] (11) Build entire solution +- [ ] (12) Solution builds with 0 errors and 0 warnings (**Verify**) + +--- + +### [ ] TASK-003: Run full test suite and validate +**References**: Plan §Migration Strategy - Phase 3, Plan §Testing & Validation Strategy + +- [ ] (1) Run tests in Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj +- [ ] (2) Fix any acceptance test failures if found +- [ ] (3) Run tests in Tests\UnitTests\UnitTests.csproj +- [ ] (4) Fix any unit test failures if found +- [ ] (5) Re-run all tests after fixes (if any fixes were needed) +- [ ] (6) All tests pass with 0 failures (**Verify**) + +--- + +### [ ] TASK-004: Commit verification results +**References**: Plan §Source Control Strategy + +- [ ] (1) Commit all changes with message: "Verify .NET 10.0 upgrade - all projects confirmed on target framework" + +--- + From 8c65125b16e122f411bf8acb8ce00deb1058db79 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 13 Jan 2026 23:21:23 -0600 Subject: [PATCH 07/15] yep --- .../new-dotnet-version_a1d9f5/assessment.csv | 2 - .../new-dotnet-version_a1d9f5/assessment.json | 222 ---- .../new-dotnet-version_a1d9f5/assessment.md | 339 ------ .../new-dotnet-version_a1d9f5/plan.md | 1028 ----------------- .../new-dotnet-version_a1d9f5/scenario.json | 12 - .../new-dotnet-version_a1d9f5/tasks.md | 59 - Tests/AcceptanceTests/ApiTests.cs | 2 +- .../TestSetup/AatTestFixture.cs | 48 +- 8 files changed, 27 insertions(+), 1685 deletions(-) delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json delete mode 100644 .github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv deleted file mode 100644 index 65de2eb..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.csv +++ /dev/null @@ -1,2 +0,0 @@ -Issue ID,Description,State,Severity,Story Points,Project Path,Location Kind,Path,Line,Column,Incident ID,Help Link,Assembly Name,Assembly Version,Assembly Public Key,Snippet -Project.0002,Project's target framework(s) needs to be changed,Active,Mandatory,1,Shogi.Database\Shogi.Database.sqlproj,File,Shogi.Database\Shogi.Database.sqlproj,,,,,,,,"Current target framework: .NETFramework,Version=v4.7.2 Recommended target framework: net10.0" diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json deleted file mode 100644 index 58e8d5c..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "settings": { - "components": { - "code": true, - "binaries": false - }, - "targetId": "net10.0", - "targetDisplayName": ".NETCoreApp,Version=v10.0" - }, - "analysisStartTime": "2026-01-14T03:11:27.1112869Z", - "analysisEndTime": "2026-01-14T03:11:27.3394814Z", - "privacyModeHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2270980", - "stats": { - "summary": { - "projects": 5, - "issues": 1, - "incidents": 1, - "effort": 1 - }, - "charts": { - "severity": { - "Mandatory": 1, - "Optional": 0, - "Potential": 0, - "Information": 0 - }, - "category": { - "Project": 1 - } - } - }, - "projects": [ - { - "path": "BoardRules\\BoardRules.csproj", - "startingProject": true, - "issues": 0, - "storyPoints": 0, - "properties": { - "appName": "BoardRules", - "projectKind": "ClassLibrary", - "frameworks": [ - "net10.0" - ], - "languages": [ - "C#" - ], - "tools": [ - "MSBuild" - ], - "isSdkStyle": true, - "numberOfFiles": 1, - "numberOfCodeFiles": 1, - "linesTotal": 120, - "linesOfCode": 120, - "totalApiScanned": 0, - "minLinesOfCodeToChange": 0, - "maxLinesOfCodeToChange": 0 - }, - "ruleInstances": [], - "features": [] - }, - { - "path": "Tests\\AcceptanceTests\\Shogi.AcceptanceTests.csproj", - "startingProject": true, - "issues": 0, - "storyPoints": 0, - "properties": { - "appName": "Shogi.AcceptanceTests", - "projectKind": "DotNetCoreApp", - "frameworks": [ - "net10.0" - ], - "languages": [ - "C#" - ], - "tools": [ - "MSBuild" - ], - "isSdkStyle": true, - "numberOfFiles": 5, - "numberOfCodeFiles": 3, - "linesTotal": 3059, - "linesOfCode": 443, - "totalApiScanned": 0, - "minLinesOfCodeToChange": 0, - "maxLinesOfCodeToChange": 0 - }, - "ruleInstances": [], - "features": [] - }, - { - "path": "Shogi\\Shogi.csproj", - "startingProject": true, - "issues": 0, - "storyPoints": 0, - "properties": { - "appName": "Shogi", - "projectKind": "AspNetCore", - "frameworks": [ - "net10.0" - ], - "languages": [ - "C#" - ], - "tools": [ - "MSBuild" - ], - "isSdkStyle": true, - "numberOfFiles": 118, - "numberOfCodeFiles": 55, - "linesTotal": 10759, - "linesOfCode": 4232, - "totalApiScanned": 0, - "minLinesOfCodeToChange": 0, - "maxLinesOfCodeToChange": 0 - }, - "ruleInstances": [], - "features": [] - }, - { - "path": "Shogi.Database\\Shogi.Database.sqlproj", - "startingProject": true, - "issues": 1, - "storyPoints": 1, - "properties": { - "appName": "Shogi.Database", - "projectKind": "ClassicClassLibrary", - "frameworks": [ - "net472" - ], - "languages": [ - "" - ], - "tools": [ - "MSBuild" - ], - "isSdkStyle": false, - "numberOfFiles": 0, - "numberOfCodeFiles": 0, - "linesTotal": 0, - "linesOfCode": 0, - "totalApiScanned": 0, - "minLinesOfCodeToChange": 0, - "maxLinesOfCodeToChange": 0 - }, - "ruleInstances": [ - { - "incidentId": "a43e59f8-9932-42d1-991f-9653db2e0408", - "ruleId": "Project.0002", - "projectPath": "Shogi.Database\\Shogi.Database.sqlproj", - "state": "Active", - "location": { - "snippetModel": { - "unrestricted": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", - "protected": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0" - }, - "kind": "File", - "path": "Shogi.Database\\Shogi.Database.sqlproj", - "snippet": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", - "protectedSnippet": "Current target framework: .NETFramework,Version=v4.7.2\nRecommended target framework: net10.0", - "properties": { - "CurrentTargetFramework": ".NETFramework,Version=v4.7.2", - "RecommendedTargetFramework": "net10.0" - } - } - } - ], - "features": [] - }, - { - "path": "Tests\\UnitTests\\UnitTests.csproj", - "startingProject": true, - "issues": 0, - "storyPoints": 0, - "properties": { - "appName": "UnitTests", - "projectKind": "DotNetCoreApp", - "frameworks": [ - "net10.0" - ], - "languages": [ - "C#" - ], - "tools": [ - "MSBuild" - ], - "isSdkStyle": true, - "numberOfFiles": 7, - "numberOfCodeFiles": 5, - "linesTotal": 3574, - "linesOfCode": 958, - "totalApiScanned": 0, - "minLinesOfCodeToChange": 0, - "maxLinesOfCodeToChange": 0 - }, - "ruleInstances": [], - "features": [] - } - ], - "rules": { - "Project.0002": { - "id": "Project.0002", - "isFeature": false, - "description": "Project\u0027s target framework(s) needs to be changed to the new target framework that you selected for this upgrade.\n\nDuring upgrade target framework will be adjusted to corresponding platform when applicable. In some cases project would result in multiple target frameworks after the upgrade if it was using features that now have their own platforms in modern .NET frameworks (windows, iOS, Android etc).", - "label": "Project\u0027s target framework(s) needs to be changed", - "severity": "Mandatory", - "effort": 1, - "links": [ - { - "title": "Overview of porting from .NET Framework to .NET", - "url": "https://go.microsoft.com/fwlink/?linkid=2265227", - "isCustom": false - }, - { - "title": ".NET project SDKs", - "url": "https://go.microsoft.com/fwlink/?linkid=2265226", - "isCustom": false - } - ] - } - } -} \ No newline at end of file diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md deleted file mode 100644 index 204da56..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/assessment.md +++ /dev/null @@ -1,339 +0,0 @@ -# Projects and dependencies analysis - -This document provides a comprehensive overview of the projects and their dependencies in the context of upgrading to .NETCoreApp,Version=v10.0. - -## Table of Contents - -- [Executive Summary](#executive-Summary) - - [Highlevel Metrics](#highlevel-metrics) - - [Projects Compatibility](#projects-compatibility) - - [Package Compatibility](#package-compatibility) - - [API Compatibility](#api-compatibility) -- [Aggregate NuGet packages details](#aggregate-nuget-packages-details) -- [Top API Migration Challenges](#top-api-migration-challenges) - - [Technologies and Features](#technologies-and-features) - - [Most Frequent API Issues](#most-frequent-api-issues) -- [Projects Relationship Graph](#projects-relationship-graph) -- [Project Details](#project-details) - - - [BoardRules\BoardRules.csproj](#boardrulesboardrulescsproj) - - [Shogi.Database\Shogi.Database.sqlproj](#shogidatabaseshogidatabasesqlproj) - - [Shogi\Shogi.csproj](#shogishogicsproj) - - [Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) - - [Tests\UnitTests\UnitTests.csproj](#testsunittestsunittestscsproj) - - -## Executive Summary - -### Highlevel Metrics - -| Metric | Count | Status | -| :--- | :---: | :--- | -| Total Projects | 5 | 1 require upgrade | -| Total NuGet Packages | 14 | All compatible | -| Total Code Files | 64 | | -| Total Code Files with Incidents | 1 | | -| Total Lines of Code | 5753 | | -| Total Number of Issues | 1 | | -| Estimated LOC to modify | 0+ | at least 0.0% of codebase | - -### Projects Compatibility - -| Project | Target Framework | Difficulty | Package Issues | API Issues | Est. LOC Impact | Description | -| :--- | :---: | :---: | :---: | :---: | :---: | :--- | -| [BoardRules\BoardRules.csproj](#boardrulesboardrulescsproj) | net10.0 | ✅ None | 0 | 0 | | ClassLibrary, Sdk Style = True | -| [Shogi.Database\Shogi.Database.sqlproj](#shogidatabaseshogidatabasesqlproj) | net472 | 🟢 Low | 0 | 0 | | ClassicClassLibrary, Sdk Style = False | -| [Shogi\Shogi.csproj](#shogishogicsproj) | net10.0 | ✅ None | 0 | 0 | | AspNetCore, Sdk Style = True | -| [Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) | net10.0 | ✅ None | 0 | 0 | | DotNetCoreApp, Sdk Style = True | -| [Tests\UnitTests\UnitTests.csproj](#testsunittestsunittestscsproj) | net10.0 | ✅ None | 0 | 0 | | DotNetCoreApp, Sdk Style = True | - -### Package Compatibility - -| Status | Count | Percentage | -| :--- | :---: | :---: | -| ✅ Compatible | 14 | 100.0% | -| ⚠️ Incompatible | 0 | 0.0% | -| 🔄 Upgrade Recommended | 0 | 0.0% | -| ***Total NuGet Packages*** | ***14*** | ***100%*** | - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - -## Aggregate NuGet packages details - -| Package | Current Version | Suggested Version | Projects | Description | -| :--- | :---: | :---: | :--- | :--- | -| coverlet.collector | 6.0.4 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | -| Dapper | 2.1.66 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| FluentValidation | 12.1.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj) | ✅Compatible | -| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.EntityFrameworkCore.Design | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| Microsoft.NET.Test.Sdk | 18.0.1 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | -| Swashbuckle.AspNetCore | 10.1.0 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| System.Data.SqlClient | 4.9.0 | | [Shogi.csproj](#shogishogicsproj) | ✅Compatible | -| xunit | 2.9.3 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | -| xunit.runner.visualstudio | 3.1.5 | | [Shogi.AcceptanceTests.csproj](#testsacceptancetestsshogiacceptancetestscsproj)
[UnitTests.csproj](#testsunittestsunittestscsproj) | ✅Compatible | - -## Top API Migration Challenges - -### Technologies and Features - -| Technology | Issues | Percentage | Migration Path | -| :--- | :---: | :---: | :--- | - -### Most Frequent API Issues - -| API | Count | Percentage | Category | -| :--- | :---: | :---: | :--- | - -## Projects Relationship Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart LR - P1["⚙️ Shogi.Database.sqlproj
net472"] - P2["📦 BoardRules.csproj
net10.0"] - P3["📦 Shogi.AcceptanceTests.csproj
net10.0"] - P4["📦 UnitTests.csproj
net10.0"] - P5["📦 Shogi.csproj
net10.0"] - P3 --> P5 - P4 --> P5 - click P1 "#shogidatabaseshogidatabasesqlproj" - click P2 "#boardrulesboardrulescsproj" - click P3 "#testsacceptancetestsshogiacceptancetestscsproj" - click P4 "#testsunittestsunittestscsproj" - click P5 "#shogishogicsproj" - -``` - -## Project Details - - -### BoardRules\BoardRules.csproj - -#### Project Info - -- **Current Target Framework:** net10.0✅ -- **SDK-style**: True -- **Project Kind:** ClassLibrary -- **Dependencies**: 0 -- **Dependants**: 0 -- **Number of Files**: 1 -- **Lines of Code**: 120 -- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) - -#### Dependency Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart TB - subgraph current["BoardRules.csproj"] - MAIN["📦 BoardRules.csproj
net10.0"] - click MAIN "#boardrulesboardrulescsproj" - end - -``` - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - - -### Shogi.Database\Shogi.Database.sqlproj - -#### Project Info - -- **Current Target Framework:** net472 -- **Proposed Target Framework:** net10.0 -- **SDK-style**: False -- **Project Kind:** ClassicClassLibrary -- **Dependencies**: 0 -- **Dependants**: 0 -- **Number of Files**: 0 -- **Number of Files with Incidents**: 1 -- **Lines of Code**: 0 -- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) - -#### Dependency Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart TB - subgraph current["Shogi.Database.sqlproj"] - MAIN["⚙️ Shogi.Database.sqlproj
net472"] - click MAIN "#shogidatabaseshogidatabasesqlproj" - end - -``` - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - - -### Shogi\Shogi.csproj - -#### Project Info - -- **Current Target Framework:** net10.0✅ -- **SDK-style**: True -- **Project Kind:** AspNetCore -- **Dependencies**: 0 -- **Dependants**: 2 -- **Number of Files**: 118 -- **Lines of Code**: 4232 -- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) - -#### Dependency Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart TB - subgraph upstream["Dependants (2)"] - P3["📦 Shogi.AcceptanceTests.csproj
net10.0"] - P4["📦 UnitTests.csproj
net10.0"] - click P3 "#testsacceptancetestsshogiacceptancetestscsproj" - click P4 "#testsunittestsunittestscsproj" - end - subgraph current["Shogi.csproj"] - MAIN["📦 Shogi.csproj
net10.0"] - click MAIN "#shogishogicsproj" - end - P3 --> MAIN - P4 --> MAIN - -``` - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - - -### Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj - -#### Project Info - -- **Current Target Framework:** net10.0✅ -- **SDK-style**: True -- **Project Kind:** DotNetCoreApp -- **Dependencies**: 1 -- **Dependants**: 0 -- **Number of Files**: 5 -- **Lines of Code**: 443 -- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) - -#### Dependency Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart TB - subgraph current["Shogi.AcceptanceTests.csproj"] - MAIN["📦 Shogi.AcceptanceTests.csproj
net10.0"] - click MAIN "#testsacceptancetestsshogiacceptancetestscsproj" - end - subgraph downstream["Dependencies (1"] - P5["📦 Shogi.csproj
net10.0"] - click P5 "#shogishogicsproj" - end - MAIN --> P5 - -``` - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - - -### Tests\UnitTests\UnitTests.csproj - -#### Project Info - -- **Current Target Framework:** net10.0✅ -- **SDK-style**: True -- **Project Kind:** DotNetCoreApp -- **Dependencies**: 1 -- **Dependants**: 0 -- **Number of Files**: 7 -- **Lines of Code**: 958 -- **Estimated LOC to modify**: 0+ (at least 0.0% of the project) - -#### Dependency Graph - -Legend: -📦 SDK-style project -⚙️ Classic project - -```mermaid -flowchart TB - subgraph current["UnitTests.csproj"] - MAIN["📦 UnitTests.csproj
net10.0"] - click MAIN "#testsunittestsunittestscsproj" - end - subgraph downstream["Dependencies (1"] - P5["📦 Shogi.csproj
net10.0"] - click P5 "#shogishogicsproj" - end - MAIN --> P5 - -``` - -### API Compatibility - -| Category | Count | Impact | -| :--- | :---: | :--- | -| 🔴 Binary Incompatible | 0 | High - Require code changes | -| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing | -| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime | -| ✅ Compatible | 0 | | -| ***Total APIs Analyzed*** | ***0*** | | - diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md deleted file mode 100644 index f0e078e..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/plan.md +++ /dev/null @@ -1,1028 +0,0 @@ -# .NET 10.0 Upgrade Plan - -## Table of Contents - -- [Executive Summary](#executive-summary) -- [Migration Strategy](#migration-strategy) -- [Detailed Dependency Analysis](#detailed-dependency-analysis) -- [Project-by-Project Migration Plans](#project-by-project-migration-plans) -- [Package Update Reference](#package-update-reference) -- [Breaking Changes Catalog](#breaking-changes-catalog) -- [Testing & Validation Strategy](#testing--validation-strategy) -- [Risk Management](#risk-management) -- [Complexity & Effort Assessment](#complexity--effort-assessment) -- [Source Control Strategy](#source-control-strategy) -- [Success Criteria](#success-criteria) - ---- - -## Executive Summary - -### Scenario Description - -Upgrade Shogi solution from .NET 8.0 to **.NET 10.0 (Long Term Support)**. The solution consists of 5 projects, with 4 .NET projects already targeting net10.0 and 1 SQL database project remaining on .NET Framework 4.7.2. - -### Scope - -**Projects in Scope:** -- **BoardRules\BoardRules.csproj** - Class library, already on net10.0 ✅ -- **Shogi\Shogi.csproj** - ASP.NET Core web application, already on net10.0 ✅ -- **Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj** - Test project, already on net10.0 ✅ -- **Tests\UnitTests\UnitTests.csproj** - Test project, already on net10.0 ✅ -- **Shogi.Database\Shogi.Database.sqlproj** - SQL database project, remains on net472 (as expected) - -**Current State:** -- 4 of 5 .NET projects already targeting net10.0 -- All 14 NuGet packages already compatible with .NET 10.0 -- No security vulnerabilities detected -- Total codebase: 5,753 lines of code across 64 files - -**Target State:** -- Confirm all .NET projects are on net10.0 -- Verify all packages are compatible -- Validate solution builds and all tests pass - -### Selected Strategy - -**All-At-Once Strategy** - All projects are already upgraded simultaneously. - -**Rationale:** -- **Small solution** - Only 5 projects -- **Already migrated** - 4 projects already on net10.0 -- **Simple dependency structure** - Clear hierarchy with no circular dependencies -- **All packages compatible** - All 14 NuGet packages support .NET 10.0 -- **Zero risk** - No breaking changes, no security issues -- **Verification focus** - Primary task is to validate current state, not perform migration - -### Complexity Assessment - -**Discovered Metrics:** -- **Project count**: 5 (4 .NET projects + 1 SQL database project) -- **Dependency depth**: 1 (test projects depend on main Shogi project) -- **High-risk projects**: 0 -- **Security vulnerabilities**: 0 -- **Package updates required**: 0 (all already compatible) -- **Lines of code**: 5,753 -- **API compatibility issues**: 0 - -**Classification**: **Simple** - Verification scenario - -**Critical Issues**: None - -### Recommended Approach - -**Phase 1: Verification** - Confirm projects are on net10.0, packages are compatible -**Phase 2: Build & Test** - Ensure solution builds and all tests pass -**Phase 3: Documentation** - Update any remaining documentation references - -This is primarily a **validation exercise** rather than an active migration, as the solution appears to have already been upgraded to .NET 10.0. - -### Iteration Strategy - -**Fast Verification Approach** (2-3 iterations): -- One iteration for all project details (since they're already upgraded) -- One iteration for validation and success criteria - ---- - -## Migration Strategy - -### Approach Selection - -**Selected: All-At-Once Strategy (Verification Mode)** - -This scenario is unique - it's not a traditional migration but a **verification** of an already-completed upgrade. All .NET projects already target net10.0. - -### Strategy Rationale - -**Why All-At-Once (Already Complete):** - -1. **Small Solution** - Only 5 projects total (4 .NET projects) -2. **Already Migrated** - All .NET projects already on net10.0 -3. **All Packages Compatible** - All 14 NuGet packages already support .NET 10.0 -4. **No Breaking Changes** - Zero API compatibility issues detected -5. **Simple Dependencies** - Clear hierarchy, no circular dependencies -6. **Low Risk** - No security vulnerabilities, no code changes required - -### All-At-Once Strategy Considerations - -**Simultaneity**: Not applicable - upgrade already performed. Focus is on validation. - -**Atomic Operation**: The original upgrade appears to have been completed atomically. Our task is to verify this state. - -**No Intermediate States**: Already achieved - all projects are on target framework. - -### Dependency-Based Ordering - -While not needed for migration, validation will follow dependency order: - -**Validation Order:** -1. **Standalone projects first**: BoardRules (no dependencies) -2. **Main application**: Shogi.csproj (depends on nothing within solution) -3. **Test projects last**: AcceptanceTests, UnitTests (depend on Shogi.csproj) - -This ensures if issues exist, they're discovered at the lowest dependency level first. - -### Parallel vs Sequential Execution - -**Validation Approach**: Sequential validation following dependency order ensures clear error isolation. However, since all projects are already upgraded, parallel validation would also be safe. - -**Recommended**: Sequential for clarity, parallel acceptable for speed. - -### Phase Definitions - -**Phase 1: Project File Verification** -- Confirm all .NET projects have `net10.0` -- Verify Shogi.Database.sqlproj correctly remains on net472 -- Validate package references are compatible versions - -**Phase 2: Build Validation** -- Restore NuGet packages -- Build entire solution -- Confirm zero errors, zero warnings - -**Phase 3: Test Validation** -- Run acceptance tests -- Run unit tests -- Confirm all tests pass - -**Phase 4: Documentation** -- Update any deployment documentation -- Confirm README reflects .NET 10.0 -- Document verification results - ---- - -## Detailed Dependency Analysis - -### Dependency Graph Summary - -The solution has a simple, clean dependency structure: - -``` -BoardRules.csproj (standalone, no dependencies) - ↓ -Shogi.csproj (main ASP.NET Core application) - ↓ -├── Shogi.AcceptanceTests.csproj (acceptance tests) -└── UnitTests.csproj (unit tests) - -Shogi.Database.sqlproj (standalone SQL database project) -``` - -**Dependency Characteristics:** -- **Depth**: 1 level (test projects depend on main project) -- **Circular dependencies**: None -- **Standalone projects**: 2 (BoardRules, Shogi.Database) -- **Leaf nodes**: 2 (test projects) -- **Root node**: 1 (Shogi.csproj) - -### Project Groupings by Migration Phase - -Since all .NET projects are already on net10.0, grouping is for validation purposes only: - -**Group 1: Standalone Libraries** -- BoardRules\BoardRules.csproj (already net10.0) -- Shogi.Database\Shogi.Database.sqlproj (remains net472 - SQL database project) - -**Group 2: Main Application** -- Shogi\Shogi.csproj (already net10.0) - -**Group 3: Test Projects** -- Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj (already net10.0) -- Tests\UnitTests\UnitTests.csproj (already net10.0) - -### Critical Path Identification - -**No critical path** - All projects are already on target framework. The validation sequence is: - -1. Verify standalone libraries (BoardRules) -2. Verify main application (Shogi) -3. Verify test projects build and pass - -### Circular Dependency Analysis - -**No circular dependencies detected** - Clean dependency tree. - ---- - -## Project-by-Project Migration Plans - -### Project: BoardRules\BoardRules.csproj - -**Current State**: -- **Target Framework**: net10.0 ✅ -- **SDK-Style**: True -- **Project Type**: Class Library -- **Dependencies**: 0 project dependencies, 0 NuGet packages -- **Dependants**: 0 (standalone library) -- **Lines of Code**: 120 -- **Risk Level**: None - -**Target State**: -- **Target Framework**: net10.0 (already achieved) -- **Package Count**: 0 (unchanged) - -**Migration Steps**: ✅ Already Complete - -This project is already on net10.0. No migration actions required. - -**Validation Steps**: -1. ✅ Verify project file contains `net10.0` -2. ✅ Build project independently -3. ✅ Confirm zero build errors -4. ✅ Confirm zero build warnings - -**Expected Breaking Changes**: None - project already on target framework - -**Code Modifications**: None required - -**Testing Strategy**: -- Build verification only (no test project exists for BoardRules) -- Confirm no compilation errors -- Verify as part of solution-wide build - -**Validation Checklist**: -- [ ] Project builds successfully -- [ ] No build warnings -- [ ] No package dependency conflicts -- [ ] Compatible with dependent projects (none currently) - ---- - -### Project: Shogi\Shogi.csproj - -**Current State**: -- **Target Framework**: net10.0 ✅ -- **SDK-Style**: True -- **Project Type**: ASP.NET Core Web Application -- **Dependencies**: 0 project dependencies (within solution) -- **Dependants**: 2 (Shogi.AcceptanceTests, UnitTests) -- **NuGet Packages**: 12 packages, all compatible with net10.0 -- **Lines of Code**: 4,232 -- **Risk Level**: None - -**Target State**: -- **Target Framework**: net10.0 (already achieved) -- **Package Count**: 12 (all already compatible) - -**Migration Steps**: ✅ Already Complete - -This project is already on net10.0 with all packages at compatible versions. - -**Package Updates**: None required - all packages already compatible - -| Package | Current Version | Status | -|---------|----------------|---------| -| Dapper | 2.1.66 | ✅ Compatible | -| FluentValidation | 12.1.1 | ✅ Compatible | -| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | ✅ Compatible | -| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | ✅ Compatible | -| Microsoft.EntityFrameworkCore.Design | 10.0.1 | ✅ Compatible | -| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | ✅ Compatible | -| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | ✅ Compatible | -| Swashbuckle.AspNetCore | 10.1.0 | ✅ Compatible | -| System.Data.SqlClient | 4.9.0 | ✅ Compatible | - -**Expected Breaking Changes**: None - project already on target framework, packages already compatible - -**Code Modifications**: None required - -**Configuration Updates**: None required - ASP.NET Core configuration already compatible - -**Testing Strategy**: -- Build project successfully -- Execute acceptance tests (see Shogi.AcceptanceTests project) -- Execute unit tests (see UnitTests project) -- Verify application startup (if applicable) - -**Validation Checklist**: -- [ ] Project builds successfully -- [ ] No build warnings -- [ ] All package dependencies restore successfully -- [ ] Entity Framework migrations compatible (if any) -- [ ] Acceptance tests pass -- [ ] Unit tests pass -- [ ] SignalR functionality works (if used) -- [ ] Identity/authentication works (if used) - ---- - -### Project: Shogi.Database\Shogi.Database.sqlproj - -**Current State**: -- **Target Framework**: net472 -- **SDK-Style**: False -- **Project Type**: SQL Database Project (Classic) -- **Dependencies**: 0 project dependencies -- **Dependants**: 0 -- **Lines of Code**: 0 (SQL project) -- **Risk Level**: None - -**Target State**: -- **Target Framework**: net472 (unchanged - SQL database projects remain on .NET Framework) -- **No migration required** - -**Migration Steps**: ⚠️ No Migration Required - -SQL Database projects (.sqlproj) are build-time tools that generate database schema scripts. They typically remain on .NET Framework and do not need to be upgraded to .NET Core/.NET 10.0. - -**Rationale for Not Upgrading**: -- SQL database projects are MSBuild-based schema definition tools -- They generate T-SQL scripts, not runtime executables -- .NET Framework 4.7.2 is appropriate for these projects -- No compatibility issues with the rest of the solution being on .NET 10.0 - -**Validation Steps**: -1. ✅ Confirm project remains on net472 -2. ✅ Verify project builds successfully -3. ✅ Confirm database scripts generate correctly - -**Expected Breaking Changes**: None - -**Code Modifications**: None - -**Testing Strategy**: -- Build verification only -- Confirm SQL scripts generate without errors -- Verify compatibility with SQL Server deployment targets - -**Validation Checklist**: -- [ ] Project builds successfully on net472 -- [ ] No build warnings -- [ ] Database schema scripts generate correctly -- [ ] No impact on .NET 10.0 projects in solution - ---- - -### Project: Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj - -**Current State**: -- **Target Framework**: net10.0 ✅ -- **SDK-Style**: True -- **Project Type**: xUnit Test Project (.NET Core) -- **Dependencies**: 1 project (Shogi.csproj) -- **Dependants**: 0 -- **NuGet Packages**: 6 packages, all compatible with net10.0 -- **Lines of Code**: 443 -- **Risk Level**: None - -**Target State**: -- **Target Framework**: net10.0 (already achieved) -- **Package Count**: 6 (all already compatible) - -**Migration Steps**: ✅ Already Complete - -This project is already on net10.0 with all test packages at compatible versions. - -**Package Updates**: None required - all packages already compatible - -| Package | Current Version | Status | -|---------|----------------|---------| -| coverlet.collector | 6.0.4 | ✅ Compatible | -| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | ✅ Compatible | -| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | ✅ Compatible | -| Microsoft.NET.Test.Sdk | 18.0.1 | ✅ Compatible | -| xunit | 2.9.3 | ✅ Compatible | -| xunit.runner.visualstudio | 3.1.5 | ✅ Compatible | - -**Expected Breaking Changes**: None - project already on target framework - -**Code Modifications**: None required - -**Testing Strategy**: -- Build test project successfully -- Execute all acceptance tests -- Verify test discovery works correctly -- Confirm code coverage collection works (coverlet.collector) -- Validate ASP.NET Core test host compatibility (Microsoft.AspNetCore.Mvc.Testing) - -**Validation Checklist**: -- [ ] Test project builds successfully -- [ ] No build warnings -- [ ] All tests are discovered -- [ ] All tests execute -- [ ] All tests pass -- [ ] Code coverage reporting works -- [ ] In-memory database tests function correctly - ---- - -### Project: Tests\UnitTests\UnitTests.csproj - -**Current State**: -- **Target Framework**: net10.0 ✅ -- **SDK-Style**: True -- **Project Type**: xUnit Test Project (.NET Core) -- **Dependencies**: 1 project (Shogi.csproj) -- **Dependants**: 0 -- **NuGet Packages**: 4 packages, all compatible with net10.0 -- **Lines of Code**: 958 -- **Risk Level**: None - -**Target State**: -- **Target Framework**: net10.0 (already achieved) -- **Package Count**: 4 (all already compatible) - -**Migration Steps**: ✅ Already Complete - -This project is already on net10.0 with all test packages at compatible versions. - -**Package Updates**: None required - all packages already compatible - -| Package | Current Version | Status | -|---------|----------------|---------| -| coverlet.collector | 6.0.4 | ✅ Compatible | -| Microsoft.NET.Test.Sdk | 18.0.1 | ✅ Compatible | -| xunit | 2.9.3 | ✅ Compatible | -| xunit.runner.visualstudio | 3.1.5 | ✅ Compatible | - -**Expected Breaking Changes**: None - project already on target framework - -**Code Modifications**: None required - -**Testing Strategy**: -- Build test project successfully -- Execute all unit tests -- Verify test discovery works correctly -- Confirm code coverage collection works (coverlet.collector) - -**Validation Checklist**: -- [ ] Test project builds successfully -- [ ] No build warnings -- [ ] All tests are discovered -- [ ] All tests execute -- [ ] All tests pass -- [ ] Code coverage reporting works - ---- - -## Package Update Reference - -### Summary - -**All packages are already compatible with .NET 10.0.** No package updates are required as part of this verification exercise. - -### Complete Package Matrix - -| Package | Current Version | Target Version | Projects | Update Reason | -|---------|----------------|----------------|----------|---------------| -| coverlet.collector | 6.0.4 | 6.0.4 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | -| Dapper | 2.1.66 | 2.1.66 (no change) | Shogi | ✅ Already compatible | -| FluentValidation | 12.1.1 | 12.1.1 (no change) | Shogi | ✅ Already compatible | -| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | -| Microsoft.AspNetCore.Mvc.Testing | 10.0.1 | 10.0.1 (no change) | AcceptanceTests | ✅ Already compatible | -| Microsoft.AspNetCore.SignalR.Client | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | -| Microsoft.EntityFrameworkCore.Design | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | -| Microsoft.EntityFrameworkCore.InMemory | 10.0.1 | 10.0.1 (no change) | AcceptanceTests, Shogi | ✅ Already compatible | -| Microsoft.EntityFrameworkCore.SqlServer | 10.0.1 | 10.0.1 (no change) | Shogi | ✅ Already compatible | -| Microsoft.NET.Test.Sdk | 18.0.1 | 18.0.1 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | -| Swashbuckle.AspNetCore | 10.1.0 | 10.1.0 (no change) | Shogi | ✅ Already compatible | -| System.Data.SqlClient | 4.9.0 | 4.9.0 (no change) | Shogi | ✅ Already compatible | -| xunit | 2.9.3 | 2.9.3 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | -| xunit.runner.visualstudio | 3.1.5 | 3.1.5 (no change) | AcceptanceTests, UnitTests | ✅ Already compatible | - -### Package Category Analysis - -**ASP.NET Core Packages** (already at .NET 10.0 versions): -- Microsoft.AspNetCore.Identity.EntityFrameworkCore: 10.0.1 -- Microsoft.AspNetCore.Mvc.Testing: 10.0.1 -- Microsoft.AspNetCore.SignalR.Client: 10.0.1 - -**Entity Framework Core Packages** (already at .NET 10.0 versions): -- Microsoft.EntityFrameworkCore.Design: 10.0.1 -- Microsoft.EntityFrameworkCore.InMemory: 10.0.1 -- Microsoft.EntityFrameworkCore.SqlServer: 10.0.1 - -**Testing Packages** (already at compatible versions): -- Microsoft.NET.Test.Sdk: 18.0.1 -- xunit: 2.9.3 -- xunit.runner.visualstudio: 3.1.5 -- coverlet.collector: 6.0.4 - -**Utility Packages** (already at compatible versions): -- Dapper: 2.1.66 -- FluentValidation: 12.1.1 -- Swashbuckle.AspNetCore: 10.1.0 -- System.Data.SqlClient: 4.9.0 - -### Security Assessment - -**No security vulnerabilities detected** in any packages. All packages are at secure, compatible versions for .NET 10.0. - ---- - -## Breaking Changes Catalog - -### Summary - -**No breaking changes detected.** All projects are already on the target framework (net10.0) and all packages are at compatible versions. - -### Framework Breaking Changes - -**From .NET 8.0 to .NET 10.0**: Not applicable - projects already on net10.0. - -If projects had been migrated from .NET 8.0 to .NET 10.0, the following categories would typically be reviewed: -- ASP.NET Core API changes (already handled) -- Entity Framework Core API changes (already handled) -- BCL (Base Class Library) changes (already handled) -- Runtime behavior changes (already handled) - -### Package-Specific Breaking Changes - -**No package breaking changes** - all packages are already at .NET 10.0-compatible versions. - -### Configuration Changes - -**No configuration changes required** - ASP.NET Core configuration is already compatible with .NET 10.0. - -### Known Issues - -**None identified** during assessment. All API compatibility checks passed. - -### Areas Requiring Special Attention - -Since this is a verification scenario, the following areas should be validated to ensure no issues were missed: - -1. **Entity Framework Migrations**: Verify all EF migrations are compatible and functional -2. **SignalR Implementation**: Confirm SignalR hubs and clients work correctly -3. **Identity/Authentication**: Validate authentication and authorization flows -4. **Dependency Injection**: Ensure all service registrations are correct -5. **Middleware Pipeline**: Verify middleware order and functionality - -### Breaking Changes by Project - -**BoardRules.csproj**: None -**Shogi.csproj**: None -**Shogi.Database.sqlproj**: None (not migrating) -**Shogi.AcceptanceTests.csproj**: None -**UnitTests.csproj**: None - ---- - -## Testing & Validation Strategy - -### Overview - -Testing strategy focuses on **verification** rather than migration validation, since all projects are already on the target framework. - -### Multi-Level Testing - -#### Phase 1: Project-Level Verification - -**For Each Project:** - -1. **BoardRules.csproj** - - [ ] Project file verification: `net10.0` exists - - [ ] Builds without errors - - [ ] Builds without warnings - - [ ] No package dependency conflicts - -2. **Shogi.csproj** - - [ ] Project file verification: `net10.0` exists - - [ ] All 12 packages restore successfully - - [ ] Builds without errors - - [ ] Builds without warnings - - [ ] Entity Framework migrations are intact - - [ ] Configuration files are valid - -3. **Shogi.Database.sqlproj** - - [ ] Project file verification: remains on net472 - - [ ] Builds without errors - - [ ] Database scripts generate successfully - -4. **Shogi.AcceptanceTests.csproj** - - [ ] Project file verification: `net10.0` exists - - [ ] All 6 test packages restore successfully - - [ ] Builds without errors - - [ ] All tests are discovered - - [ ] Tests can execute (actual pass/fail checked in Phase 3) - -5. **UnitTests.csproj** - - [ ] Project file verification: `net10.0` exists - - [ ] All 4 test packages restore successfully - - [ ] Builds without errors - - [ ] All tests are discovered - - [ ] Tests can execute (actual pass/fail checked in Phase 3) - -#### Phase 2: Solution-Level Verification - -**Complete Solution Build:** -- [ ] `dotnet restore` succeeds for entire solution -- [ ] `dotnet build` succeeds for entire solution -- [ ] Zero compilation errors across all projects -- [ ] Zero compilation warnings across all projects -- [ ] All project references resolve correctly -- [ ] All package dependencies resolve correctly - -#### Phase 3: Functional Testing - -**Acceptance Tests:** -- [ ] All acceptance tests execute -- [ ] All acceptance tests pass -- [ ] ASP.NET Core test host initializes correctly -- [ ] In-memory database tests function correctly -- [ ] Integration scenarios work as expected - -**Unit Tests:** -- [ ] All unit tests execute -- [ ] All unit tests pass -- [ ] Code coverage collection works (if enabled) -- [ ] Test isolation is maintained - -**Application Verification** (if applicable): -- [ ] Application starts successfully -- [ ] API endpoints are accessible -- [ ] Database connectivity works -- [ ] SignalR functionality operates correctly (if used) -- [ ] Identity/authentication flows work (if used) -- [ ] Swagger/OpenAPI documentation generates correctly - -#### Phase 4: Deployment Verification (if applicable) - -- [ ] Application can be published (`dotnet publish`) -- [ ] Published output is complete -- [ ] Published application runs in deployment environment -- [ ] Configuration transformations work correctly - -### Test Execution Order - -1. **Standalone projects first**: BoardRules, Shogi.Database -2. **Main application**: Shogi -3. **Test projects**: AcceptanceTests, UnitTests - -This order follows dependency hierarchy to catch issues early. - -### Success Criteria for Testing - -**Build Success:** -- All 5 projects build without errors -- Zero build warnings -- All package references restore successfully - -**Test Success:** -- All acceptance tests pass (100%) -- All unit tests pass (100%) -- Code coverage maintains or improves previous levels - -**Functional Success:** -- Application starts and runs correctly -- All major features function as expected -- No runtime exceptions related to framework version - -### Testing Tools - -- **Build**: `dotnet build` (or Visual Studio) -- **Test Execution**: `dotnet test` (or Visual Studio Test Explorer) -- **Code Coverage**: coverlet.collector (already configured) -- **Package Management**: `dotnet restore` or NuGet Package Manager - -### Regression Testing - -Since projects are already upgraded, regression testing ensures no functionality was lost: - -- Review test results history (if available) -- Compare current test pass rate with previous runs -- Validate critical business scenarios -- Check performance benchmarks (if established) - ---- - -## Risk Management - -### High-Level Risk Assessment - -**Overall Risk Level**: **Very Low** - -All .NET projects are already on the target framework (net10.0), all packages are compatible, and no security vulnerabilities were detected. This is a verification exercise, not an active migration. - -### Risk Analysis - -| Project | Risk Level | Description | Mitigation | -|---------|-----------|-------------|------------| -| BoardRules.csproj | ✅ None | Already on net10.0, standalone library, 120 LOC | Verify builds without errors | -| Shogi.csproj | ✅ None | Already on net10.0, all packages compatible, 4,232 LOC | Build and run acceptance/unit tests | -| Shogi.Database.sqlproj | ✅ None | Remains on net472 as expected for SQL database projects | No action required | -| Shogi.AcceptanceTests.csproj | ✅ None | Already on net10.0, test project, 443 LOC | Execute tests to confirm functionality | -| UnitTests.csproj | ✅ None | Already on net10.0, test project, 958 LOC | Execute tests to confirm functionality | - -### Security Vulnerabilities - -**No security vulnerabilities detected** across all NuGet packages. All packages are at compatible, secure versions for .NET 10.0. - -### Contingency Plans - -Since this is a verification scenario with minimal risk, contingency planning is straightforward: - -**If build fails:** -- Review build errors for any environmental issues (SDK version, tooling) -- Confirm .NET 10.0 SDK is properly installed -- Check for missing NuGet package sources - -**If tests fail:** -- Isolate failing tests to determine if related to framework version -- Review test execution environment -- Check for test infrastructure compatibility with .NET 10.0 - -**If package compatibility issues arise:** -- Re-assess package versions (assessment shows all compatible) -- Check for package source configuration issues -- Verify NuGet cache is not corrupted - -### Rollback Plan - -**Not applicable** - Since projects are already on net10.0, there is no migration to roll back. If issues are discovered, they would be addressed in-place rather than through rollback. - ---- - -## Complexity & Effort Assessment - -### Per-Project Complexity - -| Project | Complexity | Dependencies | Risk | LOC | Notes | -|---------|-----------|--------------|------|-----|-------| -| BoardRules.csproj | Low | 0 | None | 120 | Standalone library, already upgraded | -| Shogi.csproj | Low | 0 | None | 4,232 | Main app, already upgraded, all packages compatible | -| Shogi.Database.sqlproj | Low | 0 | None | 0 | SQL database project, no migration needed | -| Shogi.AcceptanceTests.csproj | Low | 1 | None | 443 | Test project, already upgraded | -| UnitTests.csproj | Low | 1 | None | 958 | Test project, already upgraded | - -**Overall Complexity**: **Low** - This is a verification scenario, not an active migration. - -### Phase Complexity Assessment - -**Phase 1: Project File Verification** -- **Complexity**: Low -- **Effort**: Minimal - automated verification of TargetFramework properties -- **Dependencies**: None - -**Phase 2: Build Validation** -- **Complexity**: Low -- **Effort**: Minimal - standard build process -- **Dependencies**: Phase 1 complete - -**Phase 3: Test Validation** -- **Complexity**: Low -- **Effort**: Minimal - execute existing test suites -- **Dependencies**: Phase 2 complete (solution must build) - -**Phase 4: Documentation** -- **Complexity**: Low -- **Effort**: Minimal - update deployment docs if needed -- **Dependencies**: Phase 3 complete (tests must pass) - -### Resource Requirements - -**Skill Levels Required:** -- **Basic .NET knowledge** - Sufficient for verification activities -- **Build/test execution** - Standard CI/CD skills -- **No advanced migration expertise needed** - Projects already upgraded - -**Parallel Capacity:** -- **Not applicable** - Verification is lightweight and can be done sequentially -- All validation activities can be performed by a single resource - -### Total Effort Estimate - -**Relative Complexity**: **Low** - -**Effort Distribution:** -- Project file verification: Low -- Build validation: Low -- Test validation: Low -- Documentation updates: Low - -**Note**: No real-time estimates provided. This is a verification exercise with minimal actual work required. The primary task is confirming that the existing state is correct and functional. - ---- - -## Source Control Strategy - -### Overview - -Since this is a **verification exercise** rather than an active migration, source control strategy focuses on documenting the current state and ensuring any validation findings are properly tracked. - -### Branching Strategy - -**Current Branch**: `ssr` (as requested by user) - -**Approach**: Work directly on the `ssr` branch, as no destructive changes are anticipated. - -**Rationale**: -- Projects are already upgraded to net10.0 -- Verification activities do not modify code -- Any documentation updates are additive, not breaking -- Low risk of needing to revert changes - -**Alternative Approach** (if preferred): -- Create a verification branch from `ssr` -- Perform all validation activities -- Merge back to `ssr` after successful verification -- This provides isolation but adds overhead for minimal benefit - -### Commit Strategy - -**Approach**: Single commit after successful verification - -**Recommended Commit Message**: -``` -Verify .NET 10.0 upgrade - all projects confirmed on target framework - -- Confirmed all .NET projects targeting net10.0 -- Verified all 14 packages compatible with .NET 10.0 -- Built solution successfully with zero errors/warnings -- Executed all tests (acceptance + unit) - all passing -- Validated Shogi.Database.sqlproj remains on net472 as expected -- No security vulnerabilities detected -- All-At-Once strategy already completed - -Assessment: Low complexity, verification complete -``` - -**Alternative**: Multiple commits if validation reveals issues requiring fixes: -- Commit 1: "Verify project files and target frameworks" -- Commit 2: "Fix [specific issue] discovered during validation" -- Commit 3: "Confirm all tests pass on .NET 10.0" - -### Code Review and Merge Process - -**Review Requirements**: Minimal, since no code changes are expected - -**Checklist for Review** (if formal review required): -- [ ] All projects verified on correct target framework -- [ ] Build succeeds with zero errors/warnings -- [ ] All tests pass (acceptance + unit) -- [ ] Documentation updated (if applicable) -- [ ] No new security vulnerabilities introduced -- [ ] Verification results documented - -**Merge Criteria**: -- All validation checkpoints passed -- Tests are green -- No blocking issues discovered - -### Repository State - -**Current State** (as noted by user): -- **Branch**: `ssr` -- **Pending Changes**: Exist (user requested to proceed without git operations) - -**Recommendation**: -Since user requested to skip git operations, work can proceed directly. However, best practice would be to: -1. Commit or stash pending changes before verification -2. Perform verification activities -3. Document results -4. Commit verification results separately - -**User Preference**: Proceed without git operations - verification can be performed on current state. - -### Documentation Updates - -**Files to Update** (if applicable): -- README.md: Confirm .NET 10.0 is documented as target framework -- CHANGELOG.md: Add entry for .NET 10.0 verification -- Deployment documentation: Update any .NET version references -- CI/CD configuration: Ensure build pipelines target .NET 10.0 SDK - -### All-At-Once Strategy Source Control Guidance - -Since the All-At-Once upgrade appears to have already been completed (all projects simultaneously updated to net10.0), the source control strategy is retrospective: - -**Original Migration** (already completed): -- Appears to have been done atomically (all projects upgraded together) -- Single-commit approach was likely used -- All package updates applied simultaneously -- Clean result with no intermediate states - -**Current Verification**: -- Confirm the atomic upgrade was successful -- Validate no issues remain from the original migration -- Document the current verified state - ---- - -## Success Criteria - -### Technical Criteria - -**Project Framework Verification**: ✅ Target Met (Pending Validation) -- [ ] All .NET projects target net10.0: - - [ ] BoardRules\BoardRules.csproj: `net10.0` - - [ ] Shogi\Shogi.csproj: `net10.0` - - [ ] Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj: `net10.0` - - [ ] Tests\UnitTests\UnitTests.csproj: `net10.0` -- [ ] Shogi.Database\Shogi.Database.sqlproj correctly remains on net472 - -**Package Compatibility**: ✅ Target Met (Pending Validation) -- [ ] All 14 NuGet packages verified at .NET 10.0-compatible versions -- [ ] Zero packages with security vulnerabilities -- [ ] All package references restore successfully -- [ ] No package dependency conflicts - -**Build Success**: 🔄 Requires Validation -- [ ] `dotnet restore` succeeds for entire solution -- [ ] `dotnet build` succeeds for entire solution (exit code 0) -- [ ] Zero compilation errors across all 5 projects -- [ ] Zero compilation warnings across all 5 projects -- [ ] All project references resolve correctly - -**Test Success**: 🔄 Requires Validation -- [ ] All acceptance tests execute -- [ ] All acceptance tests pass (100% pass rate) -- [ ] All unit tests execute -- [ ] All unit tests pass (100% pass rate) -- [ ] Code coverage collection works (coverlet.collector) - -**Runtime Success**: 🔄 Requires Validation (if applicable) -- [ ] Application starts successfully -- [ ] No runtime exceptions related to .NET version -- [ ] All major features functional -- [ ] Database connectivity works -- [ ] SignalR functionality operates (if used) -- [ ] Identity/authentication works (if used) - -### Quality Criteria - -**Code Quality**: ✅ Already Maintained -- [ ] No new code quality issues introduced -- [ ] Existing code quality maintained -- [ ] No deprecated API usage warnings - -**Test Coverage**: ✅ Already Maintained -- [ ] Test coverage maintained at previous levels -- [ ] No loss of test coverage -- [ ] All existing tests still relevant - -**Documentation**: 🔄 Requires Update (if applicable) -- [ ] README updated to reference .NET 10.0 (if not already) -- [ ] Deployment documentation reflects .NET 10.0 -- [ ] CI/CD pipeline documentation updated -- [ ] CHANGELOG includes .NET 10.0 verification entry - -### Process Criteria - -**All-At-Once Strategy Compliance**: ✅ Already Applied -- [ ] All-At-Once approach was used (all projects upgraded simultaneously) -- [ ] No intermediate states exist -- [ ] All projects on same target framework (net10.0) -- [ ] Atomic upgrade principle followed - -**Verification Methodology**: 🔄 Requires Execution -- [ ] Systematic project-by-project verification performed -- [ ] Solution-level build verification performed -- [ ] Comprehensive test execution completed -- [ ] Results documented - -**Source Control**: 🔄 Conditional -- [ ] Verification results committed (if git operations performed) -- [ ] Documentation updates committed (if applicable) -- [ ] Clean working directory (or documented pending changes) - -### Completion Definition - -**The verification is complete and successful when:** - -1. ✅ **All .NET projects confirmed on net10.0** - - Project files inspected and verified - - Shogi.Database.sqlproj confirmed on net472 (as expected) - -2. ✅ **All packages confirmed compatible** - - 14 packages verified at compatible versions - - Zero security vulnerabilities - - No deprecated or obsolete packages - -3. ✅ **Solution builds successfully** - - Restore succeeds - - Build succeeds with zero errors - - Zero build warnings - -4. ✅ **All tests pass** - - Acceptance tests: 100% pass - - Unit tests: 100% pass - - No skipped or failing tests - -5. ✅ **Application runs correctly** (if applicable) - - Starts without errors - - Core functionality verified - - No runtime exceptions - -6. ✅ **Documentation updated** (if applicable) - - Version references corrected - - Deployment guides updated - - Verification documented - -### Acceptance Sign-Off - -**Criteria for Final Acceptance:** - -- All technical criteria marked complete ✅ -- All quality criteria marked complete ✅ -- All process criteria marked complete ✅ -- No blocking issues remain -- Verification results documented and committed - -**Final Deliverable**: Verified .NET 10.0 solution with documented validation results, all projects on target framework, all tests passing, ready for deployment. - -### Notes - -This is a **verification scenario**, not an active migration. Success is measured by confirming the current state (net10.0) is correct and functional, not by completing migration activities. The assessment indicates the upgrade has already been completed successfully; verification activities will confirm this assessment is accurate. diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json deleted file mode 100644 index 6c714ec..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/scenario.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "scenarioId": "New-DotNet-Version", - "operationId": "a1d9f56b-ad01-4c78-bbfa-17a76f5681a3", - "description": "Upgrade solution or project to new version of .NET", - "startTime": "2026-01-14T02:57:44.7961447Z", - "lastUpdateTime": "2026-01-14T03:37:51.3730402Z", - "stage": "Execution", - "properties": { - "SelectedScenarioStrategy": "AllAtOnce", - "upgradeTargetFramework": "net10.0" - } -} \ No newline at end of file diff --git a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md b/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md deleted file mode 100644 index b22a0d6..0000000 --- a/.github/upgrades/scenarios/new-dotnet-version_a1d9f5/tasks.md +++ /dev/null @@ -1,59 +0,0 @@ -# Shogi .NET 10.0 Upgrade Verification Tasks - -## Overview - -This document tracks the verification of the Shogi solution upgrade to .NET 10.0. All projects are already targeting net10.0, and this verification confirms the upgrade is complete and functional. - -**Progress**: 0/4 tasks complete (0%) ![0%](https://progress-bar.xyz/0) - ---- - -## Tasks - -### [▶] TASK-001: Verify .NET 10.0 SDK and configuration prerequisites -**References**: Plan §Migration Strategy - Phase 0 - -- [✓] (1) Verify .NET 10.0 SDK is installed and available -- [✓] (2) .NET 10.0 SDK version meets minimum requirements (**Verify**) -- [▶] (3) Check for global.json file and verify compatibility if present -- [ ] (4) global.json compatible with .NET 10.0 or no global.json present (**Verify**) - ---- - -### [ ] TASK-002: Verify all projects target net10.0 and build solution -**References**: Plan §Migration Strategy - Phase 1, Plan §Migration Strategy - Phase 2, Plan §Project-by-Project Migration Plans, Plan §Package Update Reference - -- [ ] (1) Verify TargetFramework is net10.0 in BoardRules\BoardRules.csproj -- [ ] (2) Verify TargetFramework is net10.0 in Shogi\Shogi.csproj -- [ ] (3) Verify TargetFramework is net10.0 in Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj -- [ ] (4) Verify TargetFramework is net10.0 in Tests\UnitTests\UnitTests.csproj -- [ ] (5) Verify TargetFramework is net472 in Shogi.Database\Shogi.Database.sqlproj (SQL database project remains on .NET Framework) -- [ ] (6) All .NET projects correctly targeting net10.0 and SQL project correctly on net472 (**Verify**) -- [ ] (7) Verify all 14 package references are at compatible versions per Plan §Package Update Reference -- [ ] (8) All packages are at .NET 10.0-compatible versions (**Verify**) -- [ ] (9) Restore dependencies for entire solution -- [ ] (10) All dependencies restore successfully (**Verify**) -- [ ] (11) Build entire solution -- [ ] (12) Solution builds with 0 errors and 0 warnings (**Verify**) - ---- - -### [ ] TASK-003: Run full test suite and validate -**References**: Plan §Migration Strategy - Phase 3, Plan §Testing & Validation Strategy - -- [ ] (1) Run tests in Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj -- [ ] (2) Fix any acceptance test failures if found -- [ ] (3) Run tests in Tests\UnitTests\UnitTests.csproj -- [ ] (4) Fix any unit test failures if found -- [ ] (5) Re-run all tests after fixes (if any fixes were needed) -- [ ] (6) All tests pass with 0 failures (**Verify**) - ---- - -### [ ] TASK-004: Commit verification results -**References**: Plan §Source Control Strategy - -- [ ] (1) Commit all changes with message: "Verify .NET 10.0 upgrade - all projects confirmed on target framework" - ---- - diff --git a/Tests/AcceptanceTests/ApiTests.cs b/Tests/AcceptanceTests/ApiTests.cs index 8d4824e..e4b3927 100644 --- a/Tests/AcceptanceTests/ApiTests.cs +++ b/Tests/AcceptanceTests/ApiTests.cs @@ -1,5 +1,5 @@ using Shogi.AcceptanceTests.TestSetup; -using Shogi.Types; +using Shogi.BackEnd.Types; using System.Net; using System.Net.Http.Json; diff --git a/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs b/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs index c428c29..f94477f 100644 --- a/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs +++ b/Tests/AcceptanceTests/TestSetup/AatTestFixture.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Shogi.Identity; +using Shogi.BackEnd.Identity; using System.Net.Http.Json; using System.Text.Json; @@ -24,29 +25,25 @@ public class AatTestFixture : WebApplicationFactory, IAsyncLifetime { builder.UseEnvironment("Development"); - builder.ConfigureServices(services => + builder.ConfigureTestServices(services => + { + // Remove all EF Core and database provider related services + var descriptorsToRemove = services + .Where(d => d.ServiceType.Namespace != null && + (d.ServiceType.Namespace.StartsWith("Microsoft.EntityFrameworkCore") || + d.ServiceType == typeof(ApplicationDbContext) || + d.ServiceType == typeof(DbContextOptions))) + .ToList(); + + foreach (var descriptor in descriptorsToRemove) { - // Remove the existing DbContext registration - var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + services.Remove(descriptor); + } - if (descriptor != null) - { - services.Remove(descriptor); - } - - // Add in-memory database for testing - services.AddDbContext(options => - { - options.UseInMemoryDatabase("IntegrationTestDb_" + Guid.NewGuid().ToString()); - }); - - // Ensure the database is created and seeded - var sp = services.BuildServiceProvider(); - using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); - }); + // Add DbContext with InMemory database + services.AddDbContext(options => + options.UseInMemoryDatabase("IntegrationTestDb_" + Guid.NewGuid().ToString())); + }); } public async Task InitializeAsync() @@ -54,6 +51,13 @@ public class AatTestFixture : WebApplicationFactory, IAsyncLifetime this.HttpClient = this.CreateClient(); this.OtherHttpClient = this.CreateClient(); + // Ensure the in-memory database is created + using (var scope = this.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + } + await this.SetupTestAccountsAndLogin(); } From a3f23b199aa71fc21ceb5972c9fd96ce5ec81b87 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 14 Jan 2026 20:36:38 -0600 Subject: [PATCH 08/15] ignore appsettings.dev file --- .gitignore | 1 + Shogi/appsettings.Development.json | 13 ------------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 Shogi/appsettings.Development.json diff --git a/.gitignore b/.gitignore index 832cf47..d3310fc 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ obj *.user /Shogi.Database/Shogi.Database.dbmdl /Shogi.Database/Shogi.Database.jfm +/Shogi/appsettings.Development.json diff --git a/Shogi/appsettings.Development.json b/Shogi/appsettings.Development.json deleted file mode 100644 index 47aef72..0000000 --- a/Shogi/appsettings.Development.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "ApiKeys": { - "BrevoEmailService": "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" - }, - "TestUserPassword": "I'mAToysRUsK1d" -} From 114025fcfb651b450279e162328c9d7fb68242da Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 14 Jan 2026 22:04:37 -0600 Subject: [PATCH 09/15] all the things --- .../BackEnd/Controllers/AccountController.cs | 35 +++++++- ...entityApiEndpointRouteBuilderExtensions.cs | 2 +- .../BackEnd/Controllers/SessionsController.cs | 2 +- .../CookieAuthenticationStateProvider.cs | 37 ++++---- .../FrontEnd/Components/Layout/NavMenu.razor | 4 +- .../Components/Layout/NavMenu.razor.css | 5 +- .../Components/Pages/Home/HomePage.razor | 76 ++++++++++++----- .../Components/Pages/Home/HomePage.razor.css | 69 ++++++++++++++- .../Home/VisualAids/PieceMovesVisualAid.razor | 8 +- .../VisualAids/PromotedPieceVisualAid.razor | 4 +- .../Components/Pages/Identity/LoginPage.razor | 2 +- Shogi/Program.cs | 84 +++++++++---------- Shogi/Shogi.csproj | 4 + 13 files changed, 233 insertions(+), 99 deletions(-) diff --git a/Shogi/BackEnd/Controllers/AccountController.cs b/Shogi/BackEnd/Controllers/AccountController.cs index dda50b7..0c8f402 100644 --- a/Shogi/BackEnd/Controllers/AccountController.cs +++ b/Shogi/BackEnd/Controllers/AccountController.cs @@ -2,17 +2,20 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Shogi.BackEnd.Identity; +using Shogi.BackEnd.Repositories; using System.Security.Claims; namespace Shogi.BackEnd.Controllers; [Authorize] -[Route("[controller]")] +[Route("backend/[controller]")] [ApiController] public class AccountController( SignInManager signInManager, UserManager UserManager, - IConfiguration configuration) : ControllerBase + IConfiguration configuration, + SessionRepository sessionRepository, + QueryRepository queryRepository) : ControllerBase { [Authorize("Admin")] [HttpPost("TestAccount")] @@ -36,7 +39,31 @@ public class AccountController( return this.Created(); } - [HttpPost("/logout")] + [Authorize("Admin")] + [HttpDelete("TestAccount")] + public async Task DeleteTestAccounts() + { + var testUsers = new[] { "aat-account", "aat-account-2" }; + + foreach (var username in testUsers) + { + var user = await UserManager.FindByNameAsync(username); + if (user != null) + { + var sessions = await queryRepository.ReadSessionsMetadata(user.Id); + foreach (var session in sessions) + { + await sessionRepository.DeleteSession(session.Id); + } + + await UserManager.DeleteAsync(user); + } + } + + return this.Ok(); + } + + [HttpPost("/backend/logout")] public async Task Logout([FromBody] object empty) { // https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support @@ -50,7 +77,7 @@ public class AccountController( return this.Unauthorized(); } - [HttpGet("/roles")] + [HttpGet("/backend/roles")] public IActionResult GetRoles() { if (this.User.Identity is not null && this.User.Identity.IsAuthenticated) diff --git a/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs b/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs index d64764f..493521a 100644 --- a/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs +++ b/Shogi/BackEnd/Controllers/MyIdentityApiEndpointRouteBuilderExtensions.cs @@ -47,7 +47,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions // We'll figure out a unique endpoint name based on the final route pattern during endpoint generation. string? confirmEmailEndpointName = null; - var routeGroup = endpoints.MapGroup(""); + var routeGroup = endpoints.MapGroup("backend"); // NOTE: We cannot inject UserManager directly because the TUser generic parameter is currently unsupported by RDG. // https://github.com/dotnet/aspnetcore/issues/47338 diff --git a/Shogi/BackEnd/Controllers/SessionsController.cs b/Shogi/BackEnd/Controllers/SessionsController.cs index f6695f6..91076cb 100644 --- a/Shogi/BackEnd/Controllers/SessionsController.cs +++ b/Shogi/BackEnd/Controllers/SessionsController.cs @@ -9,7 +9,7 @@ namespace Shogi.BackEnd.Controllers; [Authorize] [ApiController] -[Route("[controller]")] +[Route("backend/[controller]")] public class SessionsController( SessionRepository sessionRepository, ShogiApplication application) : ControllerBase diff --git a/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs b/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs index fb5f1ac..30fc561 100644 --- a/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs +++ b/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs @@ -10,7 +10,11 @@ using System.Text.Json; /// /// Handles state for cookie-based auth. /// -public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IAccountManagement +/// +/// Create a new instance of the auth provider. +/// +/// Factory to retrieve auth client. +public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) : AuthenticationStateProvider, IAccountManagement { /// /// Map the JavaScript-formatted properties to C#-formatted classes. @@ -23,12 +27,12 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA /// /// Special auth client. /// - private readonly HttpClient _httpClient; + private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth"); /// /// Authentication state. /// - private bool _authenticated = false; + private bool authenticated = false; /// /// Default principal for anonymous (not authenticated) users. @@ -36,13 +40,6 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA private readonly ClaimsPrincipal Unauthenticated = new(new ClaimsIdentity()); - /// - /// Create a new instance of the auth provider. - /// - /// Factory to retrieve auth client. - public CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) - => _httpClient = httpClientFactory.CreateClient("Auth"); - /// /// Register a new user. /// @@ -57,7 +54,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA try { // make the request - var result = await _httpClient.PostAsJsonAsync("register", new + var result = await httpClient.PostAsJsonAsync("backend/register", new { email, password @@ -118,7 +115,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA try { // login with cookies - var result = await _httpClient.PostAsJsonAsync("login?useCookies=true", new + var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new { email, password @@ -154,7 +151,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA /// The authentication state asynchronous request. public override async Task GetAuthenticationStateAsync() { - _authenticated = false; + authenticated = false; // default to not authenticated var user = Unauthenticated; @@ -162,7 +159,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA try { // the user info endpoint is secured, so if the user isn't logged in this will fail - var userResponse = await _httpClient.GetAsync("manage/info"); + var userResponse = await httpClient.GetAsync("backend/manage/info"); // throw if user info wasn't retrieved userResponse.EnsureSuccessStatusCode(); @@ -187,7 +184,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA .Select(c => new Claim(c.Key, c.Value))); // tap the roles endpoint for the user's roles - var rolesResponse = await _httpClient.GetAsync("roles"); + var rolesResponse = await httpClient.GetAsync("backend/roles"); // throw if request fails rolesResponse.EnsureSuccessStatusCode(); @@ -213,7 +210,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA // set the principal var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider)); user = new ClaimsPrincipal(id); - _authenticated = true; + authenticated = true; } } catch { } @@ -226,14 +223,14 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA { const string Empty = "{}"; var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json"); - await _httpClient.PostAsync("logout", emptyContent); + await httpClient.PostAsync("backend/logout", emptyContent); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public async Task CheckAuthenticatedAsync() { await GetAuthenticationStateAsync(); - return _authenticated; + return authenticated; } /// @@ -242,7 +239,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA /// Do not surface errors from this to users which may tell bad actors if emails do or do not exist in the system. public async Task RequestPasswordReset(string email) { - return await _httpClient.PostAsJsonAsync("forgotPassword", new { email }); + return await httpClient.PostAsJsonAsync("backend/forgotPassword", new { email }); } public async Task ChangePassword(string email, string resetCode, string newPassword) @@ -253,7 +250,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA resetCode, newPassword }; - var response = await _httpClient.PostAsJsonAsync("resetPassword", body); + var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body); if (response.IsSuccessStatusCode) { return new FormResult { Succeeded = true }; diff --git a/Shogi/FrontEnd/Components/Layout/NavMenu.razor b/Shogi/FrontEnd/Components/Layout/NavMenu.razor index 2086597..9a7047b 100644 --- a/Shogi/FrontEnd/Components/Layout/NavMenu.razor +++ b/Shogi/FrontEnd/Components/Layout/NavMenu.razor @@ -3,7 +3,6 @@ @* Desktop view *@
+
+
Knight
-
-
Silver General
+
+ +
Gold General
diff --git a/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor index 7fc0731..b5ff475 100644 --- a/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor +++ b/Shogi/FrontEnd/Components/Pages/Home/VisualAids/PromotedPieceVisualAid.razor @@ -21,14 +21,14 @@
Promoted Lance
+
+
Promoted Knight
-
-
Promoted Silver General
diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor index 8befcab..1b532c0 100644 --- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor +++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor @@ -25,7 +25,7 @@ } - + @if (isEmailSubmitted) { diff --git a/Shogi/Program.cs b/Shogi/Program.cs index 972a664..f40b040 100644 --- a/Shogi/Program.cs +++ b/Shogi/Program.cs @@ -4,25 +4,25 @@ using Microsoft.AspNetCore.ResponseCompression; using Microsoft.EntityFrameworkCore; using Shogi; using Shogi.BackEnd.Application; -using Shogi.FrontEnd.Components; using Shogi.BackEnd.Controllers; using Shogi.BackEnd.Identity; using Shogi.BackEnd.Repositories; using Shogi.FrontEnd.Client; +using Shogi.FrontEnd.Components; var builder = WebApplication.CreateBuilder(args); // Add Blazor components builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); + .AddInteractiveServerComponents(); // Add API controllers builder.Services - .AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.WriteIndented = true; - }); + .AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.WriteIndented = true; + }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -46,7 +46,7 @@ AddIdentity(builder, builder.Configuration); builder.Services.AddSignalR(); builder.Services.AddResponseCompression(opts => { - opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); }); var app = builder.Build(); @@ -55,13 +55,13 @@ app.MyMapIdentityApi(builder.Environment); if (app.Environment.IsDevelopment()) { - app.UseHttpsRedirection(); + app.UseHttpsRedirection(); } else { - app.UseExceptionHandler("/Error"); - app.UseHsts(); - app.UseResponseCompression(); + app.UseExceptionHandler("/Error"); + app.UseHsts(); + app.UseResponseCompression(); } app.UseStaticFiles(); @@ -74,43 +74,43 @@ app.UseAuthorization(); app.MapControllers(); app.MapHub("/gamehub"); app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveServerRenderMode(); app.Run(); static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration) { - builder.Services - .AddAuthorizationBuilder() - .AddPolicy("Admin", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireAssertion(context => context.User?.Identity?.Name switch - { - "Hauth@live.com" => true, - "aat-account" => true, - _ => false - }); - }); + builder.Services + .AddAuthorizationBuilder() + .AddPolicy("Admin", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireAssertion(context => context.User?.Identity?.Name switch + { + "Hauth@live.com" => true, + "aat-account" => true, + _ => false + }); + }); - builder.Services - .AddDbContext(options => - { - var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured."); - options.UseSqlServer(cs); - }) - .AddIdentityApiEndpoints(options => - { - options.SignIn.RequireConfirmedEmail = true; - options.User.RequireUniqueEmail = true; - }) - .AddEntityFrameworkStores(); + builder.Services + .AddDbContext(options => + { + var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured."); + options.UseSqlServer(cs); + }) + .AddIdentityApiEndpoints(options => + { + options.SignIn.RequireConfirmedEmail = true; + options.User.RequireUniqueEmail = true; + }) + .AddEntityFrameworkStores(); - builder.Services.ConfigureApplicationCookie(options => - { - options.SlidingExpiration = true; - options.ExpireTimeSpan = TimeSpan.FromDays(3); - }); + builder.Services.ConfigureApplicationCookie(options => + { + options.SlidingExpiration = true; + options.ExpireTimeSpan = TimeSpan.FromDays(3); + }); } // Make Program accessible for WebApplicationFactory in integration tests diff --git a/Shogi/Shogi.csproj b/Shogi/Shogi.csproj index f20c609..1d24355 100644 --- a/Shogi/Shogi.csproj +++ b/Shogi/Shogi.csproj @@ -82,6 +82,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 4c52b3bde4cb3d11901502e6ff0978aba636d915 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 15 Jan 2026 20:05:22 -0600 Subject: [PATCH 10/15] update dotnet tools --- Shogi/.config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shogi/.config/dotnet-tools.json b/Shogi/.config/dotnet-tools.json index 50a0457..cf20ad1 100644 --- a/Shogi/.config/dotnet-tools.json +++ b/Shogi/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "6.0.5", + "version": "10.0.2", "commands": [ "dotnet-ef" ] From 79dd554afa814dd125ab3cc81984c6fc9a799f48 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 15 Jan 2026 20:06:01 -0600 Subject: [PATCH 11/15] yep --- README.md | 43 ++++++++++++---- Shogi.sln | 1 + Shogi/FrontEnd/Components/Routes.razor | 2 +- Shogi/Program.cs | 68 ++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cd0cb24..121322a 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,45 @@ A web application for playing the Shogi boardgame with others, developed as a ho The application uses sockets to allow players to enjoy sessions in real time. ### Technologies used -A Blazor UI backed by an Asp.net Core API service which uses Sql Server for presistent storage. - -### Known Issues - * The app is intended to support logging in via Microsoft accounts or browser-session (Guest) accounts, but currently Microsoft login does not work. - * The workaround is to use the guest login. - * On first load of the UI, guest account login will fail. - * The workaround is to refresh the page and try again. This issue only happens on first load. +A Blazor Web App which uses Sql Server for presistent storage and Identity EF Core for account management. ### Roadmap of features remaining The app is not yet finished, though much of the functionality exists. Here is a list of what remains. * Placing pieces from the hand onto the board. - * Checkmate experience in UI + * Checkmate experience * Preventing the rarely invoked rule where check-mate cannot be gained by placing a pawn from the hand. * Retaining an archive of games played and move history of each game. - * Adaptive UI layout for varying viewport (screen) sizes. \ No newline at end of file + * Adaptive UI layout for varying viewport (screen) sizes. + +### Database Setup +If you don't have them, install the `Data storage and processing` tools through Visual Studio Installer. This gives you a local MSSQL database for you to develop with, which is nice so you aren't touching live data while working. + +After that, you need to set up the database. The database has two sources of table structure. + +#### 1. Shogi.Database project +This project contains the table structure for the game. + +1. Build the Shogi.Database project. +1. Publish the Shogi.Database project (right click in Solution Explorer). + * If you're prompted for a connection string, use the one from `Shogi/appsettings.json`. + +#### 2. EntityFramework via AspNetCore.Identity +This solution uses the `Microsoft.AspNetCore.Identity.EntityFrameworkCore` package to offer authentication and authorization. This uses Entity Framework, which comes with tools to setup our database with the necessary table structure for auth. + +1. Install the Entity Framework dotnet tools that come with the project. Via Powershell run this command: + ``` + dotnet tool restore + ``` +1. Run the database migrations and fill out table structure for auth. Run this command from the MUD.Api project directory: + ``` + cd /path/to/solution/Shogi + dotnet ef database update + ``` + +After this, you should be ready to create some test accounts for local development. + +### Creating Test Accounts +To create test accounts for local development, make sure to build in DEBUG mode and then use the `/debug/create-test-accounts` endpoint +of the API. This will create two accounts. If those accounts already exist, they'll be deleted (along with all game session data associated) and recreated. \ No newline at end of file diff --git a/Shogi.sln b/Shogi.sln index d1948fd..5dcac8c 100644 --- a/Shogi.sln +++ b/Shogi.sln @@ -8,6 +8,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig .gitignore = .gitignore azure-pipelines.yml = azure-pipelines.yml + Shogi\.config\dotnet-tools.json = Shogi\.config\dotnet-tools.json global.json = global.json README.md = README.md EndProjectSection diff --git a/Shogi/FrontEnd/Components/Routes.razor b/Shogi/FrontEnd/Components/Routes.razor index 71ddacf..f3d6a9f 100644 --- a/Shogi/FrontEnd/Components/Routes.razor +++ b/Shogi/FrontEnd/Components/Routes.razor @@ -2,7 +2,7 @@ - + @* *@ Not found diff --git a/Shogi/Program.cs b/Shogi/Program.cs index f40b040..668ca59 100644 --- a/Shogi/Program.cs +++ b/Shogi/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.EntityFrameworkCore; @@ -76,6 +77,10 @@ app.MapHub("/gamehub"); app.MapRazorComponents() .AddInteractiveServerRenderMode(); +#if DEBUG +MapTestAccountSetupRoute(app); +#endif + app.Run(); static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration) @@ -113,5 +118,68 @@ static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager conf }); } +static void MapTestAccountSetupRoute(WebApplication app) +{ + app.MapPost("/debug/create-test-users", async ( + UserManager userManager, + QueryRepository queryRepository, + SessionRepository sessionRepository) => + { + var testAccounts = new[] + { + new { Email = "test1@example.com", Password = "Test123!" }, + new { Email = "test2@example.com", Password = "Test123!" } + }; + + var createdUsers = new List(); + var errors = new List(); + + foreach (var account in testAccounts) + { + // Delete existing user and their sessions if they exist + var existingUser = await userManager.FindByEmailAsync(account.Email); + if (existingUser != null) + { + // Delete all game sessions associated with this user + var sessions = await queryRepository.ReadSessionsMetadata(existingUser.Id); + foreach (var session in sessions) + { + await sessionRepository.DeleteSession(session.Id); + } + + // Delete the existing user + await userManager.DeleteAsync(existingUser); + } + + // Create fresh test user + var testUser = new ShogiUser + { + UserName = account.Email, + Email = account.Email, + EmailConfirmed = true + }; + + var result = await userManager.CreateAsync(testUser, account.Password); + + if (result.Succeeded) + { + createdUsers.Add(new { email = account.Email, password = account.Password }); + } + else + { + errors.Add(new { email = account.Email, errors = result.Errors }); + } + } + + if (errors.Count > 0) + { + return Results.BadRequest(new { message = "Failed to create some test users", createdUsers, errors }); + } + + return Results.Ok(new { message = "Test users created successfully", users = createdUsers }); + }) + .WithName("CreateTestUsers"); +} + // Make Program accessible for WebApplicationFactory in integration tests public partial class Program { } From c7cf706d6cc710ca2c877f691654267cdbefb991 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 15 Jan 2026 20:06:16 -0600 Subject: [PATCH 12/15] Update pipeline for ssr --- azure-pipelines.yml | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 15a0c90..3a08455 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,23 +13,17 @@ variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' - apiProjectName: 'Shogi' - uiProjectName: 'Shogi.UI' + projectName: 'Shogi' steps: - task: NuGetToolInstaller@1 - task: UseDotNet@2 -inputs: - packageType: sdk - #useGlobalJson: true - version: 10.x - installationPath: $(Agent.ToolsDirectory)/dotnet #Install if not already present. - -- task: CmdLine@2 inputs: - script: 'dotnet workload install wasm-tools' + packageType: sdk + version: 10.x + installationPath: $(Agent.ToolsDirectory)/dotnet - task: NuGetCommand@2 inputs: @@ -37,13 +31,7 @@ inputs: - task: FileTransform@1 inputs: - folderPath: '$(System.DefaultWorkingDirectory)\$(uiProjectName)' - fileType: 'json' - targetFiles: 'wwwroot/appsettings.json' - -- task: FileTransform@1 - inputs: - folderPath: '$(System.DefaultWorkingDirectory)\$(apiProjectName)' + folderPath: '$(System.DefaultWorkingDirectory)\$(projectName)' fileType: 'json' targetFiles: 'appsettings.json' @@ -51,25 +39,17 @@ inputs: inputs: command: 'publish' publishWebProjects: false - arguments: '-c Release' + projects: '$(projectName)/$(projectName).csproj' + arguments: '-c Release -o $(Build.ArtifactStagingDirectory)' zipAfterPublish: false - task: CopyFilesOverSSH@0 - displayName: "Copy API files." + displayName: "Copy application files" inputs: sshEndpoint: 'LucaServer' - sourceFolder: '$(System.DefaultWorkingDirectory)/$(apiProjectName)/bin/Release/net8.0/publish' + sourceFolder: '$(Build.ArtifactStagingDirectory)' contents: '**' - targetFolder: '/var/www/apps/$(apiProjectName)' - readyTimeout: '20000' - -- task: CopyFilesOverSSH@0 - displayName: "Copy UI files." - inputs: - sshEndpoint: 'LucaServer' - sourceFolder: '$(System.DefaultWorkingDirectory)/$(uiProjectName)/bin/Release/net8.0/publish' - contents: '**' - targetFolder: '/var/www/apps/$(uiProjectName)' + targetFolder: '/var/www/apps/$(projectName)' readyTimeout: '20000' - task: SSH@0 From 2eba8cdb0e33ea51f8dbd13096da7f2a9cbc6496 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 15 Jan 2026 22:53:07 -0600 Subject: [PATCH 13/15] yep --- azure-pipelines.yml | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3a08455..dfb93ab 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,10 +10,15 @@ pool: vmImage: 'windows-latest' variables: - solution: '**/*.sln' - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' - projectName: 'Shogi' +solution: '**/*.sln' +buildPlatform: 'Any CPU' +buildConfiguration: 'Release' +projectName: 'Shogi' +# Database variables - Set these as pipeline variables in Azure DevOps +# DatabaseServer: 'your-sql-server-address' +# DatabaseName: 'ShogiDb' +# DatabaseUser: 'your-db-user' +# DatabasePassword: 'your-db-password' # Mark as secret in Azure DevOps steps: @@ -43,6 +48,37 @@ steps: arguments: '-c Release -o $(Build.ArtifactStagingDirectory)' zipAfterPublish: false +- task: PowerShell@2 + displayName: "Generate EF Core migration script" + inputs: + targetType: 'inline' + script: | + dotnet tool restore --tool-manifest $(projectName)/.config/dotnet-tools.json + dotnet ef migrations script --idempotent --project $(projectName)/$(projectName).csproj --output $(Build.ArtifactStagingDirectory)/migrations.sql + workingDirectory: '$(System.DefaultWorkingDirectory)' + +- task: CopyFilesOverSSH@0 + displayName: "Copy database migration script" + inputs: + sshEndpoint: 'LucaServer' + sourceFolder: '$(Build.ArtifactStagingDirectory)' + contents: 'migrations.sql' + targetFolder: '/var/www/apps/$(projectName)/migrations' + readyTimeout: '20000' + +- task: SSH@0 + displayName: "Apply database migrations" + inputs: + sshEndpoint: 'LucaServer' + runOptions: 'commands' + commands: | + # Install sqlcmd if not already installed (one-time setup) + # which sqlcmd || curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc && curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list && sudo apt-get update && sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev + + # Apply migrations (adjust connection string as needed) + /opt/mssql-tools18/bin/sqlcmd -S $(DatabaseServer) -d $(DatabaseName) -U $(DatabaseUser) -P $(DatabasePassword) -i /var/www/apps/$(projectName)/migrations/migrations.sql -C + readyTimeout: '20000' + - task: CopyFilesOverSSH@0 displayName: "Copy application files" inputs: From 78733295e03163f5bcc74e4931b796138576fdd0 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Fri, 16 Jan 2026 17:14:03 -0600 Subject: [PATCH 14/15] yep --- .../BackEnd/Controllers/AccountController.cs | 26 +- .../BackEnd/Identity/ServerAccountManager.cs | 72 +++++ .../CookieAuthenticationStateProvider.cs | 276 ------------------ Shogi/FrontEnd/Components/App.razor | 9 +- .../FrontEnd/Components/Layout/NavMenu.razor | 4 +- .../Components/Pages/Identity/LoginPage.razor | 117 +------- Shogi/Program.cs | 5 +- Shogi/Properties/launchSettings.json | 49 ++-- 8 files changed, 129 insertions(+), 429 deletions(-) create mode 100644 Shogi/BackEnd/Identity/ServerAccountManager.cs delete mode 100644 Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs diff --git a/Shogi/BackEnd/Controllers/AccountController.cs b/Shogi/BackEnd/Controllers/AccountController.cs index 0c8f402..1de4cf5 100644 --- a/Shogi/BackEnd/Controllers/AccountController.cs +++ b/Shogi/BackEnd/Controllers/AccountController.cs @@ -60,21 +60,29 @@ public class AccountController( } } - return this.Ok(); + return this.NoContent(); } - [HttpPost("/backend/logout")] - public async Task Logout([FromBody] object empty) + [HttpPost("Login")] + [AllowAnonymous] + public async Task Login([FromForm] string email, [FromForm] string password) { - // https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support - if (empty is not null) + var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: true, lockoutOnFailure: false); + if (result.Succeeded) { - await signInManager.SignOutAsync(); - - return this.Ok(); + return Redirect("/"); } - return this.Unauthorized(); + return Redirect("/login?error=Invalid login attempt."); + } + + [HttpGet("Logout")] + [HttpPost("Logout")] + [AllowAnonymous] + public async Task Logout() + { + await signInManager.SignOutAsync(); + return Redirect("/"); } [HttpGet("/backend/roles")] diff --git a/Shogi/BackEnd/Identity/ServerAccountManager.cs b/Shogi/BackEnd/Identity/ServerAccountManager.cs new file mode 100644 index 0000000..b543fda --- /dev/null +++ b/Shogi/BackEnd/Identity/ServerAccountManager.cs @@ -0,0 +1,72 @@ +namespace Shogi.BackEnd.Identity; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.WebUtilities; +using Shogi.FrontEnd.Client; +using System.Text; +using System.Text.Encodings.Web; + +public class ServerAccountManager( + UserManager userManager, + IHttpContextAccessor httpContextAccessor, + IEmailSender emailSender) : IAccountManagement +{ + public async Task RegisterAsync(string email, string password) + { + var user = new ShogiUser { UserName = email, Email = email }; + var result = await userManager.CreateAsync(user, password); + if (result.Succeeded) + { + var userId = await userManager.GetUserIdAsync(user); + var code = await userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + + var request = httpContextAccessor.HttpContext?.Request; + var host = request?.Host.Value ?? "localhost"; + var scheme = request?.Scheme ?? "https"; + var callbackUrl = $"{scheme}://{host}/backend/confirmEmail?userId={userId}&code={code}"; + + await emailSender.SendEmailAsync(email, "Confirm your email", + $"Please confirm your account by clicking here."); + + return new FormResult { Succeeded = true }; + } + return new FormResult { Succeeded = false, ErrorList = result.Errors.Select(e => e.Description).ToArray() }; + } + + public Task LoginAsync(string email, string password) + { + throw new NotSupportedException("Login must be performed via Form POST to /backend/Account/Login in Server-Side Rendering."); + } + + public Task LogoutAsync() + { + // Logout should be performed via Form POST or Link to /backend/Account/Logout. + return Task.CompletedTask; + } + + public Task CheckAuthenticatedAsync() + { + return Task.FromResult(httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false); + } + + public async Task RequestPasswordReset(string email) + { + var user = await userManager.FindByEmailAsync(email); + if (user != null) + { + // Generate token and send email logic would go here. + } + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + } + + public async Task ChangePassword(string email, string resetCode, string newPassword) + { + var user = await userManager.FindByEmailAsync(email); + if (user == null) return new FormResult { Succeeded = false, ErrorList = ["User not found"] }; + var result = await userManager.ResetPasswordAsync(user, resetCode, newPassword); + return new FormResult { Succeeded = result.Succeeded, ErrorList = result.Errors.Select(e => e.Description).ToArray() }; + } +} diff --git a/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs b/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs deleted file mode 100644 index 30fc561..0000000 --- a/Shogi/FrontEnd/Client/CookieAuthenticationStateProvider.cs +++ /dev/null @@ -1,276 +0,0 @@ -namespace Shogi.FrontEnd.Client; - -using Microsoft.AspNetCore.Components.Authorization; -using System.Net.Http; -using System.Net.Http.Json; -using System.Security.Claims; -using System.Text; -using System.Text.Json; - -/// -/// Handles state for cookie-based auth. -/// -/// -/// Create a new instance of the auth provider. -/// -/// Factory to retrieve auth client. -public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) : AuthenticationStateProvider, IAccountManagement -{ - /// - /// Map the JavaScript-formatted properties to C#-formatted classes. - /// - private readonly JsonSerializerOptions jsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - - /// - /// Special auth client. - /// - private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth"); - - /// - /// Authentication state. - /// - private bool authenticated = false; - - /// - /// Default principal for anonymous (not authenticated) users. - /// - private readonly ClaimsPrincipal Unauthenticated = - new(new ClaimsIdentity()); - - /// - /// Register a new user. - /// - /// The user's email address. - /// The user's password. - /// The result serialized to a . - /// - public async Task RegisterAsync(string email, string password) - { - string[] defaultDetail = ["An unknown error prevented registration from succeeding."]; - - try - { - // make the request - var result = await httpClient.PostAsJsonAsync("backend/register", new - { - email, - password - }); - - // successful? - if (result.IsSuccessStatusCode) - { - return new FormResult { Succeeded = true }; - } - - // body should contain details about why it failed - var details = await result.Content.ReadAsStringAsync(); - var problemDetails = JsonDocument.Parse(details); - var errors = new List(); - var errorList = problemDetails.RootElement.GetProperty("errors"); - - foreach (var errorEntry in errorList.EnumerateObject()) - { - if (errorEntry.Value.ValueKind == JsonValueKind.String) - { - errors.Add(errorEntry.Value.GetString()!); - } - else if (errorEntry.Value.ValueKind == JsonValueKind.Array) - { - errors.AddRange( - errorEntry.Value.EnumerateArray().Select( - e => e.GetString() ?? string.Empty) - .Where(e => !string.IsNullOrEmpty(e))); - } - } - - // return the error list - return new FormResult - { - Succeeded = false, - ErrorList = problemDetails == null ? defaultDetail : [.. errors] - }; - } - catch { } - - // unknown error - return new FormResult - { - Succeeded = false, - ErrorList = defaultDetail - }; - } - - /// - /// User login. - /// - /// The user's email address. - /// The user's password. - /// The result of the login request serialized to a . - public async Task LoginAsync(string email, string password) - { - try - { - // login with cookies - var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new - { - email, - password - }); - - // success? - if (result.IsSuccessStatusCode) - { - // need to refresh auth state - NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); - - // success! - return new FormResult { Succeeded = true }; - } - } - catch { } - - // unknown error - return new FormResult - { - Succeeded = false, - ErrorList = ["Invalid email and/or password."] - }; - } - - /// - /// Get authentication state. - /// - /// - /// Called by Blazor anytime and authentication-based decision needs to be made, then cached - /// until the changed state notification is raised. - /// - /// The authentication state asynchronous request. - public override async Task GetAuthenticationStateAsync() - { - authenticated = false; - - // default to not authenticated - var user = Unauthenticated; - - try - { - // the user info endpoint is secured, so if the user isn't logged in this will fail - var userResponse = await httpClient.GetAsync("backend/manage/info"); - - // throw if user info wasn't retrieved - userResponse.EnsureSuccessStatusCode(); - - // user is authenticated,so let's build their authenticated identity - var userJson = await userResponse.Content.ReadAsStringAsync(); - var userInfo = JsonSerializer.Deserialize(userJson, jsonSerializerOptions); - - if (userInfo != null) - { - // in our system name and email are the same - var claims = new List - { - new(ClaimTypes.Name, userInfo.Email), - new(ClaimTypes.Email, userInfo.Email) - }; - - // add any additional claims - claims.AddRange( - userInfo.Claims - .Where(c => c.Key != ClaimTypes.Name && c.Key != ClaimTypes.Email) - .Select(c => new Claim(c.Key, c.Value))); - - // tap the roles endpoint for the user's roles - var rolesResponse = await httpClient.GetAsync("backend/roles"); - - // throw if request fails - rolesResponse.EnsureSuccessStatusCode(); - - // read the response into a string - var rolesJson = await rolesResponse.Content.ReadAsStringAsync(); - - // deserialize the roles string into an array - var roles = JsonSerializer.Deserialize(rolesJson, jsonSerializerOptions); - - // if there are roles, add them to the claims collection - if (roles?.Length > 0) - { - foreach (var role in roles) - { - if (!string.IsNullOrEmpty(role.Type) && !string.IsNullOrEmpty(role.Value)) - { - claims.Add(new Claim(role.Type, role.Value, role.ValueType, role.Issuer, role.OriginalIssuer)); - } - } - } - - // set the principal - var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider)); - user = new ClaimsPrincipal(id); - authenticated = true; - } - } - catch { } - - // return the state - return new AuthenticationState(user); - } - - public async Task LogoutAsync() - { - const string Empty = "{}"; - var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json"); - await httpClient.PostAsync("backend/logout", emptyContent); - NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); - } - - public async Task CheckAuthenticatedAsync() - { - await GetAuthenticationStateAsync(); - return authenticated; - } - - /// - /// Ask for an email to be sent which contains a reset code. This reset code is used during - /// - /// Do not surface errors from this to users which may tell bad actors if emails do or do not exist in the system. - public async Task RequestPasswordReset(string email) - { - return await httpClient.PostAsJsonAsync("backend/forgotPassword", new { email }); - } - - public async Task ChangePassword(string email, string resetCode, string newPassword) - { - var body = new - { - email, - resetCode, - newPassword - }; - var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body); - if (response.IsSuccessStatusCode) - { - return new FormResult { Succeeded = true }; - } - else - { - return new FormResult - { - Succeeded = false, - ErrorList = [await response.Content.ReadAsStringAsync()] - }; - } - } - - public class RoleClaim - { - public string? Issuer { get; set; } - public string? OriginalIssuer { get; set; } - public string? Type { get; set; } - public string? Value { get; set; } - public string? ValueType { get; set; } - } -} diff --git a/Shogi/FrontEnd/Components/App.razor b/Shogi/FrontEnd/Components/App.razor index df9f838..57d6e1f 100644 --- a/Shogi/FrontEnd/Components/App.razor +++ b/Shogi/FrontEnd/Components/App.razor @@ -12,7 +12,14 @@ - + + diff --git a/Shogi/FrontEnd/Components/Layout/NavMenu.razor b/Shogi/FrontEnd/Components/Layout/NavMenu.razor index 9a7047b..d1f400e 100644 --- a/Shogi/FrontEnd/Components/Layout/NavMenu.razor +++ b/Shogi/FrontEnd/Components/Layout/NavMenu.razor @@ -17,7 +17,7 @@ @context.User.Identity?.Name - Logout + Logout Login @@ -53,7 +53,7 @@
  • - Logout + Logout
  • diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor index 1b532c0..dcf0e9c 100644 --- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor +++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor @@ -1,41 +1,33 @@ @page "/login" -@inject IAccountManagement Acct @inject NavigationManager navigator
    -
    +

    Login

    You're logged in as @context.User.Identity?.Name.
    - @if (errorList.Length > 0) + @if (!string.IsNullOrEmpty(Error)) {
      - @foreach (var error in errorList) - { -
    • @error
    • - } +
    • @Error
    -
    } - + - @if (isEmailSubmitted) - { - - + + - Reset password - } + Reset password - +
    @@ -43,95 +35,6 @@
    @code { - - private string email = string.Empty; - private string password = string.Empty; - private string[] errorList = []; - private System.Threading.CancellationTokenSource? _clearErrorCts; - private bool isEmailSubmitted = false; - private Guid errorKey; - - private async Task HandleSubmit() - { - if (!isEmailSubmitted) - { - SubmitEmail(); - } - else - { - await DoLoginAsync(); - } - } - - private void SubmitEmail() - { - SetErrors([]); - - if (string.IsNullOrWhiteSpace(email)) - { - SetErrors(["Email is required."]); - return; - } - - isEmailSubmitted = true; - } - - public async Task DoLoginAsync() - { - SetErrors([]); - - if (string.IsNullOrWhiteSpace(email)) - { - SetErrors(["Email is required."]); - - return; - } - - if (string.IsNullOrWhiteSpace(password)) - { - SetErrors(["Password is required."]); - - return; - } - - var result = await Acct.LoginAsync(email, password); - - if (result.Succeeded) - { - email = password = string.Empty; - - navigator.NavigateTo(""); - } - else - { - SetErrors(result.ErrorList); - } - } - - private void SetErrors(string[] errors) - { - _clearErrorCts?.Cancel(); - errorList = errors; - - if (errors.Length > 0) - { - errorKey = Guid.NewGuid(); - _clearErrorCts = new System.Threading.CancellationTokenSource(); - _ = ClearErrorsAfterDelay(_clearErrorCts.Token); - } - } - - private async Task ClearErrorsAfterDelay(System.Threading.CancellationToken token) - { - try - { - await Task.Delay(10000, token); - errorList = []; - await InvokeAsync(StateHasChanged); - } - catch (TaskCanceledException) - { - // Ignore - } - } + [SupplyParameterFromQuery] + public string? Error { get; set; } } \ No newline at end of file diff --git a/Shogi/Program.cs b/Shogi/Program.cs index 668ca59..a0d12e8 100644 --- a/Shogi/Program.cs +++ b/Shogi/Program.cs @@ -39,9 +39,8 @@ builder.Services.Configure(builder.Configuration.GetSection("ApiKeys")) builder.Services.AddTransient(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(sp => sp.GetRequiredService()); -builder.Services.AddScoped(sp => sp.GetRequiredService()); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); AddIdentity(builder, builder.Configuration); builder.Services.AddSignalR(); diff --git a/Shogi/Properties/launchSettings.json b/Shogi/Properties/launchSettings.json index 98dbd51..b369ea4 100644 --- a/Shogi/Properties/launchSettings.json +++ b/Shogi/Properties/launchSettings.json @@ -1,35 +1,22 @@ { -"profiles": { - "Kestrel": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/", - "AZURE_USERNAME": "Hauth@live.com" + "profiles": { + "Shogi UI": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - }, - "Swagger": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/", - "AZURE_USERNAME": "Hauth@live.com" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } -}, - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50728/", - "sslPort": 44315 + "Swagger UI": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" } - } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" } \ No newline at end of file From 7e5951da2d1cce368d44e19dafade8ef692708d1 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 25 Jan 2026 12:46:00 -0600 Subject: [PATCH 15/15] remove aat project from solution --- Shogi.sln | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Shogi.sln b/Shogi.sln index 5dcac8c..2e8993e 100644 --- a/Shogi.sln +++ b/Shogi.sln @@ -19,8 +19,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\Bo EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.AcceptanceTests", "Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj", "{768F37ED-FB62-A57F-BCFA-91F26B4F794F}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{9D1DD2CD-7B04-4472-4377-027563F356CA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi", "Shogi\Shogi.csproj", "{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}" @@ -40,10 +38,6 @@ Global {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU - {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Release|Any CPU.Build.0 = Release|Any CPU {9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D1DD2CD-7B04-4472-4377-027563F356CA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -57,7 +51,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {768F37ED-FB62-A57F-BCFA-91F26B4F794F} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B} {9D1DD2CD-7B04-4472-4377-027563F356CA} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution