Migre le projet vers Blazor WebAssembly en .NET 10

This commit is contained in:
2026-04-13 21:29:12 +02:00
parent b11056097d
commit 90f17c9c89
26 changed files with 4314 additions and 94 deletions

View File

@@ -0,0 +1,604 @@
@page "/chrono"
@page "/chrono.html"
@implements IAsyncDisposable
@inject MatchStore Store
@inject NavigationManager Navigation
<PageTitle>ChessCubing Arena | Phase Chrono</PageTitle>
<PageBody Page="chrono" BodyClass="@ChronoBodyClass" />
@{
var summary = Match is null ? null : BuildSummary();
var blackZone = Match is null ? null : BuildZone(MatchEngine.ColorBlack);
var whiteZone = Match is null ? null : BuildZone(MatchEngine.ColorWhite);
}
@if (!_ready)
{
<main class="phase-shell chrono-stage">
<div class="panel" style="padding: 1.5rem;">Chargement de la phase chrono...</div>
</main>
}
else if (Match is not null && summary is not null && blackZone is not null && whiteZone is not null)
{
<main class="phase-shell chrono-stage">
<header class="phase-header">
<a class="brand-link brand-link-logo" href="application.html" aria-label="Retour a l'application">
<img class="brand-link-icon" src="logo.png" alt="Icone ChessCubing" />
<img class="brand-link-mark" src="transparent.png" alt="Logo ChessCubing" />
</a>
<div class="phase-title">
<p class="eyebrow">Phase chrono</p>
<h1 id="chronoTitle">@summary.Title</h1>
<p id="chronoSubtitle" class="phase-subtitle">@summary.Subtitle</p>
</div>
<button class="button ghost small utility-button" id="openArbiterButton" type="button" @onclick="OpenArbiterModal">
Arbitre
</button>
</header>
<section class="status-strip">
<article class="status-card">
<span id="blockTimerLabel">Temps Block</span>
<strong id="blockTimer">@summary.BlockTimer</strong>
</article>
<article class="status-card" id="moveTimerCard" hidden="@summary.HideMoveTimer">
<span>Temps coup</span>
<strong id="moveTimer">@summary.MoveTimer</strong>
</article>
<article class="status-card wide">
<span id="chronoCenterLabel">@summary.CenterLabel</span>
<strong id="chronoCenterValue">@summary.CenterValue</strong>
</article>
</section>
<section class="faceoff-board">
<article class="player-zone opponent-zone @(blackZone.ActiveZone ? "active-zone" : string.Empty) @(blackZone.HasPlayerClock ? "has-player-clock" : string.Empty)" id="blackZone">
<div class="zone-inner mirrored-mobile">
<div class="zone-head">
<div>
<span class="seat-tag dark-seat">Noir</span>
<h2 id="blackNameChrono">@blackZone.Name</h2>
</div>
<div class="zone-stats">
<strong id="blackMovesChrono">@blackZone.Moves</strong>
<span class="player-clock @(blackZone.NegativeClock ? "negative-clock" : string.Empty) @(blackZone.ActiveClock ? "active-clock" : string.Empty)" id="blackClockChrono">@blackZone.Clock</span>
</div>
</div>
<button class="zone-button dark-button @(blackZone.ActiveTurn ? "active-turn" : string.Empty)" id="blackMoveButton" type="button" disabled="@blackZone.Disabled" @onclick="() => HandleChronoTapAsync(MatchEngine.ColorBlack)">
@blackZone.ButtonText
</button>
<p class="zone-foot" id="blackHintChrono">@blackZone.Hint</p>
</div>
</article>
<article class="phase-spine">
<div class="spine-card">
<p class="micro-label" id="spineLabel">@summary.SpineLabel</p>
<strong id="spineHeadline">@summary.SpineHeadline</strong>
<p id="spineText">@summary.SpineText</p>
</div>
<button class="button primary spine-button" id="primaryChronoButton" type="button" @onclick="HandlePrimaryActionAsync">
@summary.PrimaryButtonText
</button>
</article>
<article class="player-zone @(whiteZone.ActiveZone ? "active-zone" : string.Empty) @(whiteZone.HasPlayerClock ? "has-player-clock" : string.Empty)" id="whiteZone">
<div class="zone-inner">
<div class="zone-head">
<div>
<span class="seat-tag light-seat">Blanc</span>
<h2 id="whiteNameChrono">@whiteZone.Name</h2>
</div>
<div class="zone-stats">
<strong id="whiteMovesChrono">@whiteZone.Moves</strong>
<span class="player-clock @(whiteZone.NegativeClock ? "negative-clock" : string.Empty) @(whiteZone.ActiveClock ? "active-clock" : string.Empty)" id="whiteClockChrono">@whiteZone.Clock</span>
</div>
</div>
<button class="zone-button light-button @(whiteZone.ActiveTurn ? "active-turn" : string.Empty)" id="whiteMoveButton" type="button" disabled="@whiteZone.Disabled" @onclick="() => HandleChronoTapAsync(MatchEngine.ColorWhite)">
@whiteZone.ButtonText
</button>
<p class="zone-foot" id="whiteHintChrono">@whiteZone.Hint</p>
</div>
</article>
</section>
</main>
<section class="modal @(ShowArbiterModal ? string.Empty : "hidden")" id="arbiterModal" aria-hidden="@BoolString(!ShowArbiterModal)">
<div class="modal-backdrop" @onclick="CloseArbiterModal"></div>
<div class="modal-card">
<div class="modal-head">
<div>
<p class="eyebrow">Outils arbitre</p>
<h2>Controles avances</h2>
</div>
<button class="button ghost small" id="closeArbiterButton" type="button" @onclick="CloseArbiterModal">Fermer</button>
</div>
<p class="section-copy" id="arbiterStatus">@summary.ArbiterStatus</p>
<div class="modal-actions">
<button class="button secondary" id="arbiterPauseButton" type="button" @onclick="TogglePauseAsync">
Pause / reprise
</button>
<button class="button secondary" id="arbiterCloseBlockButton" type="button" @onclick="CloseBlockAsync">
Passer au cube
</button>
<button class="button secondary" id="arbiterTimeoutButton" type="button" hidden="@summary.HideMoveTimer" disabled="@summary.HideMoveTimer" @onclick="TimeoutMoveAsync">
@summary.TimeoutButtonText
</button>
<button class="button secondary" id="arbiterSwitchTurnButton" type="button" @onclick="SwitchTurnAsync">
Corriger le trait
</button>
<button class="button ghost" id="arbiterWhiteWinButton" type="button" @onclick="() => SetResultAsync(MatchEngine.ColorWhite)">
Blanc gagne
</button>
<button class="button ghost" id="arbiterBlackWinButton" type="button" @onclick="() => SetResultAsync(MatchEngine.ColorBlack)">
Noir gagne
</button>
<button class="button ghost danger" id="arbiterStopButton" type="button" @onclick='() => SetResultAsync("stopped")'>
Abandon / arret
</button>
<button class="button ghost" id="arbiterResetButton" type="button" @onclick="ResetMatchAsync">
Reinitialiser le match
</button>
</div>
</div>
</section>
}
@code {
private CancellationTokenSource? _tickerCancellation;
private bool _ready;
private bool ShowArbiterModal;
private MatchState? Match => Store.Current;
private string ChronoBodyClass =>
Match is not null && MatchEngine.IsTimeMode(Match)
? "phase-body time-mode"
: "phase-body";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
await Store.EnsureLoadedAsync();
_ready = true;
if (Match is null)
{
Navigation.NavigateTo("/application.html", replace: true);
return;
}
if (string.IsNullOrEmpty(Match.Result) && Match.Phase == MatchEngine.PhaseCube)
{
await Store.SaveAsync();
Navigation.NavigateTo("/cube.html", replace: true);
return;
}
_tickerCancellation = new CancellationTokenSource();
_ = RunTickerAsync(_tickerCancellation.Token);
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
if (_tickerCancellation is not null)
{
_tickerCancellation.Cancel();
_tickerCancellation.Dispose();
}
await Store.FlushIfDueAsync(0);
}
private async Task RunTickerAsync(CancellationToken cancellationToken)
{
try
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
var match = Match;
if (match is null)
{
continue;
}
if (MatchEngine.SyncRunningState(match))
{
Store.MarkDirty();
}
await Store.FlushIfDueAsync();
if (string.IsNullOrEmpty(match.Result) && match.Phase == MatchEngine.PhaseCube)
{
await Store.SaveAsync();
await InvokeAsync(() => Navigation.NavigateTo("/cube.html", replace: true));
return;
}
await InvokeAsync(StateHasChanged);
}
}
catch (OperationCanceledException)
{
}
}
private void OpenArbiterModal()
=> ShowArbiterModal = true;
private void CloseArbiterModal()
=> ShowArbiterModal = false;
private async Task HandleChronoTapAsync(string color)
{
var match = Match;
if (match is null || !string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseBlock)
{
return;
}
MatchEngine.SyncRunningState(match);
if (!match.Running || match.CurrentTurn != color)
{
return;
}
if (match.DoubleCoup.Step == 1)
{
MatchEngine.RegisterFreeDoubleMove(match);
}
else
{
MatchEngine.RegisterCountedMove(match, match.DoubleCoup.Step == 2 ? "double" : "standard");
}
await PersistAndRouteAsync();
}
private async Task HandlePrimaryActionAsync()
{
var match = Match;
if (match is null)
{
return;
}
MatchEngine.SyncRunningState(match);
if (!string.IsNullOrEmpty(match.Result))
{
Navigation.NavigateTo("/application.html");
return;
}
if (match.Phase != MatchEngine.PhaseBlock)
{
await Store.SaveAsync();
Navigation.NavigateTo("/cube.html", replace: true);
return;
}
if (match.Running)
{
MatchEngine.PauseBlock(match);
}
else
{
MatchEngine.StartBlock(match);
}
await PersistAndRouteAsync();
}
private async Task TogglePauseAsync()
{
var match = Match;
if (match is null || !string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseBlock)
{
return;
}
MatchEngine.SyncRunningState(match);
if (match.Running)
{
MatchEngine.PauseBlock(match);
}
else
{
MatchEngine.StartBlock(match);
}
await PersistAndRouteAsync();
}
private async Task CloseBlockAsync()
{
var match = Match;
if (match is null || !string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseBlock)
{
return;
}
MatchEngine.SyncRunningState(match);
MatchEngine.RequestBlockClosure(match, $"Cloture manuelle {MatchEngine.GetBlockGenitivePhrase(match)} demandee par l'arbitre.");
await PersistAndRouteAsync();
}
private async Task TimeoutMoveAsync()
{
var match = Match;
if (match is null || !string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseBlock || !MatchEngine.UsesMoveLimit(match))
{
return;
}
MatchEngine.SyncRunningState(match);
MatchEngine.RegisterMoveTimeout(match, false);
await PersistAndRouteAsync();
}
private async Task SwitchTurnAsync()
{
var match = Match;
if (match is null || !string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseBlock)
{
return;
}
MatchEngine.SyncRunningState(match);
match.CurrentTurn = MatchEngine.OpponentOf(match.CurrentTurn);
if (MatchEngine.UsesMoveLimit(match))
{
match.MoveRemainingMs = MatchEngine.GetMoveLimitMs(match);
}
MatchEngine.LogEvent(match, "Trait corrige manuellement par l'arbitre.");
await PersistAndRouteAsync();
}
private async Task SetResultAsync(string result)
{
var match = Match;
if (match is null)
{
return;
}
MatchEngine.SyncRunningState(match);
MatchEngine.SetResult(match, result);
await PersistAndRouteAsync();
}
private async Task ResetMatchAsync()
{
await Store.ClearAsync();
Navigation.NavigateTo("/application.html", replace: true);
}
private async Task PersistAndRouteAsync()
{
Store.MarkDirty();
await Store.SaveAsync();
if (Match is not null && string.IsNullOrEmpty(Match.Result) && Match.Phase == MatchEngine.PhaseCube)
{
Navigation.NavigateTo("/cube.html", replace: true);
return;
}
StateHasChanged();
}
private ChronoSummaryView BuildSummary()
{
var match = Match!;
var blockHeading = MatchEngine.FormatBlockHeading(match, match.BlockNumber);
var subtitle = $"{blockHeading} - {MatchEngine.Modes[match.Config.Mode].Label} - {MatchEngine.RenderModeContext(match)}";
var hideMoveTimer = MatchEngine.IsTimeMode(match);
var timeoutButtonText = $"Depassement {MatchEngine.FormatClock(MatchEngine.GetMoveLimitMs(match))}";
if (!string.IsNullOrEmpty(match.Result))
{
return new ChronoSummaryView(
match.Config.MatchLabel,
subtitle,
MatchEngine.FormatClock(match.BlockRemainingMs),
hideMoveTimer ? "--:--" : MatchEngine.FormatClock(match.MoveRemainingMs),
"Resultat",
MatchEngine.ResultText(match),
"Termine",
MatchEngine.ResultText(match),
"Retournez a la configuration pour lancer une nouvelle rencontre.",
"Retour a l'accueil",
"Le match est termine. Vous pouvez revenir a l'accueil ou reinitialiser.",
hideMoveTimer,
timeoutButtonText);
}
if (match.Running)
{
return new ChronoSummaryView(
match.Config.MatchLabel,
subtitle,
MatchEngine.FormatClock(match.BlockRemainingMs),
hideMoveTimer ? "--:--" : MatchEngine.FormatClock(match.MoveRemainingMs),
"Trait",
MatchEngine.PlayerName(match, match.CurrentTurn),
"Chrono en cours",
$"{blockHeading} actif",
"Chaque joueur tape sur sa grande zone quand son coup est termine. La page cube s'ouvrira automatiquement a la fin de la phase chess.",
"Pause arbitre",
$"{blockHeading} en cours. Joueur au trait : {MatchEngine.PlayerName(match, match.CurrentTurn)}.",
hideMoveTimer,
timeoutButtonText);
}
return new ChronoSummaryView(
match.Config.MatchLabel,
subtitle,
MatchEngine.FormatClock(match.BlockRemainingMs),
hideMoveTimer ? "--:--" : MatchEngine.FormatClock(match.MoveRemainingMs),
"Trait",
MatchEngine.PlayerName(match, match.CurrentTurn),
MatchEngine.IsTimeMode(match) ? "Etat du Block" : "Pret",
blockHeading,
"Demarrez le Block, puis laissez uniquement les deux grandes zones aux joueurs. La page cube prendra automatiquement le relais.",
"Demarrer le Block",
$"{blockHeading} pret. {MatchEngine.PlayerName(match, match.CurrentTurn)} commencera.",
hideMoveTimer,
timeoutButtonText);
}
private ChronoZoneView BuildZone(string color)
{
var match = Match!;
var active = match.CurrentTurn == color;
var clockValue = match.Clocks is not null
? color == MatchEngine.ColorWhite ? match.Clocks.White : match.Clocks.Black
: 0;
var clockText = match.Clocks is not null
? MatchEngine.FormatSignedClock(clockValue)
: $"Dernier cube {MatchEngine.RenderLastCube(match, color)}";
if (!string.IsNullOrEmpty(match.Result))
{
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
MatchEngine.ResultText(match),
"Le match est termine.",
true,
false,
false,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
false);
}
if (!match.Running)
{
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
"Block en pause",
active
? $"{MatchEngine.GetBlockPhrase(match)} n'a pas encore demarre ou a ete mis en pause."
: $"{MatchEngine.PlayerName(match, match.CurrentTurn)} reprendra au demarrage.",
true,
false,
active,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
false);
}
if (!active)
{
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
"Attends",
$"{MatchEngine.PlayerName(match, match.CurrentTurn)} est en train de jouer.",
true,
false,
false,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
false);
}
if (match.DoubleCoup.Step == 1)
{
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
"1er coup gratuit",
"Ce coup ne compte pas et ne doit pas donner echec.",
false,
true,
true,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
match.Clocks is not null);
}
if (match.DoubleCoup.Step == 2)
{
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
"2e coup du double",
"Ce coup compte dans le quota et l'echec redevient autorise.",
false,
true,
true,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
match.Clocks is not null);
}
return new ChronoZoneView(
MatchEngine.PlayerName(match, color),
$"{(color == MatchEngine.ColorWhite ? match.Moves.White : match.Moves.Black)} / {match.Quota}",
clockText,
"J'ai fini mon coup",
"Tape des que ton coup est joue sur l'echiquier.",
false,
true,
true,
match.Clocks is not null,
match.Clocks is not null && clockValue < 0,
match.Clocks is not null);
}
private static string BoolString(bool value)
=> value ? "true" : "false";
private sealed record ChronoSummaryView(
string Title,
string Subtitle,
string BlockTimer,
string MoveTimer,
string CenterLabel,
string CenterValue,
string SpineLabel,
string SpineHeadline,
string SpineText,
string PrimaryButtonText,
string ArbiterStatus,
bool HideMoveTimer,
string TimeoutButtonText);
private sealed record ChronoZoneView(
string Name,
string Moves,
string Clock,
string ButtonText,
string Hint,
bool Disabled,
bool ActiveTurn,
bool ActiveZone,
bool HasPlayerClock,
bool NegativeClock,
bool ActiveClock);
}