checkpoint
This commit is contained in:
@@ -129,4 +129,38 @@ public class ShogiApplication(
|
|||||||
|
|
||||||
return userManager.Users.FirstOrDefault(u => u.Id == userId)?.UserName!;
|
return userManager.Users.FirstOrDefault(u => u.Id == userId)?.UserName!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<Contracts.Types.WhichPiece>().ToArray(),
|
||||||
|
Player2Hand = snap.Player2Hand.Cast<Contracts.Types.WhichPiece>().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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ public class SessionsController(
|
|||||||
BoardState = new BoardState
|
BoardState = new BoardState
|
||||||
{
|
{
|
||||||
Board = session.Board.BoardState.State.ToContract(),
|
Board = session.Board.BoardState.State.ToContract(),
|
||||||
Player1Hand = session.Board.BoardState.Player1Hand.ToContract(),
|
Player1Hand = session.Board.BoardState.Player1Hand.Select(p => p.WhichPiece.ToContract()).ToArray(),
|
||||||
Player2Hand = session.Board.BoardState.Player2Hand.ToContract(),
|
Player2Hand = session.Board.BoardState.Player2Hand.Select(p => p.WhichPiece.ToContract()).ToArray(),
|
||||||
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(),
|
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(),
|
||||||
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(),
|
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(),
|
||||||
Victor = session.Board.BoardState.IsCheckmate
|
Victor = session.Board.BoardState.IsCheckmate
|
||||||
@@ -118,4 +118,14 @@ public class SessionsController(
|
|||||||
|
|
||||||
return await application.MovePiece(id, sessionId, command);
|
return await application.MovePiece(id, sessionId, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array of board states, one per player move of the given session, in the same order that player moves occurred.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{sessionId}/History")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetHistory([FromRoute] string sessionId)
|
||||||
|
{
|
||||||
|
return await application.ReadSessionSnapshots(sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,6 @@ public static class ContractsExtensions
|
|||||||
WhichPiece = piece.WhichPiece.ToContract()
|
WhichPiece = piece.WhichPiece.ToContract()
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IReadOnlyList<Piece> ToContract(this List<Domain.ValueObjects.Piece> pieces)
|
|
||||||
{
|
|
||||||
return pieces
|
|
||||||
.Select(ToContract)
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
|
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
|
||||||
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Shogi.Api.Repositories.Dto;
|
using Shogi.Api.Repositories.Dto;
|
||||||
|
using Shogi.Api.Repositories.Dto.SessionState;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.SqlClient;
|
using System.Data.SqlClient;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Shogi.Api.Repositories;
|
namespace Shogi.Api.Repositories;
|
||||||
|
|
||||||
@@ -21,4 +23,27 @@ public class QueryRepository(IConfiguration configuration)
|
|||||||
|
|
||||||
return await results.ReadAsync<SessionDto>();
|
return await results.ReadAsync<SessionDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SessionStateDocument>> 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<SessionStateDocument>(20);
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
var json = reader.GetString("Document");
|
||||||
|
var document = JsonSerializer.Deserialize<SessionStateDocument>(json);
|
||||||
|
if (document != null)
|
||||||
|
{
|
||||||
|
documents.Add(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Shogi.Contracts.Types;
|
namespace Shogi.Contracts.Types;
|
||||||
|
|
||||||
public class BoardState
|
public class BoardState
|
||||||
{
|
{
|
||||||
public Dictionary<string, Piece?> Board { get; set; } = new Dictionary<string, Piece?>();
|
public Dictionary<string, Piece?> Board { get; set; } = [];
|
||||||
public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
|
public IReadOnlyCollection<WhichPiece> Player1Hand { get; set; } = [];
|
||||||
public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
|
public IReadOnlyCollection<WhichPiece> Player2Hand { get; set; } = [];
|
||||||
public WhichPlayer? PlayerInCheck { get; set; }
|
public WhichPlayer? PlayerInCheck { get; set; }
|
||||||
public WhichPlayer WhoseTurn { get; set; }
|
public WhichPlayer WhoseTurn { get; set; }
|
||||||
public WhichPlayer? Victor { get; set; }
|
public WhichPlayer? Victor { get; set; }
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ AS
|
|||||||
|
|
||||||
SELECT Id, SessionId, Document
|
SELECT Id, SessionId, Document
|
||||||
FROM [session].[State]
|
FROM [session].[State]
|
||||||
WHERE Id = @SessionId;
|
WHERE Id = @SessionId
|
||||||
|
ORDER BY Id ASC;
|
||||||
@@ -2,6 +2,16 @@
|
|||||||
@using System.Text.Json;
|
@using System.Text.Json;
|
||||||
|
|
||||||
<article class="game-board">
|
<article class="game-board">
|
||||||
|
<!-- Controls -->
|
||||||
|
<header class="controls">
|
||||||
|
<form @onsubmit:preventDefault>
|
||||||
|
<fieldset class="history" disabled=@Yep>
|
||||||
|
<legend>Replay</legend>
|
||||||
|
<button disabled=@Yep><</button>
|
||||||
|
<button>></button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
<!-- Game board -->
|
<!-- Game board -->
|
||||||
<section class="board" data-perspective="@Perspective">
|
<section class="board" data-perspective="@Perspective">
|
||||||
@for (var rank = 1; rank < 10; rank++)
|
@for (var rank = 1; rank < 10; rank++)
|
||||||
@@ -17,7 +27,7 @@
|
|||||||
style="grid-area: @position">
|
style="grid-area: @position">
|
||||||
@if (piece != null)
|
@if (piece != null)
|
||||||
{
|
{
|
||||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
<GamePiece Piece="piece.WhichPiece" RenderUpsideDown="@(piece.Owner != Perspective)" IsPromoted="piece.IsPromoted" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -57,7 +67,7 @@
|
|||||||
@foreach (var piece in opponentHand)
|
@foreach (var piece in opponentHand)
|
||||||
{
|
{
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
<GamePiece Piece="piece" RenderUpsideDown="true" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,12 +96,12 @@
|
|||||||
<div class="hand">
|
<div class="hand">
|
||||||
@if (userHand.Any())
|
@if (userHand.Any())
|
||||||
{
|
{
|
||||||
@foreach (var piece in userHand)
|
@foreach (var whichPiece in userHand)
|
||||||
{
|
{
|
||||||
<div @onclick="OnClickHandInternal(piece)"
|
<div @onclick="OnClickHandInternal(whichPiece)"
|
||||||
class="tile"
|
class="tile"
|
||||||
data-selected="@(piece.WhichPiece == SelectedPieceFromHand)">
|
data-selected="@(whichPiece == SelectedPieceFromHand)">
|
||||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
<GamePiece Piece="whichPiece" RenderUpsideDown="false" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +114,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,21 +127,26 @@
|
|||||||
[Parameter] public WhichPiece? SelectedPieceFromHand { get; set; }
|
[Parameter] public WhichPiece? SelectedPieceFromHand { get; set; }
|
||||||
// TODO: Exchange these OnClick actions for events like "SelectionChangedEvent" and "MoveFromBoardEvent" and "MoveFromHandEvent".
|
// TODO: Exchange these OnClick actions for events like "SelectionChangedEvent" and "MoveFromBoardEvent" and "MoveFromHandEvent".
|
||||||
[Parameter] public EventCallback<string> OnClickTile { get; set; }
|
[Parameter] public EventCallback<string> OnClickTile { get; set; }
|
||||||
[Parameter] public EventCallback<Piece> OnClickHand { get; set; }
|
[Parameter] public EventCallback<WhichPiece> OnClickHand { get; set; }
|
||||||
[Parameter] public EventCallback OnClickJoinGame { get; set; }
|
[Parameter] public EventCallback OnClickJoinGame { get; set; }
|
||||||
[Parameter] public bool UseSideboard { get; set; } = true;
|
[Parameter] public bool UseSideboard { get; set; } = true;
|
||||||
|
[Parameter] public IList<BoardState> History { get; set; }
|
||||||
|
|
||||||
private IReadOnlyCollection<Piece> opponentHand;
|
private bool Yep => History.Count == 0;
|
||||||
private IReadOnlyCollection<Piece> userHand;
|
|
||||||
|
private IReadOnlyCollection<WhichPiece> opponentHand;
|
||||||
|
private IReadOnlyCollection<WhichPiece> userHand;
|
||||||
private string? userName;
|
private string? userName;
|
||||||
private string? opponentName;
|
private string? opponentName;
|
||||||
|
private int historyIndex;
|
||||||
|
|
||||||
public GameBoardPresentation()
|
public GameBoardPresentation()
|
||||||
{
|
{
|
||||||
opponentHand = Array.Empty<Piece>();
|
opponentHand = [];
|
||||||
userHand = Array.Empty<Piece>();
|
userHand = [];
|
||||||
userName = string.Empty;
|
userName = string.Empty;
|
||||||
opponentName = string.Empty;
|
opponentName = string.Empty;
|
||||||
|
History = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
@@ -138,8 +154,8 @@
|
|||||||
base.OnParametersSet();
|
base.OnParametersSet();
|
||||||
if (Session == null)
|
if (Session == null)
|
||||||
{
|
{
|
||||||
opponentHand = Array.Empty<Piece>();
|
opponentHand = [];
|
||||||
userHand = Array.Empty<Piece>();
|
userHand = [];
|
||||||
userName = string.Empty;
|
userName = string.Empty;
|
||||||
opponentName = string.Empty;
|
opponentName = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -158,17 +174,16 @@
|
|||||||
? this.Session.Player2 ?? "Empty Seat"
|
? this.Session.Player2 ?? "Empty Seat"
|
||||||
: this.Session.Player1;
|
: this.Session.Player1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Console.WriteLine("Count: {0}", History.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective;
|
private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective;
|
||||||
|
|
||||||
private bool IsPlayerInCheck => Session?.BoardState.PlayerInCheck == Perspective;
|
private bool IsPlayerInCheck => Session?.BoardState.PlayerInCheck == Perspective;
|
||||||
|
|
||||||
private bool IsOpponentInCheck => Session?.BoardState.PlayerInCheck != null && Session.BoardState.PlayerInCheck != Perspective;
|
private bool IsOpponentInCheck => Session?.BoardState.PlayerInCheck != null && Session.BoardState.PlayerInCheck != Perspective;
|
||||||
|
|
||||||
private bool IsPlayerVictor => Session?.BoardState.Victor == Perspective;
|
private bool IsPlayerVictor => Session?.BoardState.Victor == Perspective;
|
||||||
|
|
||||||
|
|
||||||
private bool IsOpponentVictor => Session?.BoardState.Victor != null && Session.BoardState.Victor != Perspective;
|
private bool IsOpponentVictor => Session?.BoardState.Victor != null && Session.BoardState.Victor != Perspective;
|
||||||
|
|
||||||
private Func<Task> OnClickTileInternal(string position) => () =>
|
private Func<Task> OnClickTileInternal(string position) => () =>
|
||||||
@@ -180,7 +195,7 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
private Func<Task> OnClickHandInternal(Piece piece) => () =>
|
private Func<Task> OnClickHandInternal(WhichPiece piece) => () =>
|
||||||
{
|
{
|
||||||
if (IsMyTurn)
|
if (IsMyTurn)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
--ratio: 0.9;
|
--ratio: 0.9;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content repeat(9, minmax(2rem, 4rem)) max-content;
|
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;
|
background-color: #444;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
place-self: center;
|
place-self: center;
|
||||||
@@ -98,6 +98,15 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
grid-column: span 11;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls .history {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 1000px) {
|
@media all and (max-width: 1000px) {
|
||||||
.game-board {
|
.game-board {
|
||||||
|
|||||||
@@ -136,17 +136,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnClickHand(Piece piece)
|
void OnClickHand(WhichPiece piece)
|
||||||
{
|
{
|
||||||
if (showPromotePrompt) return;
|
if (showPromotePrompt) return;
|
||||||
|
|
||||||
// Prevent selecting from both the hand and the board.
|
// Prevent selecting from both the hand and the board.
|
||||||
selectedBoardPosition = null;
|
selectedBoardPosition = null;
|
||||||
|
|
||||||
selectedPieceFromHand = piece.WhichPiece == selectedPieceFromHand
|
selectedPieceFromHand = piece== selectedPieceFromHand
|
||||||
// Deselecting the already-selected piece
|
// Deselecting the already-selected piece
|
||||||
? selectedPieceFromHand = null
|
? selectedPieceFromHand = null
|
||||||
: selectedPieceFromHand = piece.WhichPiece;
|
: selectedPieceFromHand = piece;
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
@using Shogi.Contracts.Types
|
@using Shogi.Contracts.Types
|
||||||
|
|
||||||
<div class="game-piece" title="@HtmlTitle" data-upsidedown="@(Piece?.Owner != Perspective)" data-owner="@Piece?.Owner.ToString()">
|
<div class="game-piece" title="@HtmlTitle" data-upsidedown="@RenderUpsideDown">
|
||||||
@switch (Piece?.WhichPiece)
|
@switch (Piece)
|
||||||
{
|
{
|
||||||
|
|
||||||
case WhichPiece.Bishop:
|
case WhichPiece.Bishop:
|
||||||
<Bishop IsPromoted="@IsPromoted" />
|
<Bishop IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.GoldGeneral:
|
case WhichPiece.GoldGeneral:
|
||||||
<GoldGeneral />
|
<GoldGeneral />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.King:
|
case WhichPiece.King:
|
||||||
<ChallengingKing />
|
<ChallengingKing />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.Knight:
|
case WhichPiece.Knight:
|
||||||
<Knight IsPromoted="@IsPromoted" />
|
<Knight IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.Lance:
|
case WhichPiece.Lance:
|
||||||
<Lance IsPromoted="@IsPromoted" />
|
<Lance IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.Pawn:
|
case WhichPiece.Pawn:
|
||||||
<Pawn IsPromoted="@IsPromoted" />
|
<Pawn IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.Rook:
|
case WhichPiece.Rook:
|
||||||
<Rook IsPromoted="@IsPromoted" />
|
<Rook IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WhichPiece.SilverGeneral:
|
case WhichPiece.SilverGeneral:
|
||||||
<SilverGeneral IsPromoted="@IsPromoted" />
|
<SilverGeneral IsPromoted="@IsPromoted" />
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@*render nothing*@
|
@*render nothing*@
|
||||||
break;
|
break;
|
||||||
@@ -34,15 +43,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter][EditorRequired] public WhichPiece? Piece { get; set; }
|
||||||
public Contracts.Types.Piece? Piece { get; set; }
|
[Parameter] public bool IsPromoted { get; set; } = false;
|
||||||
|
[Parameter] public bool RenderUpsideDown { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
private string HtmlTitle => this.Piece switch
|
||||||
public WhichPlayer Perspective { get; set; }
|
|
||||||
|
|
||||||
private bool IsPromoted => Piece != null && Piece.IsPromoted;
|
|
||||||
|
|
||||||
private string HtmlTitle => Piece?.WhichPiece switch
|
|
||||||
{
|
{
|
||||||
WhichPiece.Bishop => "Bishop",
|
WhichPiece.Bishop => "Bishop",
|
||||||
WhichPiece.GoldGeneral => "Gold General",
|
WhichPiece.GoldGeneral => "Gold General",
|
||||||
|
|||||||
Reference in New Issue
Block a user