diff --git a/Shogi.Api/Application/ShogiApplication.cs b/Shogi.Api/Application/ShogiApplication.cs index 792e18a..61ae00a 100644 --- a/Shogi.Api/Application/ShogiApplication.cs +++ b/Shogi.Api/Application/ShogiApplication.cs @@ -92,6 +92,7 @@ public class ShogiApplication( if (moveResult.IsSuccess) { await sessionRepository.CreateMove(sessionId, command); + await sessionRepository.CreateState(session); await gameHubContext.Emit_PieceMoved(sessionId); return new NoContentResult(); } diff --git a/Shogi.Api/Repositories/Dto/SessionState/Piece.cs b/Shogi.Api/Repositories/Dto/SessionState/Piece.cs new file mode 100644 index 0000000..9a0b6ae --- /dev/null +++ b/Shogi.Api/Repositories/Dto/SessionState/Piece.cs @@ -0,0 +1,34 @@ +namespace Shogi.Api.Repositories.Dto.SessionState; + +public class Piece +{ + public bool IsPromoted { get; set; } + public WhichPiece WhichPiece { get; set; } + public WhichPlayer Owner { get; set; } + + public Piece() { } + + public Piece(Domain.ValueObjects.Piece piece) + { + IsPromoted = piece.IsPromoted; + WhichPiece = piece.WhichPiece switch + { + Domain.ValueObjects.WhichPiece.Bishop => WhichPiece.Bishop, + Domain.ValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + Domain.ValueObjects.WhichPiece.King => WhichPiece.King, + Domain.ValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + 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() + }; + + Owner = piece.Owner switch + { + Domain.ValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1, + Domain.ValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2, + _ => throw new NotImplementedException() + }; + } +} diff --git a/Shogi.Api/Repositories/Dto/SessionState/SessionStateDocument.cs b/Shogi.Api/Repositories/Dto/SessionState/SessionStateDocument.cs new file mode 100644 index 0000000..3c94ba2 --- /dev/null +++ b/Shogi.Api/Repositories/Dto/SessionState/SessionStateDocument.cs @@ -0,0 +1,69 @@ +namespace Shogi.Api.Repositories.Dto.SessionState; + +public class SessionStateDocument +{ + public long Id { get; set; } + + public Dictionary Board { get; set; } + + public WhichPiece[] Player1Hand { get; set; } + + public WhichPiece[] Player2Hand { get; set; } + + public WhichPlayer? PlayerInCheck { get; set; } + + public WhichPlayer WhoseTurn { get; set; } + + public bool IsGameOver { get; set; } + + public string DocumentVersion { get; set; } = "1"; + + public SessionStateDocument() { } + public SessionStateDocument(Domain.ValueObjects.BoardState boardState) + { + this.Board = boardState.State.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value == null ? null : new Piece(kvp.Value)); + + this.Player1Hand = boardState.Player1Hand + .Select(piece => Map(piece.WhichPiece)) + .ToArray(); + + this.Player2Hand = boardState.Player2Hand + .Select(piece => Map(piece.WhichPiece)) + .ToArray(); + + this.PlayerInCheck = boardState.InCheck.HasValue + ? Map(boardState.InCheck.Value) + : null; + + this.IsGameOver = boardState.IsCheckmate; + } + + + static WhichPiece Map(Domain.ValueObjects.WhichPiece whichPiece) + { + return whichPiece switch + { + Domain.ValueObjects.WhichPiece.Bishop => WhichPiece.Bishop, + Domain.ValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral, + Domain.ValueObjects.WhichPiece.King => WhichPiece.King, + Domain.ValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral, + 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() + }; + } + + static WhichPlayer Map(Domain.ValueObjects.WhichPlayer whichPlayer) + { + return whichPlayer switch + { + Domain.ValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1, + Domain.ValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2, + _ => throw new NotImplementedException() + }; + } +} diff --git a/Shogi.Api/Repositories/Dto/SessionState/WhichPiece.cs b/Shogi.Api/Repositories/Dto/SessionState/WhichPiece.cs new file mode 100644 index 0000000..53f0de0 --- /dev/null +++ b/Shogi.Api/Repositories/Dto/SessionState/WhichPiece.cs @@ -0,0 +1,13 @@ +namespace Shogi.Api.Repositories.Dto.SessionState; + +public enum WhichPiece +{ + King, + GoldGeneral, + SilverGeneral, + Bishop, + Rook, + Knight, + Lance, + Pawn +} diff --git a/Shogi.Api/Repositories/Dto/SessionState/WhichPlayer.cs b/Shogi.Api/Repositories/Dto/SessionState/WhichPlayer.cs new file mode 100644 index 0000000..1a81aad --- /dev/null +++ b/Shogi.Api/Repositories/Dto/SessionState/WhichPlayer.cs @@ -0,0 +1,7 @@ +namespace Shogi.Api.Repositories.Dto.SessionState; + +public enum WhichPlayer +{ + Player1, + Player2 +} diff --git a/Shogi.Api/Repositories/SessionRepository.cs b/Shogi.Api/Repositories/SessionRepository.cs index 7ccc6e5..d22b341 100644 --- a/Shogi.Api/Repositories/SessionRepository.cs +++ b/Shogi.Api/Repositories/SessionRepository.cs @@ -1,9 +1,11 @@ using Dapper; using Shogi.Api.Repositories.Dto; +using Shogi.Api.Repositories.Dto.SessionState; using Shogi.Contracts.Api.Commands; using Shogi.Domain.Aggregates; using System.Data; using System.Data.SqlClient; +using System.Text.Json; namespace Shogi.Api.Repositories; @@ -81,4 +83,19 @@ public class SessionRepository(IConfiguration configuration) }, commandType: CommandType.StoredProcedure); } + + public async Task CreateState(Session session) + { + var document = new SessionStateDocument(session.Board.BoardState); + + using var connection = new SqlConnection(this.connectionString); + await connection.ExecuteAsync( + "session.CreateState", + new + { + SessionId = session.Id.ToString(), + Document = JsonSerializer.Serialize(document) + }, + commandType: CommandType.StoredProcedure); + } } \ No newline at end of file diff --git a/Shogi.Contracts/Types/Piece.cs b/Shogi.Contracts/Types/Piece.cs index 3c7b335..f628bf7 100644 --- a/Shogi.Contracts/Types/Piece.cs +++ b/Shogi.Contracts/Types/Piece.cs @@ -1,9 +1,8 @@ -namespace Shogi.Contracts.Types -{ +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/Session/Stored Procedures/CreateState.sql b/Shogi.Database/Session/Stored Procedures/CreateState.sql new file mode 100644 index 0000000..07d45ea --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/CreateState.sql @@ -0,0 +1,6 @@ +CREATE PROCEDURE [session].[CreateState] + @SessionId [session].[SessionSurrogateKey], + @Document NVARCHAR(MAX) +AS + +INSERT INTO [session].[State] (SessionId, Document) VALUES (@SessionId, @Document); \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql b/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql new file mode 100644 index 0000000..5c71393 --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql @@ -0,0 +1,7 @@ +CREATE PROCEDURE [session].[ReadStatesBySession] + @SessionId [session].[SessionSurrogateKey] +AS + +SELECT Id, SessionId, Document +FROM [session].[State] +WHERE Id = @SessionId; \ No newline at end of file diff --git a/Shogi.Database/Session/Tables/State.sql b/Shogi.Database/Session/Tables/State.sql new file mode 100644 index 0000000..946f7de --- /dev/null +++ b/Shogi.Database/Session/Tables/State.sql @@ -0,0 +1,9 @@ +CREATE TABLE [session].[State] +( + [Id] BIGINT NOT NULL PRIMARY KEY IDENTITY, + [SessionId] [session].[SessionSurrogateKey] NOT NULL, + [Document] NVARCHAR(MAX) NOT NULL, + + CONSTRAINT [FK_State_ToSession] FOREIGN KEY (SessionId) REFERENCES [session].[Session](Id), + CONSTRAINT [StateDocument must be JSON] CHECK(ISJSON(Document)=1) +) diff --git a/Shogi.Database/Session/Types/SessionSurrogateKey.sql b/Shogi.Database/Session/Types/SessionSurrogateKey.sql index a10a0f4..218c5a2 100644 --- a/Shogi.Database/Session/Types/SessionSurrogateKey.sql +++ b/Shogi.Database/Session/Types/SessionSurrogateKey.sql @@ -1,2 +1,3 @@ -CREATE TYPE [session].[SessionSurrogateKey] +-- C# Guid +CREATE TYPE [session].[SessionSurrogateKey] FROM CHAR(36) NOT NULL diff --git a/Shogi.Database/Shogi.Database.sqlproj b/Shogi.Database/Shogi.Database.sqlproj index 26024ad..8039d62 100644 --- a/Shogi.Database/Shogi.Database.sqlproj +++ b/Shogi.Database/Shogi.Database.sqlproj @@ -79,6 +79,9 @@ + + + diff --git a/Shogi.Domain/ValueObjects/WhichPlayer.cs b/Shogi.Domain/ValueObjects/WhichPlayer.cs index 796c095..ef69beb 100644 --- a/Shogi.Domain/ValueObjects/WhichPlayer.cs +++ b/Shogi.Domain/ValueObjects/WhichPlayer.cs @@ -1,8 +1,7 @@ -namespace Shogi.Domain.ValueObjects +namespace Shogi.Domain.ValueObjects; + +public enum WhichPlayer { - public enum WhichPlayer - { - Player1, - Player2 - } + Player1, + Player2 } diff --git a/Tests/UnitTests/ShogiShould.cs b/Tests/UnitTests/ShogiShould.cs index 064e303..8d00b1a 100644 --- a/Tests/UnitTests/ShogiShould.cs +++ b/Tests/UnitTests/ShogiShould.cs @@ -4,13 +4,8 @@ using System.Linq; namespace UnitTests { - public class ShogiShould + public class ShogiShould(ITestOutputHelper console) { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) - { - this.console = console; - } [Fact] public void MoveAPieceToAnEmptyPosition()