From da769174904ac9692c1fd8ce41b8e38f0422f321 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 9 Nov 2022 16:08:04 -0600 Subject: [PATCH] create, read, playercount --- ...ionController.cs => SessionsController.cs} | 73 +- Shogi.Api/Controllers/UserController.cs | 5 +- .../{Extensions.cs => ClaimsExtensions.cs} | 18 +- Shogi.Api/Extensions/ContractsExtensions.cs | 52 ++ Shogi.Api/Program.cs | 43 +- Shogi.Api/Repositories/Dto/BoardStateDto.cs | 22 - Shogi.Api/Repositories/Dto/MoveDto.cs | 10 + Shogi.Api/Repositories/Dto/SessionDto.cs | 11 +- Shogi.Api/Repositories/QueryRepository.cs | 7 +- Shogi.Api/Repositories/SessionRepository.cs | 65 +- Shogi.Api/Services/SocketService.cs | 5 +- Shogi.Api/Shogi.Api.csproj | 4 +- .../Api/Commands/CreateSessionCommand.cs | 3 +- .../Api/Queries/ReadAllSessionsResponse.cs | 2 +- .../ShogiApiJsonSerializerSettings.cs | 11 + Shogi.Contracts/Types/Session.cs | 5 +- .../Post Deployment/Script.PostDeployment.sql | 3 +- .../Scripts/EnableSnapshotIsolationLevel.sql | 2 + .../Scripts/PopulatePieces.sql | 21 + .../Stored Procedures/CreateSession.sql | 9 +- .../Stored Procedures/DeleteSession.sql | 5 + .../Session/Stored Procedures/ReadSession.sql | 37 +- ...etadata.sql => ReadSessionPlayerCount.sql} | 2 +- Shogi.Database/Session/Tables/Move.sql | 20 + Shogi.Database/Session/Tables/Piece.sql | 5 + Shogi.Database/Session/Tables/Session.sql | 3 +- Shogi.Database/Shogi.Database.sqlproj | 10 +- Shogi.Domain/Aggregates/Session.cs | 5 +- Shogi.Domain/BoardState.cs | 2 +- Shogi.UI/Pages/Home/Api/IShogiApi.cs | 2 +- Shogi.UI/Pages/Home/Api/ShogiApi.cs | 5 +- Tests/AcceptanceTests/AcceptanceTests.cs | 77 +- Tests/AcceptanceTests/AcceptanceTests.csproj | 18 +- .../TestSetup/GuestTestFixture.cs | 96 +-- Tests/UnitTests/ShogiBoardStateShould.cs | 340 ++++---- Tests/UnitTests/ShogiShould.cs | 809 +++++++++--------- Tests/UnitTests/UnitTests.csproj | 6 +- 37 files changed, 999 insertions(+), 814 deletions(-) rename Shogi.Api/Controllers/{SessionController.cs => SessionsController.cs} (78%) rename Shogi.Api/Extensions/{Extensions.cs => ClaimsExtensions.cs} (57%) create mode 100644 Shogi.Api/Extensions/ContractsExtensions.cs delete mode 100644 Shogi.Api/Repositories/Dto/BoardStateDto.cs create mode 100644 Shogi.Api/Repositories/Dto/MoveDto.cs create mode 100644 Shogi.Contracts/ShogiApiJsonSerializerSettings.cs create mode 100644 Shogi.Database/Post Deployment/Scripts/EnableSnapshotIsolationLevel.sql create mode 100644 Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql create mode 100644 Shogi.Database/Session/Stored Procedures/DeleteSession.sql rename Shogi.Database/Session/Stored Procedures/{ReadAllSessionsMetadata.sql => ReadSessionPlayerCount.sql} (72%) create mode 100644 Shogi.Database/Session/Tables/Move.sql create mode 100644 Shogi.Database/Session/Tables/Piece.sql diff --git a/Shogi.Api/Controllers/SessionController.cs b/Shogi.Api/Controllers/SessionsController.cs similarity index 78% rename from Shogi.Api/Controllers/SessionController.cs rename to Shogi.Api/Controllers/SessionsController.cs index f16db55..516ec7a 100644 --- a/Shogi.Api/Controllers/SessionController.cs +++ b/Shogi.Api/Controllers/SessionsController.cs @@ -1,32 +1,32 @@ -using Shogi.Api.Managers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shogi.Api.Extensions; +using Shogi.Api.Managers; using Shogi.Api.Repositories; using Shogi.Contracts.Api; using Shogi.Contracts.Socket; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Data.SqlClient; using Shogi.Contracts.Types; -using Shogi.Api.Extensions; +using System.Data.SqlClient; namespace Shogi.Api.Controllers; [ApiController] [Route("[controller]")] [Authorize] -public class SessionController : ControllerBase +public class SessionsController : ControllerBase { private readonly ISocketConnectionManager communicationManager; private readonly IModelMapper mapper; private readonly ISessionRepository sessionRepository; private readonly IQueryRespository queryRespository; - private readonly ILogger logger; + private readonly ILogger logger; - public SessionController( + public SessionsController( ISocketConnectionManager communicationManager, IModelMapper mapper, ISessionRepository sessionRepository, IQueryRespository queryRespository, - ILogger logger) + ILogger logger) { this.communicationManager = communicationManager; this.mapper = mapper; @@ -39,14 +39,10 @@ public class SessionController : ControllerBase public async Task CreateSession([FromBody] CreateSessionCommand request) { var userId = User.GetShogiUserId(); - if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized(); - var session = new Domain.Aggregates.Session( - request.Name, - userId, - new Domain.ValueObjects.ShogiBoard(Domain.BoardState.StandardStarting)); + var session = new Domain.Aggregates.Session(request.Name, userId); try { - await sessionRepository.CreateShogiBoard(board, request.Name, userId); + await sessionRepository.CreateSession(session); } catch (SqlException e) { @@ -58,6 +54,23 @@ public class SessionController : ControllerBase return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null); } + [HttpDelete("{name}")] + public async Task DeleteSession(string name) + { + var userId = User.GetShogiUserId(); + var session = await sessionRepository.ReadSession(name); + + if (session == null) return this.NoContent(); + + if (session.Player1 == userId) + { + await sessionRepository.DeleteSession(name); + return this.NoContent(); + } + + return this.Unauthorized("Cannot delete sessions created by others."); + } + //[HttpPost("{sessionName}/Move")] //public async Task MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request) //{ @@ -156,12 +169,12 @@ public class SessionController : ControllerBase // return Ok(response); //} - [HttpGet] - public async Task> GetSessions() + [HttpGet("PlayerCount")] + public async Task> GetSessionsPlayerCount() { - var sessions = await this.queryRespository.ReadAllSessionsMetadata(); + var sessions = await this.queryRespository.ReadSessionPlayerCount(); - return Ok(new ReadAllSessionsResponse + return Ok(new ReadSessionsPlayerCountResponse { PlayerHasJoinedSessions = Array.Empty(), AllOtherSessions = sessions.ToList() @@ -171,8 +184,26 @@ public class SessionController : ControllerBase [HttpGet("{name}")] public async Task> GetSession(string name) { - await Task.CompletedTask; - return new ReadSessionResponse(); + var session = await sessionRepository.ReadSession(name); + if (session == null) return this.NotFound(); + + return new ReadSessionResponse + { + Session = 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() + }, + Player1 = session.Player1, + Player2 = session.Player2, + SessionName = session.Name + } + }; } //[HttpPut("{sessionName}")] diff --git a/Shogi.Api/Controllers/UserController.cs b/Shogi.Api/Controllers/UserController.cs index 128dba3..310d359 100644 --- a/Shogi.Api/Controllers/UserController.cs +++ b/Shogi.Api/Controllers/UserController.cs @@ -1,14 +1,11 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Shogi.Contracts.Api; using Shogi.Api.Extensions; using Shogi.Api.Managers; -using Shogi.Api.Models; using Shogi.Api.Repositories; -using System.Security.Claims; +using Shogi.Contracts.Api; namespace Shogi.Api.Controllers; diff --git a/Shogi.Api/Extensions/Extensions.cs b/Shogi.Api/Extensions/ClaimsExtensions.cs similarity index 57% rename from Shogi.Api/Extensions/Extensions.cs rename to Shogi.Api/Extensions/ClaimsExtensions.cs index 49f2c51..00b8742 100644 --- a/Shogi.Api/Extensions/Extensions.cs +++ b/Shogi.Api/Extensions/ClaimsExtensions.cs @@ -2,7 +2,7 @@ namespace Shogi.Api.Extensions; -public static class Extensions +public static class ClaimsExtensions { private static readonly string MsalUsernameClaim = "preferred_username"; @@ -26,5 +26,17 @@ public static class Extensions return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value; } - public static string? GetShogiUserId(this ClaimsPrincipal self) => self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId(); -} + /// + /// Reads the userId from claims after claims transformation has occurred. + /// Throws if a shogi userid is not found. + /// + /// + public static string GetShogiUserId(this ClaimsPrincipal self) + { + var id = self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId(); + + if (string.IsNullOrEmpty(id)) throw new InvalidOperationException("Shogi UserId not found in claims."); + + return id; + } +} \ No newline at end of file diff --git a/Shogi.Api/Extensions/ContractsExtensions.cs b/Shogi.Api/Extensions/ContractsExtensions.cs new file mode 100644 index 0000000..0e8945f --- /dev/null +++ b/Shogi.Api/Extensions/ContractsExtensions.cs @@ -0,0 +1,52 @@ +using Shogi.Contracts.Types; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Shogi.Api.Extensions; + +public static class ContractsExtensions +{ + public static WhichPlayer ToContract(this Domain.WhichPlayer player) + { + return player switch + { + Domain.WhichPlayer.Player1 => WhichPlayer.Player1, + Domain.WhichPlayer.Player2 => WhichPlayer.Player2, + _ => throw new NotImplementedException(), + }; + } + + public static WhichPiece ToContract(this Domain.WhichPiece piece) + { + return piece switch + { + Domain.WhichPiece.King => WhichPiece.King, + Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + Domain.WhichPiece.Bishop => WhichPiece.Bishop, + Domain.WhichPiece.Rook => WhichPiece.Rook, + Domain.WhichPiece.Knight => WhichPiece.Knight, + Domain.WhichPiece.Lance => WhichPiece.Lance, + Domain.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()); +} diff --git a/Shogi.Api/Program.cs b/Shogi.Api/Program.cs index fc6584f..3b96cd3 100644 --- a/Shogi.Api/Program.cs +++ b/Shogi.Api/Program.cs @@ -2,12 +2,11 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.HttpLogging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using Shogi.Api.Managers; using Shogi.Api.Repositories; using Shogi.Api.Services; @@ -38,7 +37,7 @@ namespace Shogi.Api }); ConfigureAuthentication(builder); - ConfigureControllersWithNewtonsoft(builder); + ConfigureControllers(builder); ConfigureSwagger(builder); ConfigureDependencyInjection(builder); ConfigureLogging(builder); @@ -165,39 +164,13 @@ namespace Shogi.Api } } - private static void ConfigureControllersWithNewtonsoft(WebApplicationBuilder builder) + private static void ConfigureControllers(WebApplicationBuilder builder) { - builder.Services - .AddControllers() - //.AddJsonOptions(options => - //{ - // options.AllowInputFormatterExceptionMessages = true; - // options.JsonSerializerOptions.WriteIndented = true; - //}); - .AddNewtonsoftJson(options => - { - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = false } - }; - options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); - - JsonConvert.DefaultSettings = () => new JsonSerializerSettings + builder.Services.AddControllers(); + builder.Services.Configure(options => { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - ProcessDictionaryKeys = false - } - }, - Converters = new[] { new StringEnumConverter() }, - NullValueHandling = NullValueHandling.Ignore, - }; + options.SerializerOptions.WriteIndented = true; + }); } private static void ConfigureDependencyInjection(WebApplicationBuilder builder) diff --git a/Shogi.Api/Repositories/Dto/BoardStateDto.cs b/Shogi.Api/Repositories/Dto/BoardStateDto.cs deleted file mode 100644 index 312e791..0000000 --- a/Shogi.Api/Repositories/Dto/BoardStateDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Shogi.Domain; -using Shogi.Domain.ValueObjects; -using System.Collections.ObjectModel; - -namespace Shogi.Api.Repositories.Dto; - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -public class BoardStateDto -{ - public ReadOnlyDictionary State { get; set; } - - public List Player1Hand { get; set; } - - public List Player2Hand { get; set; } - - public Move PreviousMove { get; } - - public WhichPlayer WhoseTurn { get; set; } - public WhichPlayer? InCheck { get; set; } - public bool IsCheckmate { get; set; } -} -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/Shogi.Api/Repositories/Dto/MoveDto.cs b/Shogi.Api/Repositories/Dto/MoveDto.cs new file mode 100644 index 0000000..f6e2bd3 --- /dev/null +++ b/Shogi.Api/Repositories/Dto/MoveDto.cs @@ -0,0 +1,10 @@ +using Shogi.Domain; + +namespace Shogi.Api.Repositories.Dto; + +/// +/// Useful with Dapper to read from database. +/// +public readonly record struct MoveDto(string From, string To, bool IsPromotion, WhichPiece? PieceFromHand) +{ +} diff --git a/Shogi.Api/Repositories/Dto/SessionDto.cs b/Shogi.Api/Repositories/Dto/SessionDto.cs index bdf8e5d..996b504 100644 --- a/Shogi.Api/Repositories/Dto/SessionDto.cs +++ b/Shogi.Api/Repositories/Dto/SessionDto.cs @@ -1,14 +1,5 @@ namespace Shogi.Api.Repositories.Dto; -/// -/// Useful with Dapper to read from database. -/// -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -public class SessionDto +public readonly record struct SessionDto(string Name, string Player1, string Player2) { - public string Name { get; set; } - public string Player1 { get; set; } - public string Player2 { get; set; } - public BoardStateDto BoardState { get; set; } } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. \ No newline at end of file diff --git a/Shogi.Api/Repositories/QueryRepository.cs b/Shogi.Api/Repositories/QueryRepository.cs index ac9bfbe..cf2988b 100644 --- a/Shogi.Api/Repositories/QueryRepository.cs +++ b/Shogi.Api/Repositories/QueryRepository.cs @@ -1,5 +1,4 @@ using Dapper; -using Shogi.Api.Repositories.Dto; using Shogi.Contracts.Types; using System.Data.SqlClient; @@ -14,16 +13,16 @@ public class QueryRepository : IQueryRespository connectionString = configuration.GetConnectionString("ShogiDatabase"); } - public async Task> ReadAllSessionsMetadata() + public async Task> ReadSessionPlayerCount() { using var connection = new SqlConnection(connectionString); return await connection.QueryAsync( - "session.ReadAllSessionsMetadata", + "session.ReadSessionPlayerCount", commandType: System.Data.CommandType.StoredProcedure); } } public interface IQueryRespository { - Task> ReadAllSessionsMetadata(); + Task> ReadSessionPlayerCount(); } \ No newline at end of file diff --git a/Shogi.Api/Repositories/SessionRepository.cs b/Shogi.Api/Repositories/SessionRepository.cs index e78b327..2c9bcd5 100644 --- a/Shogi.Api/Repositories/SessionRepository.cs +++ b/Shogi.Api/Repositories/SessionRepository.cs @@ -1,11 +1,8 @@ using Dapper; using Shogi.Api.Repositories.Dto; -using Shogi.Domain; using Shogi.Domain.Aggregates; -using Shogi.Domain.ValueObjects; using System.Data; using System.Data.SqlClient; -using System.Text.Json; namespace Shogi.Api.Repositories; @@ -20,45 +17,52 @@ public class SessionRepository : ISessionRepository public async Task CreateSession(Session session) { - var boardStateDto = new BoardStateDto - { - InCheck = session.BoardState.InCheck, - IsCheckmate = session.BoardState.IsCheckmate, - Player1Hand = session.BoardState.Player1Hand, - Player2Hand = session.BoardState.Player2Hand, - State = session.BoardState.State, - WhoseTurn = session.BoardState.WhoseTurn, - }; - using var connection = new SqlConnection(connectionString); await connection.ExecuteAsync( "session.CreateSession", new { - Name = sessionName, - InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto), - Player1Name = player1, + session.Name, + Player1Name = session.Player1, }, commandType: CommandType.StoredProcedure); } - public async Task ReadShogiBoard(string name) + public async Task DeleteSession(string name) { using var connection = new SqlConnection(connectionString); - var results = await connection.QueryAsync( - "session.ReadSession", + await connection.ExecuteAsync( + "session.DeleteSession", + new { Name = name }, commandType: CommandType.StoredProcedure); - var dto = results.SingleOrDefault(); - if (dto == null) return null; + } - var boardState = new BoardState( - state: new(dto.BoardState.State), - player1Hand: dto.BoardState.Player1Hand, - player2Hand: dto.BoardState.Player2Hand, - whoseTurn: dto.BoardState.WhoseTurn, - playerInCheck: dto.BoardState.InCheck, - previousMove: dto.BoardState.PreviousMove); - var session = new ShogiBoard(boardState); + public async Task ReadSession(string name) + { + using var connection = new SqlConnection(connectionString); + var results = await connection.QueryMultipleAsync( + "session.ReadSession", + new { Name = name }, + commandType: CommandType.StoredProcedure); + + var sessionDtos = await results.ReadAsync(); + if (!sessionDtos.Any()) return null; + var dto = sessionDtos.First(); + var session = new Session(dto.Name, dto.Player1); + if (!string.IsNullOrWhiteSpace(dto.Player2)) session.AddPlayer2(dto.Player2); + + var moveDtos = await results.ReadAsync(); + foreach (var move in moveDtos) + { + if (move.PieceFromHand.HasValue) + { + session.Board.Move(move.PieceFromHand.Value, move.To); + } + else + { + session.Board.Move(move.From, move.To, false); + } + } return session; } } @@ -66,5 +70,6 @@ public class SessionRepository : ISessionRepository public interface ISessionRepository { Task CreateSession(Session session); - Task ReadShogiBoard(string name); + Task DeleteSession(string name); + Task ReadSession(string name); } \ No newline at end of file diff --git a/Shogi.Api/Services/SocketService.cs b/Shogi.Api/Services/SocketService.cs index a32dedc..8c7666a 100644 --- a/Shogi.Api/Services/SocketService.cs +++ b/Shogi.Api/Services/SocketService.cs @@ -1,12 +1,11 @@ using FluentValidation; -using Newtonsoft.Json; using Shogi.Contracts.Socket; using Shogi.Contracts.Types; using Shogi.Api.Extensions; using Shogi.Api.Managers; -using Shogi.Api.Repositories; using System.Net; using System.Net.WebSockets; +using System.Text.Json; namespace Shogi.Api.Services { @@ -59,7 +58,7 @@ namespace Shogi.Api.Services var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); - var request = JsonConvert.DeserializeObject(message); + var request = JsonSerializer.Deserialize(message); if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); diff --git a/Shogi.Api/Shogi.Api.csproj b/Shogi.Api/Shogi.Api.csproj index bf92b1c..a213d13 100644 --- a/Shogi.Api/Shogi.Api.csproj +++ b/Shogi.Api/Shogi.Api.csproj @@ -29,12 +29,10 @@ - - - + diff --git a/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs b/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs index 3bbbde1..98a33f6 100644 --- a/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs +++ b/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs @@ -5,6 +5,5 @@ namespace Shogi.Contracts.Api; public class CreateSessionCommand { [Required] - public string Name { get; set; } = string.Empty; - public bool IsPrivate { get; set; } + public string Name { get; set; } } diff --git a/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs b/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs index 4956250..1e98968 100644 --- a/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs +++ b/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Shogi.Contracts.Api; -public class ReadAllSessionsResponse +public class ReadSessionsPlayerCountResponse { public IList PlayerHasJoinedSessions { get; set; } public IList AllOtherSessions { get; set; } diff --git a/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs b/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs new file mode 100644 index 0000000..89277f8 --- /dev/null +++ b/Shogi.Contracts/ShogiApiJsonSerializerSettings.cs @@ -0,0 +1,11 @@ +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/Session.cs b/Shogi.Contracts/Types/Session.cs index 0a67ed1..ecd4f16 100644 --- a/Shogi.Contracts/Types/Session.cs +++ b/Shogi.Contracts/Types/Session.cs @@ -2,9 +2,8 @@ public class Session { - public string Player1 { get; set; } = string.Empty; + public string Player1 { get; set; } public string? Player2 { get; set; } - public string SessionName { get; set; } = string.Empty; - public bool GameOver { get; set; } + public string SessionName { get; set; } public BoardState BoardState { get; set; } } diff --git a/Shogi.Database/Post Deployment/Script.PostDeployment.sql b/Shogi.Database/Post Deployment/Script.PostDeployment.sql index 17665e5..d14db3c 100644 --- a/Shogi.Database/Post Deployment/Script.PostDeployment.sql +++ b/Shogi.Database/Post Deployment/Script.PostDeployment.sql @@ -10,4 +10,5 @@ Post-Deployment Script Template -------------------------------------------------------------------------------------- */ -:r .\Scripts\PopulateLoginPlatforms.sql \ No newline at end of file +:r .\Scripts\PopulateLoginPlatforms.sql +:r .\Scripts\PopulatePieces.sql \ No newline at end of file diff --git a/Shogi.Database/Post Deployment/Scripts/EnableSnapshotIsolationLevel.sql b/Shogi.Database/Post Deployment/Scripts/EnableSnapshotIsolationLevel.sql new file mode 100644 index 0000000..8c7dfac --- /dev/null +++ b/Shogi.Database/Post Deployment/Scripts/EnableSnapshotIsolationLevel.sql @@ -0,0 +1,2 @@ +ALTER DATABASE Shogi +SET ALLOW_SNAPSHOT_ISOLATION ON \ No newline at end of file diff --git a/Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql b/Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql new file mode 100644 index 0000000..5abcfce --- /dev/null +++ b/Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql @@ -0,0 +1,21 @@ +DECLARE @Pieces TABLE( + [Name] NVARCHAR(13) +) + +INSERT INTO @Pieces ([Name]) +VALUES + ('King'), + ('GoldGeneral'), + ('SilverGeneral'), + ('Bishop'), + ('Rook'), + ('Knight'), + ('Lance'), + ('Pawn'); + +MERGE [session].[Piece] as t +USING @Pieces as s +ON t.[Name] = s.[Name] +WHEN NOT MATCHED THEN + INSERT ([Name]) + VALUES (s.[Name]); \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/CreateSession.sql b/Shogi.Database/Session/Stored Procedures/CreateSession.sql index d349e4f..879f2ab 100644 --- a/Shogi.Database/Session/Stored Procedures/CreateSession.sql +++ b/Shogi.Database/Session/Stored Procedures/CreateSession.sql @@ -1,15 +1,12 @@ CREATE PROCEDURE [session].[CreateSession] @Name [session].[SessionName], - @Player1Name [user].[UserName], - @InitialBoardStateDocument [session].[JsonDocument] + @Player1Name [user].[UserName] AS BEGIN SET NOCOUNT ON - INSERT INTO [session].[Session] (BoardState, Player1Id) - SELECT - @InitialBoardStateDocument, - Id + INSERT INTO [session].[Session] ([Name], Player1Id) + SELECT @Name, Id FROM [user].[User] WHERE [Name] = @Player1Name END \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/DeleteSession.sql b/Shogi.Database/Session/Stored Procedures/DeleteSession.sql new file mode 100644 index 0000000..38cace2 --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/DeleteSession.sql @@ -0,0 +1,5 @@ +CREATE PROCEDURE [session].[DeleteSession] + @Name [session].[SessionName] +AS + +DELETE FROM [session].[Session] WHERE [Name] = @Name; \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/ReadSession.sql b/Shogi.Database/Session/Stored Procedures/ReadSession.sql index 5cece40..7acdb93 100644 --- a/Shogi.Database/Session/Stored Procedures/ReadSession.sql +++ b/Shogi.Database/Session/Stored Procedures/ReadSession.sql @@ -2,15 +2,32 @@ @Name [session].[SessionName] AS BEGIN - SET NOCOUNT ON + SET NOCOUNT ON -- Performance boost + SET XACT_ABORT ON -- Rollback transaction on error + SET TRANSACTION ISOLATION LEVEL SNAPSHOT -- Ignores data changes that happen after the transaction begins. - SELECT - sess.[Name], - BoardState, - p1.[Name] as Player1, - p2.[Name] as Player2 - FROM [session].[Session] sess - INNER JOIN [user].[User] p1 on sess.Player1Id = p1.Id - LEFT JOIN [user].[User] p2 on sess.Player2Id = p2.Id - WHERE sess.[Name] = @Name; + BEGIN TRANSACTION + + -- Session + SELECT + sess.[Name], + p1.[Name] as Player1, + p2.[Name] as Player2 + FROM [session].[Session] sess + INNER JOIN [user].[User] p1 on sess.Player1Id = p1.Id + LEFT JOIN [user].[User] p2 on sess.Player2Id = p2.Id + WHERE sess.[Name] = @Name; + + -- Player moves + SELECT + mv.[From], + mv.[To], + mv.IsPromotion, + piece.[Name] as PieceFromHand + FROM [session].[Move] mv + INNER JOIN [session].[Session] sess ON sess.Id = mv.SessionId + RIGHT JOIN [session].Piece piece on piece.Id = mv.PieceIdFromHand + WHERE sess.[Name] = @Name; + + COMMIT END diff --git a/Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql b/Shogi.Database/Session/Stored Procedures/ReadSessionPlayerCount.sql similarity index 72% rename from Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql rename to Shogi.Database/Session/Stored Procedures/ReadSessionPlayerCount.sql index 88252d5..212c863 100644 --- a/Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql +++ b/Shogi.Database/Session/Stored Procedures/ReadSessionPlayerCount.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [session].[ReadAllSessionsMetadata] +CREATE PROCEDURE [session].[ReadSessionPlayerCount] AS BEGIN SET NOCOUNT ON; diff --git a/Shogi.Database/Session/Tables/Move.sql b/Shogi.Database/Session/Tables/Move.sql new file mode 100644 index 0000000..4339650 --- /dev/null +++ b/Shogi.Database/Session/Tables/Move.sql @@ -0,0 +1,20 @@ +CREATE TABLE [session].[Move] +( + [Id] INT NOT NULL PRIMARY KEY IDENTITY, + [SessionId] BIGINT NOT NULL, + [From] VARCHAR(2) NOT NULL, + [To] VARCHAR(2) NOT NULL, + [IsPromotion] BIT NOT NULL, + [PieceIdFromHand] INT NULL + + CONSTRAINT [Cannot end where you start] + CHECK ([From] <> [To]), + + CONSTRAINT FK_Move_Session FOREIGN KEY (SessionId) REFERENCES [session].[Session] (Id) + ON DELETE CASCADE + ON UPDATE CASCADE, + + CONSTRAINT FK_Move_Piece FOREIGN KEY (PieceIdFromHand) REFERENCES [session].[Piece] (Id) + ON DELETE NO ACTION + ON UPDATE NO ACTION +) diff --git a/Shogi.Database/Session/Tables/Piece.sql b/Shogi.Database/Session/Tables/Piece.sql new file mode 100644 index 0000000..ba19425 --- /dev/null +++ b/Shogi.Database/Session/Tables/Piece.sql @@ -0,0 +1,5 @@ +CREATE TABLE [session].[Piece] +( + [Id] INT NOT NULL PRIMARY KEY IDENTITY, + [Name] NVARCHAR(13) NOT NULL UNIQUE +) diff --git a/Shogi.Database/Session/Tables/Session.sql b/Shogi.Database/Session/Tables/Session.sql index 4b1912f..889e61e 100644 --- a/Shogi.Database/Session/Tables/Session.sql +++ b/Shogi.Database/Session/Tables/Session.sql @@ -4,13 +4,12 @@ [Name] [session].[SessionName] UNIQUE, Player1Id BIGINT NOT NULL, Player2Id BIGINT NULL, - BoardState [session].[JsonDocument] NOT NULL, Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(), - CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1), CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id) ON DELETE NO ACTION ON UPDATE NO ACTION diff --git a/Shogi.Database/Shogi.Database.sqlproj b/Shogi.Database/Shogi.Database.sqlproj index 01ae76c..a9e9bed 100644 --- a/Shogi.Database/Shogi.Database.sqlproj +++ b/Shogi.Database/Shogi.Database.sqlproj @@ -22,6 +22,7 @@ False True True + session bin\Release\ @@ -77,14 +78,21 @@ - + + + + + + + + \ No newline at end of file diff --git a/Shogi.Domain/Aggregates/Session.cs b/Shogi.Domain/Aggregates/Session.cs index 1c6d529..2a817cf 100644 --- a/Shogi.Domain/Aggregates/Session.cs +++ b/Shogi.Domain/Aggregates/Session.cs @@ -6,12 +6,11 @@ public class Session { public Session( string name, - string player1Name, - ShogiBoard board) + string player1Name) { Name = name; Player1 = player1Name; - Board = board; + Board = new(BoardState.StandardStarting); } public string Name { get; } diff --git a/Shogi.Domain/BoardState.cs b/Shogi.Domain/BoardState.cs index 3850bec..c2b8ca4 100644 --- a/Shogi.Domain/BoardState.cs +++ b/Shogi.Domain/BoardState.cs @@ -9,7 +9,7 @@ public class BoardState /// /// Board state before any moves have been made, using standard setup and rules. /// - public static readonly BoardState StandardStarting = new( + public static BoardState StandardStarting => new( state: BuildStandardStartingBoardState(), player1Hand: new(), player2Hand: new(), diff --git a/Shogi.UI/Pages/Home/Api/IShogiApi.cs b/Shogi.UI/Pages/Home/Api/IShogiApi.cs index e7bb155..5773fd4 100644 --- a/Shogi.UI/Pages/Home/Api/IShogiApi.cs +++ b/Shogi.UI/Pages/Home/Api/IShogiApi.cs @@ -8,7 +8,7 @@ namespace Shogi.UI.Pages.Home.Api { Task GetGuestToken(); Task GetSession(string name); - Task GetSessions(); + Task GetSessions(); Task GetToken(); Task GuestLogout(); Task PostMove(string sessionName, Move move); diff --git a/Shogi.UI/Pages/Home/Api/ShogiApi.cs b/Shogi.UI/Pages/Home/Api/ShogiApi.cs index 92abe7e..ef2f73f 100644 --- a/Shogi.UI/Pages/Home/Api/ShogiApi.cs +++ b/Shogi.UI/Pages/Home/Api/ShogiApi.cs @@ -63,12 +63,12 @@ namespace Shogi.UI.Pages.Home.Api return null; } - public async Task GetSessions() + public async Task GetSessions() { var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative)); if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync(serializerOptions); + return await response.Content.ReadFromJsonAsync(serializerOptions); } return null; } @@ -90,7 +90,6 @@ namespace Shogi.UI.Pages.Home.Api var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand { Name = name, - IsPrivate = isPrivate }); return response.StatusCode; } diff --git a/Tests/AcceptanceTests/AcceptanceTests.cs b/Tests/AcceptanceTests/AcceptanceTests.cs index da27da7..d5f9b21 100644 --- a/Tests/AcceptanceTests/AcceptanceTests.cs +++ b/Tests/AcceptanceTests/AcceptanceTests.cs @@ -1,5 +1,6 @@ using Shogi.AcceptanceTests.TestSetup; using Shogi.Contracts.Api; +using Shogi.Contracts.Types; using System.Net; using System.Net.Http.Json; using Xunit.Abstractions; @@ -21,16 +22,78 @@ public class AcceptanceTests : IClassFixture private HttpClient Service => fixture.Service; + [Fact] + public async Task ReadSessionsPlayerCount() + { + try + { + // Arrange + await CreateSession(); + + // Act + var readAllResponse = await Service + .GetFromJsonAsync(new Uri("Sessions/PlayerCount", UriKind.Relative), + Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions); + + // Assert + readAllResponse.Should().NotBeNull(); + readAllResponse! + .AllOtherSessions + .Should() + .ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1); + } + finally + { + // Annul + await DeleteSession(); + } + } + [Fact] public async Task CreateAndReadSession() { - var createResponse = await Service.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand { Name = "Acceptance Tests" }); - createResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var yep = await createResponse.Content.ReadAsStringAsync(); - console.WriteLine(yep); + try + { + // Arrange + await CreateSession(); + + // Act + var response = await Service.GetFromJsonAsync( + new Uri("Sessions/Acceptance Tests", UriKind.Relative), + Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions); + + // Assert + response.Should().NotBeNull(); + response!.Session.Should().NotBeNull(); + response.Session.BoardState.Board.Should().NotBeEmpty(); + response.Session.BoardState.Player1Hand.Should().BeEmpty(); + response.Session.BoardState.Player2Hand.Should().BeEmpty(); + response.Session.BoardState.PlayerInCheck.Should().BeNull(); + response.Session.BoardState.WhoseTurn.Should().Be(WhichPlayer.Player1); + response.Session.Player1.Should().NotBeNullOrEmpty(); + response.Session.Player2.Should().BeNull(); + response.Session.SessionName.Should().Be("Acceptance Tests"); + } + finally + { + // Annul + await DeleteSession(); + } + } + + private async Task CreateSession() + { + var createResponse = await Service.PostAsJsonAsync( + new Uri("Sessions", UriKind.Relative), + new CreateSessionCommand { Name = "Acceptance Tests" }, + Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + } + + private async Task DeleteSession() + { + var response = await Service.DeleteAsync(new Uri("Sessions/Acceptance Tests", UriKind.Relative)); + response.StatusCode.Should().Be(HttpStatusCode.NoContent, because: "Test cleanup should succeed"); - var readAllResponse = await Service.GetFromJsonAsync(new Uri("Session", UriKind.Relative)); - readAllResponse.Should().NotBeNull(); - readAllResponse!.AllOtherSessions.Should().ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1); } } \ No newline at end of file diff --git a/Tests/AcceptanceTests/AcceptanceTests.csproj b/Tests/AcceptanceTests/AcceptanceTests.csproj index caac119..e0ed7c1 100644 --- a/Tests/AcceptanceTests/AcceptanceTests.csproj +++ b/Tests/AcceptanceTests/AcceptanceTests.csproj @@ -21,21 +21,21 @@ - - - - - - - + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs b/Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs index dd3fd0f..423326d 100644 --- a/Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs +++ b/Tests/AcceptanceTests/TestSetup/GuestTestFixture.cs @@ -1,62 +1,60 @@ using Microsoft.Extensions.Configuration; -namespace Shogi.AcceptanceTests.TestSetup +namespace Shogi.AcceptanceTests.TestSetup; + +/// +/// Acceptance Test fixture for tests which assert features for Microsoft accounts. +/// +public class GuestTestFixture : IAsyncLifetime, IDisposable { - /// - /// Acceptance Test fixture for tests which assert features for Microsoft accounts. - /// - public class GuestTestFixture : IAsyncLifetime, IDisposable + private bool disposedValue; + + public GuestTestFixture() { - private bool disposedValue; + Configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); - public GuestTestFixture() + Service = new HttpClient { - Configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - //.AddEnvironmentVariables() - .Build(); + BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute) + }; + } - Service = new HttpClient + public IConfiguration Configuration { get; private set; } + public HttpClient Service { get; } + + public async Task InitializeAsync() + { + // Log in as a guest account and retain the session cookie for future requests. + var loginResponse = await Service.GetAsync(new Uri("User/LoginAsGuest", UriKind.Relative)); + loginResponse.IsSuccessStatusCode.Should().BeTrue(because: "Guest accounts should work"); + var guestSessionCookie = loginResponse.Headers.GetValues("Set-Cookie").SingleOrDefault(); + Service.DefaultRequestHeaders.Add("Set-Cookie", guestSessionCookie); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) { - BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute) - }; - - } - - public IConfiguration Configuration { get; private set; } - - public HttpClient Service { get; } - - public async Task InitializeAsync() - { - // Log in as a guest account. - var loginResponse = await Service.GetAsync(new Uri("User/LoginAsGuest", UriKind.Relative)); - loginResponse.EnsureSuccessStatusCode(); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - Service.Dispose(); - } - - disposedValue = true; + Service.Dispose(); } - } - public Task DisposeAsync() - { - Dispose(true); - return Task.CompletedTask; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + disposedValue = true; } } + + public Task DisposeAsync() + { + Dispose(true); + return Task.CompletedTask; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/Tests/UnitTests/ShogiBoardStateShould.cs b/Tests/UnitTests/ShogiBoardStateShould.cs index a2592f7..26588cd 100644 --- a/Tests/UnitTests/ShogiBoardStateShould.cs +++ b/Tests/UnitTests/ShogiBoardStateShould.cs @@ -1,186 +1,182 @@ -using FluentAssertions; -using Xunit; +namespace Shogi.Domain.UnitTests; -namespace Shogi.Domain.UnitTests +public class ShogiBoardStateShould { - public class ShogiBoardStateShould + [Fact] + public void InitializeBoardState() { - [Fact] - public void InitializeBoardState() - { - // Act - var board = new BoardState(); + // Act + 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 + 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); - 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(); + 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(); - 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); + 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); - 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(); + 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(); - 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(); + 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(); - 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(); + 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(); - 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); + 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); - 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(); + 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(); - 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); - } + 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); } } diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index cc6ea13..0d2b80d 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -1,462 +1,463 @@ -using System; +using Shogi.Domain.ValueObjects; +using System; namespace Shogi.Domain.UnitTests { - public class ShogiShould - { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) - { - this.console = console; - } + public class ShogiShould + { + private readonly ITestOutputHelper console; + public ShogiShould(ITestOutputHelper console) + { + this.console = console; + } - [Fact] - public void MoveAPieceToAnEmptyPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; + [Fact] + public void MoveAPieceToAnEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; - board["A4"].Should().BeNull(); - var expectedPiece = board["A3"]; - expectedPiece.Should().NotBeNull(); + board["A4"].Should().BeNull(); + var expectedPiece = board["A3"]; + expectedPiece.Should().NotBeNull(); - // Act - shogi.Move("A3", "A4", false); + // Act + shogi.Move("A3", "A4", false); - // Assert - board["A3"].Should().BeNull(); - board["A4"].Should().Be(expectedPiece); - } + // 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); + [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); + // 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 + using (new AssertionScope()) + { + board.InCheck.Should().BeNull(); + } + } - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var shogi = MockShogiBoard(); - var board = shogi.BoardState; - board["D5"].Should().BeNull(); + [Fact] + public void PreventInvalidMoves_MoveFromEmptyPosition() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + board["D5"].Should().BeNull(); - // Act - var act = () => shogi.Move("D5", "D6", false); + // Act + var act = () => shogi.Move("D5", "D6", false); - // Assert - act.Should().Throw(); - board["D5"].Should().BeNull(); - board["D6"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } + // Assert + act.Should().Throw(); + 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"]; + [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 act = () => shogi.Move("A3", "A3", false); + // Act - P1 "moves" pawn to the position it already exists at. + var act = () => shogi.Move("A3", "A3", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A3"].Should().Be(expectedPiece); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + 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["A1"]; - expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); + [Fact] + public void PreventInvalidMoves_MoveSet() + { + // Arrange + var shogi = MockShogiBoard(); + var board = shogi.BoardState; + var expectedPiece = board["A1"]; + expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); - // Act - Move Lance illegally - var act = () => shogi.Move("A1", "D5", false); + // Act - Move Lance illegally + var act = () => shogi.Move("A1", "D5", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(expectedPiece); - board["A5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board["A1"].Should().Be(expectedPiece); + board["A5"].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); + [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 act = () => shogi.Move("A7", "A6", false); + // Act - Move Player2 Pawn when it is Player1 turn. + var act = () => shogi.Move("A7", "A6", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A7"].Should().Be(expectedPiece); - board["A6"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + 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); + [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 act = () => shogi.Move("A1", "A5", false); + // Act - Move P1 Lance through P1 Pawn. + var act = () => shogi.Move("A1", "A5", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(lance); - board["A3"].Should().Be(pawn); - board["A5"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + 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); + [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 act = () => shogi.Move("B1", "C3", false); + // Act - P1 Knight tries to capture P1 Pawn. + var act = () => shogi.Move("B1", "C3", false); - // Arrange - using (new AssertionScope()) - { - act.Should().Throw(); - board["B1"].Should().Be(knight); - board["C3"].Should().Be(pawn); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } + // Arrange + using (new AssertionScope()) + { + act.Should().Throw(); + 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"]; + [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 act = () => shogi.Move("I9", "I8", false); + // Act - P2 moves Lance while in check. + var act = () => shogi.Move("I9", "I8", false); - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().Be(lance); - board["I8"].Should().BeNull(); - } - } + // Assert + using (new AssertionScope()) + { + act.Should().Throw(); + board.InCheck.Should().Be(WhichPlayer.Player2); + board["I9"].Should().Be(lance); + board["I8"].Should().BeNull(); + } + } - [Fact] - // TODO: Consider nesting classes to share this setup in a constructor but have act and assert as separate facts. - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var 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); + [Fact] + // TODO: Consider nesting classes to share this setup in a constructor but have act and assert as separate facts. + public void PreventInvalidDrops_MoveSet() + { + // Arrange + var 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 act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in farthest rank. + board["H9"].Should().BeNull(); + var act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally placing Knight from the hand in second farthest row. - board["H8"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H8"); - act.Should().Throw(); - board["H8"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); + // Act | Assert - Illegally placing Knight from the hand in second farthest row. + board["H8"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H8"); + act.Should().Throw(); + board["H8"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - // Act | Assert - Illegally place Lance from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); + // Act | Assert - Illegally place Lance from the hand. + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Knight, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - // Act | Assert - Illegally place Pawn from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Pawn, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); + // Act | Assert - Illegally place Pawn from the hand. + board["H9"].Should().BeNull(); + act = () => shogi.Move(WhichPiece.Pawn, "H9"); + act.Should().Throw(); + board["H9"].Should().BeNull(); + board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // // TODO - } + // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. + // // TODO + } - //[Fact] - //public void PreventInvalidDrop_Check() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Pawn, arbitrary move. - // new Move("A3", "A4"), - // // P2 Bishop takes P1 Bishop - // new Move("H8", "B2"), - // // P1 Silver takes P2 Bishop - // new Move("C1", "B2"), - // // P2 Pawn, arbtrary move - // new Move("A7", "A6"), - // // P1 drop Bishop, place P2 in check - // new Move(WhichPiece.Bishop, "G7") - // }; - // var shogi = new Shogi(moves); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // board["E5"].Should().BeNull(); + //[Fact] + //public void PreventInvalidDrop_Check() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Pawn, arbitrary move. + // new Move("A3", "A4"), + // // P2 Bishop takes P1 Bishop + // new Move("H8", "B2"), + // // P1 Silver takes P2 Bishop + // new Move("C1", "B2"), + // // P2 Pawn, arbtrary move + // new Move("A7", "A6"), + // // P1 drop Bishop, place P2 in check + // new Move(WhichPiece.Bishop, "G7") + // }; + // var shogi = new Shogi(moves); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // board["E5"].Should().BeNull(); - // // Act - P2 places a Bishop while in check. - // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); + // // Act - P2 places a Bishop while in check. + // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); - // // Assert - // dropSuccess.Should().BeFalse(); - // board["E5"].Should().BeNull(); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - //} + // // Assert + // dropSuccess.Should().BeFalse(); + // board["E5"].Should().BeNull(); + // shogi.InCheck.Should().Be(WhichPlayer.Player2); + // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + //} - //[Fact] - //public void PreventInvalidDrop_Capture() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop capture P2 Bishop - // new Move("B2", "H8"), - // // P2 Pawn - // new Move("G6", "G5") - // }; - // var shogi = new Shogi(moves); - // using (new AssertionScope()) - // { - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // board["I9"].Should().NotBeNull(); - // board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // board["I9"].Owner.Should().Be(WhichPlayer.Player2); - // } + //[Fact] + //public void PreventInvalidDrop_Capture() + //{ + // // Arrange + // var moves = new[] + // { + // // P1 Pawn + // new Move("C3", "C4"), + // // P2 Pawn + // new Move("G7", "G6"), + // // P1 Bishop capture P2 Bishop + // new Move("B2", "H8"), + // // P2 Pawn + // new Move("G6", "G5") + // }; + // var shogi = new Shogi(moves); + // using (new AssertionScope()) + // { + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // board["I9"].Should().NotBeNull(); + // board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // board["I9"].Owner.Should().Be(WhichPlayer.Player2); + // } - // // Act - P1 tries to place a piece where an opponent's piece resides. - // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); + // // Act - P1 tries to place a piece where an opponent's piece resides. + // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); - // // Assert - // using (new AssertionScope()) - // { - // dropSuccess.Should().BeFalse(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // board["I9"].Should().NotBeNull(); - // board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // board["I9"].Owner.Should().Be(WhichPlayer.Player2); - // } - //} + // // Assert + // using (new AssertionScope()) + // { + // dropSuccess.Should().BeFalse(); + // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); + // board["I9"].Should().NotBeNull(); + // board["I9"].WhichPiece.Should().Be(WhichPiece.Lance); + // board["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); + [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); + // Act - P1 Bishop, check + shogi.Move("B2", "G7", false); - // Assert - board.InCheck.Should().Be(WhichPlayer.Player2); - } + // 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); + [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); + // 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(); - } - } + // 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); + [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); + // Act - P1 Bishop captures P2 Bishop + shogi.Move("B2", "H8", false); - // Assert - board["B2"].Should().BeNull(); - board["H8"].Should().Be(p1Bishop); + // Assert + board["B2"].Should().BeNull(); + board["H8"].Should().Be(p1Bishop); - board - .Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - } + 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); + [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); + // Act - P1 Pawn wins by checkmate. + shogi.Move("E7", "E8", false); - // Assert - checkmate - console.WriteLine(shogi.ToStringStateAsAscii()); - board.IsCheckmate.Should().BeTrue(); - board.InCheck.Should().Be(WhichPlayer.Player2); - } + // Assert - checkmate + console.WriteLine(shogi.ToStringStateAsAscii()); + board.IsCheckmate.Should().BeTrue(); + board.InCheck.Should().Be(WhichPlayer.Player2); + } - private static ShogiBoard MockShogiBoard() => new ShogiBoard("Test Session", BoardState.StandardStarting); - } + private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting); + } } diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 2697c25..a15325a 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -8,14 +8,14 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all