From 460dfd608e7631c7e6e892c39a1700db001bbdb8 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sat, 16 Nov 2024 12:37:56 -0600 Subject: [PATCH] checkpoint --- Shogi.Api/Application/ShogiApplication.cs | 34 ++++++++++++ Shogi.Api/Controllers/SessionsController.cs | 14 ++++- Shogi.Api/Extensions/ContractsExtensions.cs | 8 --- Shogi.Api/Repositories/QueryRepository.cs | 25 +++++++++ Shogi.Contracts/Types/BoardState.cs | 9 ++-- .../Stored Procedures/ReadStatesBySession.sql | 3 +- .../GameBoard/GameBoardPresentation.razor | 53 ++++++++++++------- .../GameBoard/GameboardPresentation.razor.css | 11 +++- .../Play/GameBoard/SeatedGameBoard.razor | 6 +-- Shogi.UI/Pages/Play/GamePiece.razor | 25 +++++---- 10 files changed, 139 insertions(+), 49 deletions(-) diff --git a/Shogi.Api/Application/ShogiApplication.cs b/Shogi.Api/Application/ShogiApplication.cs index 61ae00a..06fe242 100644 --- a/Shogi.Api/Application/ShogiApplication.cs +++ b/Shogi.Api/Application/ShogiApplication.cs @@ -129,4 +129,38 @@ public class ShogiApplication( return userManager.Users.FirstOrDefault(u => u.Id == userId)?.UserName!; } + + public async Task ReadSessionSnapshots(string sessionId) + { + var session = this.ReadSession(sessionId); + if (session == null) + { + return new NotFoundResult(); + } + + var snapshots = await queryRepository.ReadSessionSnapshots(sessionId); + + var boardStates = snapshots.Select(snap => new Contracts.Types.BoardState + { + Board = snap.Board.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value == null + ? null + : new Contracts.Types.Piece + { + IsPromoted = kvp.Value.IsPromoted, + Owner = (Contracts.Types.WhichPlayer)kvp.Value.Owner, + WhichPiece = (Contracts.Types.WhichPiece)kvp.Value.WhichPiece, + }), + Player1Hand = snap.Player1Hand.Cast().ToArray(), + Player2Hand = snap.Player2Hand.Cast().ToArray(), + PlayerInCheck = snap.PlayerInCheck == null ? null : (Contracts.Types.WhichPlayer)snap.PlayerInCheck, + Victor = snap.IsGameOver + ? snap.PlayerInCheck == Repositories.Dto.SessionState.WhichPlayer.Player1 ? Contracts.Types.WhichPlayer.Player2 : Contracts.Types.WhichPlayer.Player1 + : null, + WhoseTurn = (Contracts.Types.WhichPlayer)snap.WhoseTurn, + }); + + return new OkObjectResult(boardStates.ToArray()); + } } diff --git a/Shogi.Api/Controllers/SessionsController.cs b/Shogi.Api/Controllers/SessionsController.cs index 95b3a2b..997e6b8 100644 --- a/Shogi.Api/Controllers/SessionsController.cs +++ b/Shogi.Api/Controllers/SessionsController.cs @@ -65,8 +65,8 @@ public class SessionsController( BoardState = new BoardState { Board = session.Board.BoardState.State.ToContract(), - Player1Hand = session.Board.BoardState.Player1Hand.ToContract(), - Player2Hand = session.Board.BoardState.Player2Hand.ToContract(), + Player1Hand = session.Board.BoardState.Player1Hand.Select(p => p.WhichPiece.ToContract()).ToArray(), + Player2Hand = session.Board.BoardState.Player2Hand.Select(p => p.WhichPiece.ToContract()).ToArray(), PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(), WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(), Victor = session.Board.BoardState.IsCheckmate @@ -118,4 +118,14 @@ public class SessionsController( return await application.MovePiece(id, sessionId, command); } + + /// + /// Returns an array of board states, one per player move of the given session, in the same order that player moves occurred. + /// + [HttpGet("{sessionId}/History")] + [AllowAnonymous] + public async Task GetHistory([FromRoute] string sessionId) + { + return await application.ReadSessionSnapshots(sessionId); + } } diff --git a/Shogi.Api/Extensions/ContractsExtensions.cs b/Shogi.Api/Extensions/ContractsExtensions.cs index 71321e0..4bcf605 100644 --- a/Shogi.Api/Extensions/ContractsExtensions.cs +++ b/Shogi.Api/Extensions/ContractsExtensions.cs @@ -38,14 +38,6 @@ public static class ContractsExtensions 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/Repositories/QueryRepository.cs b/Shogi.Api/Repositories/QueryRepository.cs index 6b489fb..6b90a38 100644 --- a/Shogi.Api/Repositories/QueryRepository.cs +++ b/Shogi.Api/Repositories/QueryRepository.cs @@ -1,7 +1,9 @@ using Dapper; using Shogi.Api.Repositories.Dto; +using Shogi.Api.Repositories.Dto.SessionState; using System.Data; using System.Data.SqlClient; +using System.Text.Json; namespace Shogi.Api.Repositories; @@ -21,4 +23,27 @@ public class QueryRepository(IConfiguration configuration) return await results.ReadAsync(); } + + public async Task> ReadSessionSnapshots(string sessionId) + { + using var connection = new SqlConnection(this.connectionString); + var command = connection.CreateCommand(); + command.CommandText = "session.ReadStatesBySession"; + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("SessionId", sessionId); + + await using var reader = await command.ExecuteReaderAsync(); + var documents = new List(20); + while (await reader.ReadAsync()) + { + var json = reader.GetString("Document"); + var document = JsonSerializer.Deserialize(json); + if (document != null) + { + documents.Add(document); + } + } + + return documents; + } } diff --git a/Shogi.Contracts/Types/BoardState.cs b/Shogi.Contracts/Types/BoardState.cs index ef9b987..05c9d6f 100644 --- a/Shogi.Contracts/Types/BoardState.cs +++ b/Shogi.Contracts/Types/BoardState.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Shogi.Contracts.Types; public class BoardState { - public Dictionary Board { get; set; } = new Dictionary(); - public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty(); - public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty(); + public Dictionary Board { get; set; } = []; + public IReadOnlyCollection Player1Hand { get; set; } = []; + public IReadOnlyCollection Player2Hand { get; set; } = []; public WhichPlayer? PlayerInCheck { get; set; } public WhichPlayer WhoseTurn { get; set; } public WhichPlayer? Victor { get; set; } diff --git a/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql b/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql index 5c71393..e3b93ed 100644 --- a/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql +++ b/Shogi.Database/Session/Stored Procedures/ReadStatesBySession.sql @@ -4,4 +4,5 @@ AS SELECT Id, SessionId, Document FROM [session].[State] -WHERE Id = @SessionId; \ No newline at end of file +WHERE Id = @SessionId +ORDER BY Id ASC; \ No newline at end of file diff --git a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor b/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor index 8dbd0b0..cfd1dee 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor +++ b/Shogi.UI/Pages/Play/GameBoard/GameBoardPresentation.razor @@ -2,6 +2,16 @@ @using System.Text.Json;
+ +
+
+
+ Replay + + +
+
+
@for (var rank = 1; rank < 10; rank++) @@ -17,7 +27,7 @@ style="grid-area: @position"> @if (piece != null) { - + } } @@ -57,7 +67,7 @@ @foreach (var piece in opponentHand) {
- +
} } @@ -86,12 +96,12 @@
@if (userHand.Any()) { - @foreach (var piece in userHand) + @foreach (var whichPiece in userHand) { -
- + data-selected="@(whichPiece == SelectedPieceFromHand)"> +
} } @@ -104,6 +114,7 @@
@code { + static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; /// @@ -116,21 +127,26 @@ [Parameter] public WhichPiece? SelectedPieceFromHand { get; set; } // TODO: Exchange these OnClick actions for events like "SelectionChangedEvent" and "MoveFromBoardEvent" and "MoveFromHandEvent". [Parameter] public EventCallback OnClickTile { get; set; } - [Parameter] public EventCallback OnClickHand { get; set; } + [Parameter] public EventCallback OnClickHand { get; set; } [Parameter] public EventCallback OnClickJoinGame { get; set; } [Parameter] public bool UseSideboard { get; set; } = true; + [Parameter] public IList History { get; set; } - private IReadOnlyCollection opponentHand; - private IReadOnlyCollection userHand; + private bool Yep => History.Count == 0; + + private IReadOnlyCollection opponentHand; + private IReadOnlyCollection userHand; private string? userName; private string? opponentName; + private int historyIndex; public GameBoardPresentation() { - opponentHand = Array.Empty(); - userHand = Array.Empty(); + opponentHand = []; + userHand = []; userName = string.Empty; opponentName = string.Empty; + History = []; } protected override void OnParametersSet() @@ -138,8 +154,8 @@ base.OnParametersSet(); if (Session == null) { - opponentHand = Array.Empty(); - userHand = Array.Empty(); + opponentHand = []; + userHand = []; userName = string.Empty; opponentName = string.Empty; } @@ -158,17 +174,16 @@ ? this.Session.Player2 ?? "Empty Seat" : this.Session.Player1; } + + + + Console.WriteLine("Count: {0}", History.Count); } private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective; - private bool IsPlayerInCheck => Session?.BoardState.PlayerInCheck == Perspective; - private bool IsOpponentInCheck => Session?.BoardState.PlayerInCheck != null && Session.BoardState.PlayerInCheck != Perspective; - private bool IsPlayerVictor => Session?.BoardState.Victor == Perspective; - - private bool IsOpponentVictor => Session?.BoardState.Victor != null && Session.BoardState.Victor != Perspective; private Func OnClickTileInternal(string position) => () => @@ -180,7 +195,7 @@ return Task.CompletedTask; }; - private Func OnClickHandInternal(Piece piece) => () => + private Func OnClickHandInternal(WhichPiece piece) => () => { if (IsMyTurn) { diff --git a/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css b/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css index 26133b8..2c688db 100644 --- a/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css +++ b/Shogi.UI/Pages/Play/GameBoard/GameboardPresentation.razor.css @@ -2,7 +2,7 @@ --ratio: 0.9; display: grid; grid-template-columns: min-content repeat(9, minmax(2rem, 4rem)) max-content; - grid-template-rows: repeat(9, 1fr) auto; + grid-template-rows: auto repeat(9, 1fr) auto; background-color: #444; gap: 3px; place-self: center; @@ -98,6 +98,15 @@ padding: 0.5rem; } + .controls { + grid-column: span 11; + + display: flex; + } + + .controls .history { + + } @media all and (max-width: 1000px) { .game-board { diff --git a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor index fc66d1c..c5d1867 100644 --- a/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor +++ b/Shogi.UI/Pages/Play/GameBoard/SeatedGameBoard.razor @@ -136,17 +136,17 @@ } } - void OnClickHand(Piece piece) + void OnClickHand(WhichPiece piece) { if (showPromotePrompt) return; // Prevent selecting from both the hand and the board. selectedBoardPosition = null; - selectedPieceFromHand = piece.WhichPiece == selectedPieceFromHand + selectedPieceFromHand = piece== selectedPieceFromHand // Deselecting the already-selected piece ? selectedPieceFromHand = null - : selectedPieceFromHand = piece.WhichPiece; + : selectedPieceFromHand = piece; StateHasChanged(); } diff --git a/Shogi.UI/Pages/Play/GamePiece.razor b/Shogi.UI/Pages/Play/GamePiece.razor index eedd33f..d2532ea 100644 --- a/Shogi.UI/Pages/Play/GamePiece.razor +++ b/Shogi.UI/Pages/Play/GamePiece.razor @@ -1,32 +1,41 @@ @using Shogi.Contracts.Types -
- @switch (Piece?.WhichPiece) +
+ @switch (Piece) { + case WhichPiece.Bishop: break; + case WhichPiece.GoldGeneral: break; + case WhichPiece.King: break; + case WhichPiece.Knight: break; + case WhichPiece.Lance: break; + case WhichPiece.Pawn: break; + case WhichPiece.Rook: break; + case WhichPiece.SilverGeneral: break; + default: @*render nothing*@ break; @@ -34,15 +43,11 @@
@code { - [Parameter] - public Contracts.Types.Piece? Piece { get; set; } + [Parameter][EditorRequired] public WhichPiece? Piece { get; set; } + [Parameter] public bool IsPromoted { get; set; } = false; + [Parameter] public bool RenderUpsideDown { get; set; } - [Parameter] - public WhichPlayer Perspective { get; set; } - - private bool IsPromoted => Piece != null && Piece.IsPromoted; - - private string HtmlTitle => Piece?.WhichPiece switch + private string HtmlTitle => this.Piece switch { WhichPiece.Bishop => "Bishop", WhichPiece.GoldGeneral => "Gold General",