- @if (OpponentHand.Any())
+ @if (opponentHand.Any())
{
- @foreach (var piece in OpponentHand)
+ @foreach (var piece in opponentHand)
{
@@ -86,6 +86,8 @@
+
@opponentName
+
@userName
@@ -93,15 +95,15 @@
{
Seat is Empty
-
+
}
else
{
- @if (UserHand.Any())
+ @if (userHand.Any())
{
- @foreach (var piece in UserHand)
+ @foreach (var piece in userHand)
{
@@ -121,6 +123,11 @@
@code {
+ static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
+
+ ///
+ /// When true, an icon is displayed indicating that the user is spectating.
+ ///
[Parameter] public bool IsSpectating { get; set; } = false;
[Parameter] public WhichPlayer Perspective { get; set; }
[Parameter] public Session? Session { get; set; }
@@ -130,32 +137,48 @@
[Parameter] public Func
? OnClickHand { get; set; }
[Parameter] public Func? OnClickJoinGame { get; set; }
- static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
+ private IReadOnlyCollection opponentHand;
+ private IReadOnlyCollection userHand;
+ private string? userName;
+ private string? opponentName;
- private IReadOnlyCollection OpponentHand
+ public GameBoardPresentation()
{
- get
- {
- if (this.Session == null) return Array.Empty();
-
- return Perspective == WhichPlayer.Player1
- ? this.Session.BoardState.Player1Hand
- : this.Session.BoardState.Player2Hand;
- }
+ opponentHand = Array.Empty();
+ userHand = Array.Empty();
+ userName = string.Empty;
+ opponentName = string.Empty;
}
- IReadOnlyCollection UserHand
- {
- get
- {
- if (this.Session == null) return Array.Empty();
- return Perspective == WhichPlayer.Player1
+ protected override void OnParametersSet()
+ {
+ Console.WriteLine("Params changed. SelectedPosition = {0}", SelectedPosition);
+ base.OnParametersSet();
+ if (Session == null)
+ {
+ opponentHand = Array.Empty();
+ userHand = Array.Empty();
+ userName = string.Empty;
+ opponentName = string.Empty;
+ }
+ else
+ {
+ opponentHand = Perspective == WhichPlayer.Player1
+ ? this.Session.BoardState.Player2Hand
+ : this.Session.BoardState.Player1Hand;
+ userHand = Perspective == WhichPlayer.Player1
? this.Session.BoardState.Player1Hand
: this.Session.BoardState.Player2Hand;
+ userName = Perspective == WhichPlayer.Player1
+ ? this.Session.Player1
+ : this.Session.Player2;
+ opponentName = Perspective == WhichPlayer.Player1
+ ? this.Session.Player2
+ : this.Session.Player1;
}
}
private Action OnClickTileInternal(Piece? piece, string position) => () => OnClickTile?.Invoke(piece, position);
private Action OnClickHandInternal(Piece piece) => () => OnClickHand?.Invoke(piece);
- private Action OnClickJoinGameInternal() => () => OnClickJoinGame?.Invoke();
+ private void OnClickJoinGameInternal() => OnClickJoinGame?.Invoke();
}
diff --git a/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
index af5bdc2..4bb7fd1 100644
--- a/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
+++ b/Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
@@ -9,18 +9,26 @@
Perspective="Perspective"
OnClickHand="OnClickHand"
OnClickTile="OnClickTile"
- OnClickJoinGame="OnClickJoinGame" />
+ SelectedPosition="@selectedBoardPosition" />
@code {
[Parameter, EditorRequired]
public WhichPlayer Perspective { get; set; }
[Parameter, EditorRequired]
public Session Session { get; set; }
- [Parameter] public Func? OnRefetchSession { get; set; }
private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective;
private string? selectedBoardPosition;
private WhichPiece? selectedPieceFromHand;
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+ if (Session == null)
+ {
+ throw new ArgumentException($"{nameof(Session)} cannot be null.", nameof(Session));
+ }
+ }
+
bool ShouldPromptForPromotion(string position)
{
if (Perspective == WhichPlayer.Player1 && Regex.IsMatch(position, ".[7-9]"))
@@ -36,12 +44,16 @@
async Task OnClickTile(Piece? piece, string position)
{
+ Console.WriteLine("Is my turn?");
+ Console.WriteLine(true);
if (!IsMyTurn) return;
if (selectedBoardPosition == null || piece?.Owner == Perspective)
{
// Select a position.
+ Console.WriteLine("Position {0}", position);
selectedBoardPosition = position;
+ StateHasChanged();
return;
}
if (selectedBoardPosition == position)
@@ -77,16 +89,4 @@
selectedPieceFromHand = piece.WhichPiece;
await Task.CompletedTask;
}
-
- async Task OnClickJoinGame()
- {
- if (Session != null && OnRefetchSession != null)
- {
- var status = await ShogiApi.PatchJoinGame(Session.SessionName);
- if (status == HttpStatusCode.OK)
- {
- await OnRefetchSession.Invoke();
- }
- }
- }
}
diff --git a/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor b/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
index 4c2f5df..8a5f386 100644
--- a/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
+++ b/Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
@@ -1,7 +1,27 @@
@using Contracts.Types;
+@using System.Net;
+@inject IShogiApi ShogiApi;
-
+
@code {
[Parameter] public Session Session { get; set; }
+
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+ if (Session == null)
+ {
+ throw new ArgumentException($"{nameof(Session)} cannot be null.", nameof(Session));
+ }
+ }
+
+ async Task OnClickJoinGame()
+ {
+ var response = await ShogiApi.PatchJoinGame(Session.SessionName);
+ response.EnsureSuccessStatusCode();
+ }
}
diff --git a/Shogi.UI/Pages/Home/GameBrowser.razor b/Shogi.UI/Pages/Home/GameBrowser.razor
index a00a961..886d39e 100644
--- a/Shogi.UI/Pages/Home/GameBrowser.razor
+++ b/Shogi.UI/Pages/Home/GameBrowser.razor
@@ -1,7 +1,10 @@
-@using Shogi.Contracts.Types;
+@implements IDisposable;
+
+@using Shogi.Contracts.Types;
@using System.ComponentModel.DataAnnotations;
@using System.Net;
@using System.Text.Json;
+
@inject IShogiApi ShogiApi;
@inject ShogiSocket ShogiSocket;
@inject AccountState Account;
@@ -76,7 +79,6 @@
The name you chose is taken; choose another.
}
-
@@ -92,17 +94,12 @@
private SessionMetadata? activeSession;
private HttpStatusCode? createSessionStatusCode;
- protected override async Task OnInitializedAsync()
+ protected override void OnInitialized()
{
- ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
- Account.LoginChangedEvent += async (sender, message) =>
- {
- Console.WriteLine($"LoginEvent. Message={JsonSerializer.Serialize(message)}.");
- if (message.User != null)
- {
- await FetchSessions();
- }
- };
+ base.OnInitialized();
+ ShogiSocket.OnSessionCreated += FetchSessions;
+ ShogiSocket.OnSessionJoined += FetchSessions;
+ Account.LoginChangedEvent += LoginChangedEvent_FetchSessions;
}
string ActiveCss(SessionMetadata s) => s == activeSession ? "active" : string.Empty;
@@ -112,6 +109,14 @@
activeSession = s;
ActiveSessionChanged?.Invoke(s);
}
+ Task LoginChangedEvent_FetchSessions(LoginEventArgs args)
+ {
+ if (args.User != null)
+ {
+ return FetchSessions();
+ }
+ return Task.CompletedTask;
+ }
async Task FetchSessions()
{
@@ -129,6 +134,13 @@
createSessionStatusCode = await ShogiApi.PostSession(createForm.Name, createForm.IsPrivate);
}
+ public void Dispose()
+ {
+ ShogiSocket.OnSessionCreated -= FetchSessions;
+ ShogiSocket.OnSessionJoined -= FetchSessions;
+ Account.LoginChangedEvent -= LoginChangedEvent_FetchSessions;
+ }
+
private class CreateForm
{
[Required]
diff --git a/Shogi.UI/Pages/Home/Home.razor b/Shogi.UI/Pages/Home/Home.razor
index 1ecf90e..f600e12 100644
--- a/Shogi.UI/Pages/Home/Home.razor
+++ b/Shogi.UI/Pages/Home/Home.razor
@@ -26,9 +26,16 @@
@code {
- bool welcomeModalIsVisible = false;
- string activeSessionName = string.Empty;
- ClientWebSocket socket = new ClientWebSocket();
+ private bool welcomeModalIsVisible;
+ private string activeSessionName;
+ private ClientWebSocket socket;
+
+ public Home()
+ {
+ welcomeModalIsVisible = false;
+ activeSessionName = string.Empty;
+ socket = new ClientWebSocket();
+ }
protected override async Task OnInitializedAsync()
{
@@ -40,10 +47,11 @@
}
}
- private void OnLoginChanged(object? sender, LoginEventArgs args)
+ private Task OnLoginChanged(LoginEventArgs args)
{
welcomeModalIsVisible = args.User == null;
StateHasChanged();
+ return Task.CompletedTask;
}
private void OnChangeSession(SessionMetadata s)
{
diff --git a/Shogi.UI/Pages/Home/PageHeader.razor b/Shogi.UI/Pages/Home/PageHeader.razor
index 1b57f9d..7aac75b 100644
--- a/Shogi.UI/Pages/Home/PageHeader.razor
+++ b/Shogi.UI/Pages/Home/PageHeader.razor
@@ -5,10 +5,10 @@
Shogi
@if (user != null)
{
-
-
@user.Value.DisplayName
-
-
+
+
@user.Value.DisplayName
+
+
}
@*
*@
@@ -21,11 +21,12 @@
Account.LoginChangedEvent += OnLoginChange;
}
- private void OnLoginChange(object? sender, LoginEventArgs args)
+ private Task OnLoginChange(LoginEventArgs args)
{
if (args == null)
throw new ArgumentException(nameof(args));
user = args.User;
StateHasChanged();
+ return Task.CompletedTask;
}
}
diff --git a/Shogi.UI/Program.cs b/Shogi.UI/Program.cs
index 8c3f8c9..6a5d3a2 100644
--- a/Shogi.UI/Program.cs
+++ b/Shogi.UI/Program.cs
@@ -17,49 +17,55 @@ await builder.Build().RunAsync();
static void ConfigureDependencies(IServiceCollection services, IConfiguration configuration)
{
- /**
+ /**
* Why two HTTP clients?
* See qhttps://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?source=recommendations&view=aspnetcore-6.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client
*/
- var shogiApiUrl = new Uri(configuration["ShogiApiUrl"], UriKind.Absolute);
- services
- .AddHttpClient(ShogiApi.MsalClientName, client => client.BaseAddress = shogiApiUrl)
- .AddHttpMessageHandler
();
- services
- .AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl)
- .AddHttpMessageHandler();
+ var baseUrl = configuration["ShogiApiUrl"];
+ if (string.IsNullOrWhiteSpace(baseUrl))
+ {
+ throw new InvalidOperationException("ShogiApiUrl configuration is missing.");
+ }
- // Authorization
- services.AddMsalAuthentication(options =>
- {
- configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
- options.ProviderOptions.LoginMode = "redirect";
- });
- services.AddOidcAuthentication(options =>
- {
- // Configure your authentication provider options here.
- // For more information, see https://aka.ms/blazor-standalone-auth
- configuration.Bind("AzureAd", options.ProviderOptions);
- options.ProviderOptions.ResponseType = "code";
- });
+ var shogiApiUrl = new Uri(baseUrl, UriKind.Absolute);
+ services
+ .AddHttpClient(ShogiApi.MsalClientName, client => client.BaseAddress = shogiApiUrl)
+ .AddHttpMessageHandler();
+ services
+ .AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl)
+ .AddHttpMessageHandler();
- // https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-6.0#service-lifetime
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
+ // Authorization
+ services.AddMsalAuthentication(options =>
+ {
+ configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
+ options.ProviderOptions.LoginMode = "redirect";
+ });
+ services.AddOidcAuthentication(options =>
+ {
+ // Configure your authentication provider options here.
+ // For more information, see https://aka.ms/blazor-standalone-auth
+ configuration.Bind("AzureAd", options.ProviderOptions);
+ options.ProviderOptions.ResponseType = "code";
+ });
- var serializerOptions = new JsonSerializerOptions
- {
- DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- WriteIndented = true
- };
- services.AddScoped((sp) => serializerOptions);
+ // https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-6.0#service-lifetime
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ var serializerOptions = new JsonSerializerOptions
+ {
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true
+ };
+ services.AddScoped((sp) => serializerOptions);
}
\ No newline at end of file
diff --git a/Shogi.UI/Shared/Events.cs b/Shogi.UI/Shared/Events.cs
new file mode 100644
index 0000000..1f1b93a
--- /dev/null
+++ b/Shogi.UI/Shared/Events.cs
@@ -0,0 +1,8 @@
+namespace Shogi.UI.Shared
+{
+ public static class Events
+ {
+ public delegate Task AsyncEventHandler();
+ public delegate Task AsyncEventHandler(TArgs args);
+ }
+}
diff --git a/Shogi.UI/Shared/LoginDisplay.razor b/Shogi.UI/Shared/LoginDisplay.razor
deleted file mode 100644
index 1f0d155..0000000
--- a/Shogi.UI/Shared/LoginDisplay.razor
+++ /dev/null
@@ -1,24 +0,0 @@
-@*@using Microsoft.AspNetCore.Components.Authorization
-@using Microsoft.AspNetCore.Components.WebAssembly.Authentication*@
-
-@*@inject NavigationManager Navigation
-@inject SignOutSessionStateManager SignOutManager*@
-
-@*
-
- Hello, @context.User.Identity?.Name!
-
-
-
- Log in
-
-*@
-
-@code{
- // https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-6.0#customize-the-authentication-user-interface
- //private async Task BeginSignOut(MouseEventArgs args)
- //{
- // await SignOutManager.SetSignOutState();
- // Navigation.NavigateTo("authentication/logout");
- //}
-}
diff --git a/Shogi.UI/Shared/MyNotAuthorized.razor b/Shogi.UI/Shared/MyNotAuthorized.razor
deleted file mode 100644
index fe34ce9..0000000
--- a/Shogi.UI/Shared/MyNotAuthorized.razor
+++ /dev/null
@@ -1,5 +0,0 @@
-MyNotAuthorized
-
-@code {
-
-}
diff --git a/Shogi.UI/Shared/RedirectToLogin.razor b/Shogi.UI/Shared/RedirectToLogin.razor
deleted file mode 100644
index 8a7fa95..0000000
--- a/Shogi.UI/Shared/RedirectToLogin.razor
+++ /dev/null
@@ -1,14 +0,0 @@
-@inject NavigationManager Navigation
-@inject ModalService ModalService
-@inject AccountManager ShogiService
-
-@**@
-Not implemented!
-
-@code {
- protected override void OnInitialized()
- {
- //ModalService.ShowLoginModal();
- Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
- }
-}
diff --git a/Shogi.UI/Shared/ShogiSocket.cs b/Shogi.UI/Shared/ShogiSocket.cs
index 4d1e911..ea01560 100644
--- a/Shogi.UI/Shared/ShogiSocket.cs
+++ b/Shogi.UI/Shared/ShogiSocket.cs
@@ -4,12 +4,15 @@ using Shogi.Contracts.Types;
using System.Buffers;
using System.Net.WebSockets;
using System.Text.Json;
+using static Shogi.UI.Shared.Events;
namespace Shogi.UI.Shared;
public class ShogiSocket : IDisposable
{
- public event EventHandler? OnCreateGameMessage;
+ public event AsyncEventHandler? OnSessionCreated;
+ public event AsyncEventHandler? OnSessionJoined;
+ public event AsyncEventHandler? OnPlayerMoved;
private readonly ClientWebSocket socket;
private readonly JsonSerializerOptions serializerOptions;
@@ -37,8 +40,11 @@ public class ShogiSocket : IDisposable
await socket.ConnectAsync(this.uriBuilder.Uri, cancelToken.Token);
Console.WriteLine("Socket Connected");
// Fire and forget! I'm way too lazy to write my own javascript interop to a web worker. Nooo thanks.
- var listening = Listen().ContinueWith(antecedent =>
+ _ = Listen().ContinueWith(async antecedent =>
{
+ Console.WriteLine($"Socket fault. {antecedent.Exception}");
+ this.cancelToken.Cancel();
+ await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Page was probably closed or refresh.", CancellationToken.None);
if (antecedent.Exception != null)
{
throw antecedent.Exception;
@@ -58,16 +64,33 @@ public class ShogiSocket : IDisposable
.GetProperty(nameof(ISocketResponse.Action))
.Deserialize();
+ Console.WriteLine($"Socket action: {action}");
switch (action)
{
case SocketAction.SessionCreated:
- Console.WriteLine("Session created event.");
- this.OnCreateGameMessage?.Invoke(this, JsonSerializer.Deserialize(memory, this.serializerOptions)!);
+ if (this.OnSessionCreated is not null)
+ {
+ await this.OnSessionCreated();
+ }
+ break;
+ case SocketAction.SessionJoined:
+ if (this.OnSessionJoined is not null)
+ {
+ await this.OnSessionJoined();
+ }
+ break;
+ case SocketAction.PieceMoved:
+ if (this.OnPlayerMoved is not null)
+ {
+ var args = JsonSerializer.Deserialize(memory[..result.Count], serializerOptions);
+ await this.OnPlayerMoved(args!);
+ }
break;
default:
- break;
+ throw new NotImplementedException($"Socket message for action:{action} is not implemented.");
}
}
+ await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Socket closed because cancellation token was cancelled.", CancellationToken.None);
if (!cancelToken.IsCancellationRequested)
{
throw new InvalidOperationException("Stopped socket listening without cancelling.");