yep
This commit is contained in:
@@ -10,6 +10,6 @@ public interface IShogiApi
|
||||
Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount();
|
||||
Task<CreateTokenResponse?> GetToken();
|
||||
Task GuestLogout();
|
||||
Task PostMove(string sessionName, Move move);
|
||||
Task PostMove(string sessionName, MovePieceCommand move);
|
||||
Task<HttpStatusCode> PostSession(string name, bool isPrivate);
|
||||
}
|
||||
@@ -63,9 +63,9 @@ namespace Shogi.UI.Pages.Home.Api
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task PostMove(string sessionName, Contracts.Types.Move move)
|
||||
public async Task PostMove(string sessionName, MovePieceCommand command)
|
||||
{
|
||||
await this.HttpClient.PostAsJsonAsync($"Sessions{sessionName}/Move", new MovePieceCommand { Move = move });
|
||||
await this.HttpClient.PostAsJsonAsync($"Sessions{sessionName}/Move", command);
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
||||
|
||||
@@ -2,61 +2,142 @@
|
||||
@inject IShogiApi ShogiApi
|
||||
@inject AccountState Account;
|
||||
|
||||
<section class="game-board" data-perspective="@Perspective">
|
||||
@for (var rank = 9; rank > 0; rank--)
|
||||
{
|
||||
foreach (var file in Files)
|
||||
{
|
||||
var position = $"{file}{rank}";
|
||||
var piece = session?.BoardState.Board[position];
|
||||
<div class="tile" data-position="@(position)" style="grid-area: @(position)" @onclick="() => OnClickTile(piece, position)">
|
||||
<GamePiece Piece="piece" Perspective="@Perspective" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="ruler vertical" style="grid-area: rank">
|
||||
<span>9</span>
|
||||
<span>8</span>
|
||||
<span>7</span>
|
||||
<span>6</span>
|
||||
<span>5</span>
|
||||
<span>4</span>
|
||||
<span>3</span>
|
||||
<span>2</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="ruler" style="grid-area: file">
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
<span>G</span>
|
||||
<span>H</span>
|
||||
<span>I</span>
|
||||
</div>
|
||||
</section>
|
||||
<article class="game-board">
|
||||
<!-- Game board -->
|
||||
<section class="board" data-perspective="@Perspective">
|
||||
@for (var rank = 1; rank < 10; rank++)
|
||||
{
|
||||
foreach (var file in Files)
|
||||
{
|
||||
var position = $"{file}{rank}";
|
||||
var piece = session?.BoardState.Board[position];
|
||||
<div class="tile"
|
||||
data-position="@(position)"
|
||||
data-selected="@(piece != null && selectedPosition == position)"
|
||||
style="grid-area: @(position)"
|
||||
@onclick="() => OnClickTile(piece, position)">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="ruler vertical" style="grid-area: rank">
|
||||
<span>9</span>
|
||||
<span>8</span>
|
||||
<span>7</span>
|
||||
<span>6</span>
|
||||
<span>5</span>
|
||||
<span>4</span>
|
||||
<span>3</span>
|
||||
<span>2</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="ruler" style="grid-area: file">
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
<span>G</span>
|
||||
<span>H</span>
|
||||
<span>I</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Side board -->
|
||||
@if (session != null)
|
||||
{
|
||||
<aside class="side-board">
|
||||
<div class="hand">
|
||||
@foreach (var piece in OpponentHand)
|
||||
{
|
||||
<div class="tile">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<div class="hand">
|
||||
@foreach (var piece in UserHand)
|
||||
{
|
||||
<div class="title" @onclick="() => OnClickHand(piece)">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
}
|
||||
</article>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? SessionName { get; set; }
|
||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||
WhichPlayer Perspective => Account.User?.Id == session?.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2;
|
||||
[Parameter]
|
||||
public string? SessionName { get; set; }
|
||||
|
||||
Session? session;
|
||||
string? selectedPosition;
|
||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||
WhichPlayer Perspective => Account.User?.Id == session?.Player1
|
||||
? WhichPlayer.Player1
|
||||
: WhichPlayer.Player2;
|
||||
Session? session;
|
||||
IReadOnlyCollection<Piece> OpponentHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.session == null) return Array.Empty<Piece>();
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SessionName))
|
||||
{
|
||||
this.session = await ShogiApi.GetSession(SessionName);
|
||||
}
|
||||
}
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.session.BoardState.Player1Hand
|
||||
: this.session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
IReadOnlyCollection<Piece> UserHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.session == null) return Array.Empty<Piece>();
|
||||
|
||||
void OnClickTile(Piece? piece, string position)
|
||||
{
|
||||
|
||||
}
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.session.BoardState.Player1Hand
|
||||
: this.session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
|
||||
string? selectedPosition;
|
||||
WhichPiece? selectedPiece;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SessionName))
|
||||
{
|
||||
this.session = await ShogiApi.GetSession(SessionName);
|
||||
}
|
||||
}
|
||||
|
||||
void OnClickTile(Piece? piece, string position)
|
||||
{
|
||||
if (selectedPosition == null)
|
||||
{
|
||||
selectedPosition = position;
|
||||
return;
|
||||
}
|
||||
else if (selectedPosition == position)
|
||||
{
|
||||
selectedPosition = null;
|
||||
return;
|
||||
}
|
||||
else if (piece != null)
|
||||
{
|
||||
ShogiApi.PostMove(SessionName!, new Contracts.Api.MovePieceCommand
|
||||
{
|
||||
From = selectedPosition,
|
||||
To = position,
|
||||
IsP
|
||||
});
|
||||
}
|
||||
}
|
||||
void OnClickHand(Piece piece)
|
||||
{
|
||||
selectedPiece = piece.WhichPiece;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
.game-board {
|
||||
display: grid;
|
||||
grid-template-areas: "board side-board";
|
||||
grid-template-columns: 3fr minmax(10rem, 1fr);
|
||||
gap: 0.5rem;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.board {
|
||||
grid-area: board;
|
||||
}
|
||||
|
||||
.side-board {
|
||||
grid-area: side-board;
|
||||
}
|
||||
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"rank A9 B9 C9 D9 E9 F9 G9 H9 I9"
|
||||
@@ -20,7 +35,7 @@
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] {
|
||||
.board[data-perspective="Player2"] {
|
||||
grid-template-areas:
|
||||
"file file file file file file file file file ."
|
||||
"I1 H1 G1 F1 E1 D1 C1 B1 A1 rank"
|
||||
@@ -41,6 +56,11 @@
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 0.25rem;
|
||||
overflow: hidden; /* Because SVGs are shaped weird */
|
||||
transition: filter linear 0.25s;
|
||||
}
|
||||
.tile[data-selected] {
|
||||
filter: invert(0.8);
|
||||
}
|
||||
|
||||
.ruler {
|
||||
@@ -54,10 +74,20 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] .ruler {
|
||||
.board[data-perspective="Player2"] .ruler {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] .ruler.vertical {
|
||||
.board[data-perspective="Player2"] .ruler.vertical {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.side-board {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.side-board .hand {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -3,107 +3,116 @@
|
||||
@using System.Net
|
||||
@inject IShogiApi ShogiApi;
|
||||
@inject ShogiSocket ShogiSocket;
|
||||
@inject AccountState Account;
|
||||
|
||||
<section class="game-browser">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#search-pane">Search</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#create-pane">Create</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#search-pane">Search</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#create-pane">Create</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="search-pane">
|
||||
<div class="list-group">
|
||||
@if (!sessions.Any())
|
||||
{
|
||||
<p>No games exist</p>
|
||||
}
|
||||
@foreach (var session in sessions)
|
||||
{
|
||||
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
|
||||
<span>@session.Name</span>
|
||||
<span>(@session.PlayerCount/2)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="create-pane">
|
||||
<EditForm Model="createForm" OnValidSubmit="async () => await CreateSession()">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="search-pane">
|
||||
<div class="list-group">
|
||||
@if (!sessions.Any())
|
||||
{
|
||||
<p>No games exist</p>
|
||||
}
|
||||
@foreach (var session in sessions)
|
||||
{
|
||||
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
|
||||
<span>@session.Name</span>
|
||||
<span>(@session.PlayerCount/2)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="create-pane">
|
||||
<EditForm Model="createForm" OnValidSubmit="async () => await CreateSession()">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<h3>Start a new session</h3>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="text" class="form-control" id="session-name" placeholder="Session name" @bind-Value="createForm.Name" />
|
||||
<label for="session-name">Session name</label>
|
||||
</div>
|
||||
<div class="flex-between mb-3">
|
||||
<div class="form-check">
|
||||
<InputCheckbox class="form-check-input" role="switch" id="session-privacy" @bind-Value="createForm.IsPrivate" />
|
||||
<label class="form-check-label" for="session-privacy">Private?</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
<h3>Start a new session</h3>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="text" class="form-control" id="session-name" placeholder="Session name" @bind-Value="createForm.Name" />
|
||||
<label for="session-name">Session name</label>
|
||||
</div>
|
||||
<div class="flex-between mb-3">
|
||||
<div class="form-check">
|
||||
<InputCheckbox class="form-check-input" role="switch" id="session-privacy" @bind-Value="createForm.IsPrivate" />
|
||||
<label class="form-check-label" for="session-privacy">Private?</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
|
||||
@if (createSessionStatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
<div class="alert alert-success" role="alert">
|
||||
Session started. View it in the search tab.
|
||||
</div>
|
||||
}
|
||||
else if (createSessionStatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
The name you chose is taken; choose another.
|
||||
</div>
|
||||
}
|
||||
@if (createSessionStatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
<div class="alert alert-success" role="alert">
|
||||
Session started. View it in the search tab.
|
||||
</div>
|
||||
}
|
||||
else if (createSessionStatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
The name you chose is taken; choose another.
|
||||
</div>
|
||||
}
|
||||
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Action<SessionMetadata>? ActiveSessionChanged { get; set; }
|
||||
CreateForm createForm = new();
|
||||
SessionMetadata[] sessions = Array.Empty<SessionMetadata>();
|
||||
SessionMetadata? activeSession;
|
||||
HttpStatusCode? createSessionStatusCode;
|
||||
[Parameter]
|
||||
public Action<SessionMetadata>? ActiveSessionChanged { get; set; }
|
||||
CreateForm createForm = new();
|
||||
SessionMetadata[] sessions = Array.Empty<SessionMetadata>();
|
||||
SessionMetadata? activeSession;
|
||||
HttpStatusCode? createSessionStatusCode;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
|
||||
await FetchSessions();
|
||||
}
|
||||
string ActiveCss(SessionMetadata s) => s == activeSession ? "active" : string.Empty;
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
|
||||
Account.LoginChangedEvent += async (sender, message) =>
|
||||
{
|
||||
if (message.User != null)
|
||||
{
|
||||
await FetchSessions();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void OnClickSession(SessionMetadata s)
|
||||
{
|
||||
activeSession = s;
|
||||
ActiveSessionChanged?.Invoke(s);
|
||||
}
|
||||
string ActiveCss(SessionMetadata s) => s == activeSession ? "active" : string.Empty;
|
||||
|
||||
async Task FetchSessions()
|
||||
{
|
||||
var sessions = await ShogiApi.GetSessionsPlayerCount();
|
||||
if (sessions != null)
|
||||
{
|
||||
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();
|
||||
}
|
||||
}
|
||||
void OnClickSession(SessionMetadata s)
|
||||
{
|
||||
activeSession = s;
|
||||
ActiveSessionChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
async Task CreateSession()
|
||||
{
|
||||
createSessionStatusCode = await ShogiApi.PostSession(createForm.Name, createForm.IsPrivate);
|
||||
}
|
||||
async Task FetchSessions()
|
||||
{
|
||||
var sessions = await ShogiApi.GetSessionsPlayerCount();
|
||||
if (sessions != null)
|
||||
{
|
||||
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateForm
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
async Task CreateSession()
|
||||
{
|
||||
createSessionStatusCode = await ShogiApi.PostSession(createForm.Name, createForm.IsPrivate);
|
||||
}
|
||||
|
||||
private class CreateForm
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ html, body, #app {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
color: var(--primary-color);
|
||||
font-family: cursive;
|
||||
}
|
||||
|
||||
span {
|
||||
|
||||
Reference in New Issue
Block a user