Migre le projet vers Blazor WebAssembly en .NET 10
This commit is contained in:
352
ChessCubing.App/Pages/ApplicationPage.razor
Normal file
352
ChessCubing.App/Pages/ApplicationPage.razor
Normal file
@@ -0,0 +1,352 @@
|
||||
@page "/application"
|
||||
@page "/application.html"
|
||||
@inject BrowserBridge Browser
|
||||
@inject MatchStore Store
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>ChessCubing Arena | Application</PageTitle>
|
||||
<PageBody Page="setup" BodyClass="@SetupBodyClass" />
|
||||
|
||||
<div class="ambient ambient-left"></div>
|
||||
<div class="ambient ambient-right"></div>
|
||||
|
||||
<div class="setup-shell">
|
||||
<header class="hero hero-setup">
|
||||
<div class="hero-copy">
|
||||
<a class="logo-lockup" href="index.html" aria-label="Accueil ChessCubing">
|
||||
<img class="hero-logo-icon" src="logo.png" alt="Icone ChessCubing" />
|
||||
<img class="hero-logo" src="transparent.png" alt="Logo ChessCubing" />
|
||||
</a>
|
||||
<p class="eyebrow">Application officielle de match</p>
|
||||
<h1>ChessCubing Arena</h1>
|
||||
<div class="hero-actions">
|
||||
<a class="button ghost" href="index.html">Accueil du site</a>
|
||||
<a class="button secondary" href="reglement.html">Consulter le reglement</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Flux de match</p>
|
||||
<ol class="phase-list">
|
||||
<li>Configurer la rencontre</li>
|
||||
<li>Passer a la page chrono</li>
|
||||
<li>Basculer automatiquement sur la page cube</li>
|
||||
<li>Revenir sur la page chrono pour le Block suivant</li>
|
||||
</ol>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main class="setup-grid">
|
||||
<section class="panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Nouvelle rencontre</p>
|
||||
<h2>Configuration</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les reglages ci-dessous preparent les pages chrono et cube.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="setup-form" @onsubmit="HandleSubmit">
|
||||
<label class="option-card competition-option span-2 @(Form.CompetitionMode ? "is-selected" : string.Empty)">
|
||||
<input @bind="Form.CompetitionMode" id="competitionMode" name="competitionMode" type="checkbox" />
|
||||
<strong>Mode competition</strong>
|
||||
<span>
|
||||
Affiche le nom de la rencontre, l'arbitre, le club ou l'evenement
|
||||
et les notes d'organisation.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@if (Form.CompetitionMode)
|
||||
{
|
||||
<label class="field span-2" id="matchLabelField">
|
||||
<span>Nom de la rencontre</span>
|
||||
<input @bind="Form.MatchLabel" @bind:event="oninput" name="matchLabel" type="text" maxlength="80" placeholder="Open ChessCubing de Paris" />
|
||||
</label>
|
||||
}
|
||||
|
||||
<fieldset class="field span-2">
|
||||
<legend>Mode officiel</legend>
|
||||
<div class="option-grid mode-grid">
|
||||
<label class="option-card @(Form.Mode == MatchEngine.ModeTwice ? "is-selected" : string.Empty)">
|
||||
<input type="radio" name="mode" checked="@(Form.Mode == MatchEngine.ModeTwice)" @onchange="() => SetMode(MatchEngine.ModeTwice)" />
|
||||
<strong>ChessCubing Twice</strong>
|
||||
<span>
|
||||
Le gagnant du cube ouvre le Block suivant et peut obtenir un
|
||||
double coup V2.
|
||||
</span>
|
||||
</label>
|
||||
<label class="option-card @(Form.Mode == MatchEngine.ModeTime ? "is-selected" : string.Empty)">
|
||||
<input type="radio" name="mode" checked="@(Form.Mode == MatchEngine.ModeTime)" @onchange="() => SetMode(MatchEngine.ModeTime)" />
|
||||
<strong>ChessCubing Time</strong>
|
||||
<span>
|
||||
Meme structure de Blocks, avec chronos cumules et alternance
|
||||
bloc - / bloc +.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="field span-2">
|
||||
<legend>Cadence du match</legend>
|
||||
<div class="option-grid preset-grid">
|
||||
<label class="option-card @(Form.Preset == MatchEngine.PresetFast ? "is-selected" : string.Empty)">
|
||||
<input type="radio" name="preset" checked="@(Form.Preset == MatchEngine.PresetFast)" @onchange="() => SetPreset(MatchEngine.PresetFast)" />
|
||||
<strong>FAST</strong>
|
||||
<span>6 coups par joueur</span>
|
||||
</label>
|
||||
<label class="option-card @(Form.Preset == MatchEngine.PresetFreeze ? "is-selected" : string.Empty)">
|
||||
<input type="radio" name="preset" checked="@(Form.Preset == MatchEngine.PresetFreeze)" @onchange="() => SetPreset(MatchEngine.PresetFreeze)" />
|
||||
<strong>FREEZE</strong>
|
||||
<span>8 coups par joueur</span>
|
||||
</label>
|
||||
<label class="option-card @(Form.Preset == MatchEngine.PresetMasters ? "is-selected" : string.Empty)">
|
||||
<input type="radio" name="preset" checked="@(Form.Preset == MatchEngine.PresetMasters)" @onchange="() => SetPreset(MatchEngine.PresetMasters)" />
|
||||
<strong>MASTERS</strong>
|
||||
<span>10 coups par joueur</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="field span-2">
|
||||
<legend>Temps personnalises</legend>
|
||||
<div class="timing-grid">
|
||||
<label class="field">
|
||||
<span>Temps du Block (minutes)</span>
|
||||
<input @bind="Form.BlockMinutes" name="blockMinutes" type="number" min="1" max="180" step="1" />
|
||||
</label>
|
||||
|
||||
@if (!UsesMoveLimit)
|
||||
{
|
||||
<label class="field" id="timeInitialField">
|
||||
<span>Temps de chaque joueur (minutes)</span>
|
||||
<input @bind="Form.TimeInitialMinutes" name="timeInitialMinutes" type="number" min="1" max="180" step="1" />
|
||||
</label>
|
||||
}
|
||||
|
||||
@if (UsesMoveLimit)
|
||||
{
|
||||
<label class="field" id="moveSecondsField">
|
||||
<span>Temps coup (secondes)</span>
|
||||
<input @bind="Form.MoveSeconds" name="moveSeconds" type="number" min="5" max="300" step="1" />
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label class="field">
|
||||
<span>Joueur blanc</span>
|
||||
<input @bind="Form.WhiteName" @bind:event="oninput" name="whiteName" type="text" maxlength="40" placeholder="Blanc" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Joueur noir</span>
|
||||
<input @bind="Form.BlackName" @bind:event="oninput" name="blackName" type="text" maxlength="40" placeholder="Noir" />
|
||||
</label>
|
||||
|
||||
@if (Form.CompetitionMode)
|
||||
{
|
||||
<label class="field" id="arbiterField">
|
||||
<span>Arbitre</span>
|
||||
<input @bind="Form.ArbiterName" @bind:event="oninput" name="arbiterName" type="text" maxlength="40" placeholder="Arbitre principal" />
|
||||
</label>
|
||||
|
||||
<label class="field" id="eventField">
|
||||
<span>Club / evenement</span>
|
||||
<input @bind="Form.EventName" @bind:event="oninput" name="eventName" type="text" maxlength="60" placeholder="Club, tournoi, demonstration" />
|
||||
</label>
|
||||
|
||||
<label class="field span-2" id="notesField">
|
||||
<span>Notes</span>
|
||||
<textarea @bind="Form.Notes" @bind:event="oninput" name="notes" rows="3" placeholder="Tirage au sort effectue, 8 cubes verifies, variante prete..."></textarea>
|
||||
</label>
|
||||
}
|
||||
|
||||
<div id="setupSummary" class="setup-summary span-2">
|
||||
<strong>@CurrentMode.Label</strong>
|
||||
<span>@CurrentPreset.Description</span>
|
||||
<span>@TimingText</span>
|
||||
<span>@TimeImpact</span>
|
||||
<span>@QuotaText</span>
|
||||
</div>
|
||||
|
||||
<div class="setup-actions span-2">
|
||||
<button class="button primary" type="submit">Ouvrir la page chrono</button>
|
||||
<button class="button secondary" id="loadDemoButton" type="button" @onclick="LoadDemo">Charger une demo</button>
|
||||
<a class="button ghost" href="reglement.html">Consulter le reglement</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="panel side-panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Match en memoire</p>
|
||||
<h2>Reprise rapide</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_ready || CurrentMatch is null)
|
||||
{
|
||||
<div id="resumeCard" class="resume-card empty">
|
||||
<p>Aucun match en cours pour l'instant.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="resumeCard" class="resume-card">
|
||||
<strong>@ResumeMatchLabel(CurrentMatch)</strong>
|
||||
<p>@CurrentMatchMode(CurrentMatch)</p>
|
||||
<p>@($"{CurrentMatch.Config.WhiteName} vs {CurrentMatch.Config.BlackName}")</p>
|
||||
<p>@ResumePhaseLabel(CurrentMatch)</p>
|
||||
<div class="resume-actions">
|
||||
<button class="button primary" id="resumeMatchButton" type="button" @onclick="ResumeMatch">
|
||||
@(string.IsNullOrEmpty(CurrentMatch.Result) ? "Reprendre la phase" : "Voir le match")
|
||||
</button>
|
||||
<button class="button ghost" id="clearMatchButton" type="button" @onclick="ClearMatchAsync">Effacer le match</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="rules-stack">
|
||||
<article class="rule-card">
|
||||
<span class="micro-label">Page chrono</span>
|
||||
<strong>Gros boutons uniquement</strong>
|
||||
<p>
|
||||
Chaque joueur dispose d'une grande zone tactile pour signaler la
|
||||
fin de son coup, puis l'app ouvre automatiquement la phase cube
|
||||
quand le Block d'echecs est termine.
|
||||
</p>
|
||||
</article>
|
||||
<article class="rule-card">
|
||||
<span class="micro-label">Page cube</span>
|
||||
<strong>Une page dediee</strong>
|
||||
<p>
|
||||
Les deux joueurs lancent et arretent leur propre chrono cube sur
|
||||
un ecran separe, toujours en face-a-face sur mobile.
|
||||
</p>
|
||||
</article>
|
||||
<article class="rule-card">
|
||||
<span class="micro-label">Sources</span>
|
||||
<strong>Reglements integres</strong>
|
||||
<p>
|
||||
<a href="reglement.html">Page reglement du site</a>
|
||||
<br />
|
||||
<a href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank">Reglement ChessCubing Twice</a>
|
||||
<br />
|
||||
<a href="ChessCubing_Time_Reglement_Officiel_V1-1.pdf" target="_blank">Reglement ChessCubing Time</a>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<div class="setup-refresh-footer">
|
||||
<button class="refresh-link-button" id="refreshAppButton" type="button" @onclick="ForceRefreshAsync">
|
||||
Rafraichir l'app
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private SetupFormModel Form { get; set; } = new();
|
||||
private bool _ready;
|
||||
|
||||
private MatchState? CurrentMatch => Store.Current;
|
||||
|
||||
private string SetupBodyClass => UsesMoveLimit ? string.Empty : "time-setup-mode";
|
||||
|
||||
private bool UsesMoveLimit => MatchEngine.UsesMoveLimit(Form.Mode);
|
||||
|
||||
private MatchPresetInfo CurrentPreset =>
|
||||
MatchEngine.Presets.TryGetValue(Form.Preset, out var preset)
|
||||
? preset
|
||||
: MatchEngine.Presets[MatchEngine.PresetFast];
|
||||
|
||||
private MatchModeInfo CurrentMode =>
|
||||
MatchEngine.Modes.TryGetValue(Form.Mode, out var mode)
|
||||
? mode
|
||||
: MatchEngine.Modes[MatchEngine.ModeTwice];
|
||||
|
||||
private string TimingText =>
|
||||
UsesMoveLimit
|
||||
? $"Temps configures : Block {MatchEngine.FormatClock(Form.BlockMinutes * 60_000L)}, coup {MatchEngine.FormatClock(Form.MoveSeconds * 1_000L)}."
|
||||
: $"Temps configures : Block {MatchEngine.FormatClock(Form.BlockMinutes * 60_000L)}, temps de chaque joueur {MatchEngine.FormatClock(Form.TimeInitialMinutes * 60_000L)}.";
|
||||
|
||||
private string TimeImpact =>
|
||||
Form.Mode == MatchEngine.ModeTime
|
||||
? $"Chronos cumules de {MatchEngine.FormatClock(Form.TimeInitialMinutes * 60_000L)} par joueur, ajustes apres chaque phase cube avec plafond de 120 s pris en compte. Aucun temps par coup en mode Time."
|
||||
: "Le gagnant du cube commence le Block suivant, avec double coup V2 possible.";
|
||||
|
||||
private string QuotaText =>
|
||||
UsesMoveLimit
|
||||
? $"Quota actif : {CurrentPreset.Quota} coups par joueur."
|
||||
: $"Quota actif : {CurrentPreset.Quota} coups par joueur et par Block.";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Store.EnsureLoadedAsync();
|
||||
_ready = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
var match = MatchEngine.CreateMatch(Form.ToMatchConfig());
|
||||
Store.SetCurrent(match);
|
||||
await Store.SaveAsync();
|
||||
Navigation.NavigateTo("/chrono.html");
|
||||
}
|
||||
|
||||
private void LoadDemo()
|
||||
=> Form = SetupFormModel.CreateDemo();
|
||||
|
||||
private void SetMode(string mode)
|
||||
=> Form.Mode = mode;
|
||||
|
||||
private void SetPreset(string preset)
|
||||
=> Form.Preset = preset;
|
||||
|
||||
private void ResumeMatch()
|
||||
{
|
||||
if (CurrentMatch is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.NavigateTo(MatchEngine.RouteForMatch(CurrentMatch));
|
||||
}
|
||||
|
||||
private async Task ClearMatchAsync()
|
||||
{
|
||||
await Store.ClearAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private Task ForceRefreshAsync()
|
||||
=> Browser.ForceRefreshAsync("/application.html").AsTask();
|
||||
|
||||
private static string ResumeMatchLabel(MatchState match)
|
||||
=> string.IsNullOrWhiteSpace(match.Config.MatchLabel) ? "Rencontre ChessCubing" : match.Config.MatchLabel;
|
||||
|
||||
private static string CurrentMatchMode(MatchState match)
|
||||
=> MatchEngine.Modes.TryGetValue(match.Config.Mode, out var mode) ? mode.Label : "ChessCubing Twice";
|
||||
|
||||
private static string ResumePhaseLabel(MatchState match)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(match.Result))
|
||||
{
|
||||
return MatchEngine.ResultText(match);
|
||||
}
|
||||
|
||||
return match.Phase == MatchEngine.PhaseCube ? "Page cube prete" : "Page chrono prete";
|
||||
}
|
||||
}
|
||||
604
ChessCubing.App/Pages/ChronoPage.razor
Normal file
604
ChessCubing.App/Pages/ChronoPage.razor
Normal 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);
|
||||
}
|
||||
821
ChessCubing.App/Pages/CubePage.razor
Normal file
821
ChessCubing.App/Pages/CubePage.razor
Normal file
@@ -0,0 +1,821 @@
|
||||
@page "/cube"
|
||||
@page "/cube.html"
|
||||
@implements IAsyncDisposable
|
||||
@inject BrowserBridge Browser
|
||||
@inject MatchStore Store
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>ChessCubing Arena | Phase Cube</PageTitle>
|
||||
<PageBody Page="cube" BodyClass="phase-body" />
|
||||
|
||||
@{
|
||||
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);
|
||||
var resultView = Match is null ? null : BuildResultView();
|
||||
}
|
||||
|
||||
@if (!_ready)
|
||||
{
|
||||
<main class="phase-shell cube-shell">
|
||||
<div class="panel" style="padding: 1.5rem;">Chargement de la phase cube...</div>
|
||||
</main>
|
||||
}
|
||||
else if (Match is not null && summary is not null && blackZone is not null && whiteZone is not null)
|
||||
{
|
||||
<main class="phase-shell cube-shell">
|
||||
<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 cube</p>
|
||||
<h1 id="cubeTitle">@summary.Title</h1>
|
||||
<p id="cubeSubtitle" class="phase-subtitle">@summary.Subtitle</p>
|
||||
</div>
|
||||
<button class="button ghost small utility-button" id="openCubeHelpButton" type="button" @onclick="OpenHelpModal">
|
||||
Arbitre
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="status-strip">
|
||||
<article class="status-card">
|
||||
<span id="cubeBlockLabelText">Block</span>
|
||||
<strong id="cubeBlockLabel">@Match.BlockNumber</strong>
|
||||
</article>
|
||||
<article class="status-card">
|
||||
<span>Temps max</span>
|
||||
<strong id="cubeElapsed">@MatchEngine.RenderCubeElapsed(Match)</strong>
|
||||
</article>
|
||||
<article class="status-card wide">
|
||||
<span id="cubeCenterLabel">@summary.CenterLabel</span>
|
||||
<strong id="cubeCenterValue">@summary.CenterValue</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="faceoff-board">
|
||||
<article class="player-zone opponent-zone" id="blackCubeZone">
|
||||
<div class="zone-inner mirrored-mobile">
|
||||
<div class="zone-head">
|
||||
<div>
|
||||
<span class="seat-tag dark-seat">Noir</span>
|
||||
<h2 id="blackNameCube">@blackZone.Name</h2>
|
||||
</div>
|
||||
<div class="zone-stats">
|
||||
<strong id="blackCubeResult">@blackZone.ResultText</strong>
|
||||
<span id="blackCubeCap">@blackZone.MetaText</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="zone-button dark-button @(blackZone.HoldArmed && !blackZone.HoldReady ? "cube-hold-arming" : string.Empty) @(blackZone.HoldReady ? "cube-hold-ready" : string.Empty)" id="blackCubeButton" type="button" disabled="@blackZone.Disabled" style="@blackZone.ProgressStyle" @onpointerdown="() => HandleCubePointerDownAsync(MatchEngine.ColorBlack)" @onpointerup="() => HandleCubePointerUpAsync(MatchEngine.ColorBlack)" @onpointercancel="() => CancelCubeHold(MatchEngine.ColorBlack)" @onpointerleave="() => CancelCubeHold(MatchEngine.ColorBlack)">
|
||||
@blackZone.ButtonText
|
||||
</button>
|
||||
|
||||
<p class="zone-foot" id="blackHintCube">@blackZone.Hint</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="phase-spine">
|
||||
<div class="spine-card">
|
||||
<p class="micro-label" id="cubeSpineLabel">@summary.SpineLabel</p>
|
||||
<strong id="cubeSpineHeadline">@summary.SpineHeadline</strong>
|
||||
<p id="cubeSpineText">@summary.SpineText</p>
|
||||
</div>
|
||||
|
||||
<button class="button primary spine-button" id="primaryCubeButton" type="button" disabled="@summary.PrimaryDisabled" @onclick="HandlePrimaryActionAsync">
|
||||
@summary.PrimaryButtonText
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="player-zone" id="whiteCubeZone">
|
||||
<div class="zone-inner">
|
||||
<div class="zone-head">
|
||||
<div>
|
||||
<span class="seat-tag light-seat">Blanc</span>
|
||||
<h2 id="whiteNameCube">@whiteZone.Name</h2>
|
||||
</div>
|
||||
<div class="zone-stats">
|
||||
<strong id="whiteCubeResult">@whiteZone.ResultText</strong>
|
||||
<span id="whiteCubeCap">@whiteZone.MetaText</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="zone-button light-button @(whiteZone.HoldArmed && !whiteZone.HoldReady ? "cube-hold-arming" : string.Empty) @(whiteZone.HoldReady ? "cube-hold-ready" : string.Empty)" id="whiteCubeButton" type="button" disabled="@whiteZone.Disabled" style="@whiteZone.ProgressStyle" @onpointerdown="() => HandleCubePointerDownAsync(MatchEngine.ColorWhite)" @onpointerup="() => HandleCubePointerUpAsync(MatchEngine.ColorWhite)" @onpointercancel="() => CancelCubeHold(MatchEngine.ColorWhite)" @onpointerleave="() => CancelCubeHold(MatchEngine.ColorWhite)">
|
||||
@whiteZone.ButtonText
|
||||
</button>
|
||||
|
||||
<p class="zone-foot" id="whiteHintCube">@whiteZone.Hint</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="modal @(ShowHelpModal ? string.Empty : "hidden")" id="cubeHelpModal" aria-hidden="@BoolString(!ShowHelpModal)">
|
||||
<div class="modal-backdrop" @onclick="CloseHelpModal"></div>
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<p class="eyebrow">Outils arbitre</p>
|
||||
<h2>Phase cube</h2>
|
||||
</div>
|
||||
<button class="button ghost small" id="closeCubeHelpButton" type="button" @onclick="CloseHelpModal">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="section-copy" id="cubeHelpStatus">@summary.HelpStatus</p>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="button secondary" id="replayCubeButton" type="button" @onclick="ReplayCubeAsync">
|
||||
Rejouer la phase cube
|
||||
</button>
|
||||
<button class="button ghost danger" id="cubeResetButton" type="button" @onclick="ResetMatchAsync">
|
||||
Reinitialiser le match
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="modal @(ShowResultModal && resultView is not null ? string.Empty : "hidden")" id="cubeResultModal" aria-hidden="@BoolString(!(ShowResultModal && resultView is not null))">
|
||||
<div class="modal-backdrop" @onclick="CloseResultModal"></div>
|
||||
@if (resultView is not null)
|
||||
{
|
||||
<div class="modal-card result-modal-card">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<p class="eyebrow">Fin de phase cube</p>
|
||||
<h2 id="cubeResultModalTitle">@resultView.Title</h2>
|
||||
</div>
|
||||
<button class="button ghost small" id="closeCubeResultButton" type="button" @onclick="CloseResultModal">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="section-copy" id="cubeResultSummary">@resultView.Summary</p>
|
||||
|
||||
<div class="cube-result-overview">
|
||||
<article class="result-pill-card">
|
||||
<span>Vainqueur cube</span>
|
||||
<strong id="cubeResultWinner">@resultView.Winner</strong>
|
||||
</article>
|
||||
<article class="result-pill-card">
|
||||
<span>Suite</span>
|
||||
<strong id="cubeResultOutcome">@resultView.Outcome</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="cube-result-player-grid">
|
||||
<article class="cube-result-player-card">
|
||||
<span class="seat-tag light-seat">Blanc</span>
|
||||
<strong id="cubeResultWhiteName">@resultView.WhiteName</strong>
|
||||
<span id="cubeResultWhiteTime">@resultView.WhiteTime</span>
|
||||
<span id="cubeResultWhiteDetail">@resultView.WhiteDetail</span>
|
||||
<span id="cubeResultWhiteClock">@resultView.WhiteClock</span>
|
||||
</article>
|
||||
<article class="cube-result-player-card">
|
||||
<span class="seat-tag dark-seat">Noir</span>
|
||||
<strong id="cubeResultBlackName">@resultView.BlackName</strong>
|
||||
<span id="cubeResultBlackTime">@resultView.BlackTime</span>
|
||||
<span id="cubeResultBlackDetail">@resultView.BlackDetail</span>
|
||||
<span id="cubeResultBlackClock">@resultView.BlackClock</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="button primary" id="cubeResultActionButton" type="button" @onclick="ApplyResultAsync">
|
||||
@resultView.ActionLabel
|
||||
</button>
|
||||
<button class="button ghost" id="cubeResultDismissButton" type="button" @onclick="CloseResultModal">
|
||||
Revenir a la phase cube
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly Dictionary<string, CubeHoldState> _holdStates = new(StringComparer.Ordinal)
|
||||
{
|
||||
[MatchEngine.ColorWhite] = new(),
|
||||
[MatchEngine.ColorBlack] = new(),
|
||||
};
|
||||
|
||||
private CancellationTokenSource? _tickerCancellation;
|
||||
private bool _ready;
|
||||
private bool ShowHelpModal;
|
||||
private bool ShowResultModal;
|
||||
private string? _resultModalKey;
|
||||
|
||||
private MatchState? Match => Store.Current;
|
||||
|
||||
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)
|
||||
{
|
||||
Navigation.NavigateTo("/chrono.html", replace: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_tickerCancellation = new CancellationTokenSource();
|
||||
_ = RunTickerAsync(_tickerCancellation.Token);
|
||||
await TryPlayPendingAlertAsync();
|
||||
UpdateResultModalState();
|
||||
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 (string.IsNullOrEmpty(match.Result) && match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
await Store.SaveAsync();
|
||||
await InvokeAsync(() => Navigation.NavigateTo("/chrono.html", replace: true));
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateResultModalState();
|
||||
await Store.FlushIfDueAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenHelpModal()
|
||||
=> ShowHelpModal = true;
|
||||
|
||||
private void CloseHelpModal()
|
||||
=> ShowHelpModal = false;
|
||||
|
||||
private void CloseResultModal()
|
||||
=> ShowResultModal = false;
|
||||
|
||||
private async Task HandlePrimaryActionAsync()
|
||||
{
|
||||
if (Match is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Match.Result))
|
||||
{
|
||||
Navigation.NavigateTo("/application.html");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
Navigation.NavigateTo("/chrono.html", replace: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BuildResultView() is not null)
|
||||
{
|
||||
ShowResultModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCubePointerDownAsync(string color)
|
||||
{
|
||||
var match = Match;
|
||||
if (match is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Browser.PrimeAudioAsync();
|
||||
await TryPlayPendingAlertAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black) is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
||||
if (playerState.Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_holdStates[color].Armed = true;
|
||||
_holdStates[color].StartedAt = MatchEngine.NowUnixMs();
|
||||
}
|
||||
|
||||
private async Task HandleCubePointerUpAsync(string color)
|
||||
{
|
||||
var match = Match;
|
||||
if (match is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Browser.PrimeAudioAsync();
|
||||
await TryPlayPendingAlertAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
CancelCubeHold(color);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black) is not null)
|
||||
{
|
||||
CancelCubeHold(color);
|
||||
return;
|
||||
}
|
||||
|
||||
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
||||
if (playerState.Running)
|
||||
{
|
||||
MatchEngine.CaptureCubeTime(match, color);
|
||||
CancelCubeHold(color);
|
||||
await PersistCubeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var ready = IsHoldReady(color);
|
||||
CancelCubeHold(color);
|
||||
if (!ready)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MatchEngine.StartCubeTimer(match, color);
|
||||
await PersistCubeAsync();
|
||||
}
|
||||
|
||||
private void CancelCubeHold(string color)
|
||||
{
|
||||
_holdStates[color].Armed = false;
|
||||
_holdStates[color].StartedAt = 0;
|
||||
}
|
||||
|
||||
private async Task ApplyResultAsync()
|
||||
{
|
||||
var match = Match;
|
||||
var resultView = BuildResultView();
|
||||
if (match is null || resultView is null)
|
||||
{
|
||||
ShowResultModal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultView.ReplayRequired)
|
||||
{
|
||||
ShowResultModal = false;
|
||||
_resultModalKey = null;
|
||||
MatchEngine.ReplayCubePhase(match);
|
||||
await PersistCubeAsync();
|
||||
await TryPlayPendingAlertAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
ShowResultModal = false;
|
||||
MatchEngine.ApplyCubeOutcome(match);
|
||||
Store.MarkDirty();
|
||||
await Store.SaveAsync();
|
||||
Navigation.NavigateTo("/chrono.html");
|
||||
}
|
||||
|
||||
private async Task ReplayCubeAsync()
|
||||
{
|
||||
var match = Match;
|
||||
if (match is null || match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ShowResultModal = false;
|
||||
_resultModalKey = null;
|
||||
MatchEngine.ReplayCubePhase(match);
|
||||
await PersistCubeAsync();
|
||||
await TryPlayPendingAlertAsync();
|
||||
}
|
||||
|
||||
private async Task ResetMatchAsync()
|
||||
{
|
||||
await Store.ClearAsync();
|
||||
Navigation.NavigateTo("/application.html", replace: true);
|
||||
}
|
||||
|
||||
private async Task PersistCubeAsync()
|
||||
{
|
||||
Store.MarkDirty();
|
||||
await Store.SaveAsync();
|
||||
UpdateResultModalState();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task TryPlayPendingAlertAsync()
|
||||
{
|
||||
var match = Match;
|
||||
if (match is null || !match.Cube.PhaseAlertPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await Browser.PlayCubePhaseAlertAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match.Cube.PhaseAlertPending = false;
|
||||
Store.MarkDirty();
|
||||
await Store.SaveAsync();
|
||||
}
|
||||
|
||||
private CubeSummaryView BuildSummary()
|
||||
{
|
||||
var match = Match!;
|
||||
var blockHeading = MatchEngine.FormatBlockHeading(match, match.BlockNumber);
|
||||
var subtitle = $"{blockHeading} - {MatchEngine.Modes[match.Config.Mode].Label} - {MatchEngine.RenderModeContext(match)}";
|
||||
|
||||
if (!string.IsNullOrEmpty(match.Result))
|
||||
{
|
||||
return new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Resultat",
|
||||
MatchEngine.ResultText(match),
|
||||
"Termine",
|
||||
MatchEngine.ResultText(match),
|
||||
"Retournez a la configuration pour relancer une rencontre.",
|
||||
"Retour a l'accueil",
|
||||
"Le match est termine.",
|
||||
false);
|
||||
}
|
||||
|
||||
if (match.Cube.Times.White is not null && match.Cube.Times.Black is not null && match.Config.Mode == MatchEngine.ModeTwice && match.Cube.Times.White == match.Cube.Times.Black)
|
||||
{
|
||||
return new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Decision",
|
||||
"Egalite parfaite",
|
||||
"Reglement",
|
||||
"Rejouer la phase cube",
|
||||
"Le mode Twice impose de relancer immediatement la phase cube en cas d'egalite parfaite.",
|
||||
"Voir le resume du cube",
|
||||
"Le mode Twice impose de relancer immediatement la phase cube en cas d'egalite parfaite.",
|
||||
false);
|
||||
}
|
||||
|
||||
if (match.Cube.Times.White is not null && match.Cube.Times.Black is not null)
|
||||
{
|
||||
var preview = match.Config.Mode == MatchEngine.ModeTime
|
||||
? MatchEngine.GetTimeAdjustmentPreview(match, match.Cube.Times.White.Value, match.Cube.Times.Black.Value)
|
||||
: null;
|
||||
|
||||
return preview is null
|
||||
? new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Decision",
|
||||
"Phase cube complete",
|
||||
"Suite",
|
||||
"Ouvrir la page chrono",
|
||||
"Appliquer le resultat du cube pour preparer le Block suivant.",
|
||||
"Voir le resume du cube",
|
||||
"Appliquer le resultat du cube pour preparer le Block suivant.",
|
||||
false)
|
||||
: new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Vainqueur cube",
|
||||
preview.Winner is not null ? MatchEngine.PlayerName(match, preview.Winner) : "Egalite",
|
||||
"Impact chrono",
|
||||
preview.BlockType == "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer",
|
||||
$"Blanc {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)} -> {MatchEngine.FormatSignedClock(preview.WhiteAfter)}. Noir {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)} -> {MatchEngine.FormatSignedClock(preview.BlackAfter)}.",
|
||||
"Voir le resume du cube",
|
||||
$"Blanc {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)} -> {MatchEngine.FormatSignedClock(preview.WhiteAfter)}. Noir {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)} -> {MatchEngine.FormatSignedClock(preview.BlackAfter)}.",
|
||||
false);
|
||||
}
|
||||
|
||||
if (match.Cube.Running)
|
||||
{
|
||||
return new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Etat",
|
||||
"Chronos lances",
|
||||
"Arrets",
|
||||
"Chaque joueur se chronometre",
|
||||
"Chaque joueur demarre en relachant sa zone, puis retape sa zone une fois le cube termine.",
|
||||
"Attendre les deux temps",
|
||||
"Chaque joueur demarre en relachant sa zone, puis retape sa zone une fois le cube termine.",
|
||||
true);
|
||||
}
|
||||
|
||||
if (match.Cube.Times.White is not null || match.Cube.Times.Black is not null)
|
||||
{
|
||||
return new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Etat",
|
||||
"Un temps saisi",
|
||||
"Suite",
|
||||
"Attendre l'autre joueur",
|
||||
"Le deuxieme joueur peut encore maintenir puis relacher sa zone pour demarrer son propre chrono.",
|
||||
"Attendre le deuxieme temps",
|
||||
"Le deuxieme joueur peut encore maintenir puis relacher sa zone pour demarrer son propre chrono.",
|
||||
true);
|
||||
}
|
||||
|
||||
return new CubeSummaryView(
|
||||
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
||||
subtitle,
|
||||
"Etat",
|
||||
"Pret",
|
||||
"Depart libre",
|
||||
"Chaque joueur lance son chrono",
|
||||
"Au debut de sa resolution, chaque joueur maintient sa grande zone puis la relache pour demarrer son propre chrono.",
|
||||
"En attente des joueurs",
|
||||
"Au debut de sa resolution, chaque joueur maintient sa grande zone puis la relache pour demarrer son propre chrono.",
|
||||
true);
|
||||
}
|
||||
|
||||
private CubeZoneView BuildZone(string color)
|
||||
{
|
||||
var match = Match!;
|
||||
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
||||
var time = color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black;
|
||||
var hold = _holdStates[color];
|
||||
var holdReady = IsHoldReady(color);
|
||||
var holdArmed = hold.Armed && !holdReady && !playerState.Running && time is null && match.Phase == MatchEngine.PhaseCube && string.IsNullOrEmpty(match.Result);
|
||||
var holdProgress = hold.Armed && time is null && !playerState.Running
|
||||
? Math.Min((MatchEngine.NowUnixMs() - hold.StartedAt) / (double)MatchEngine.CubeStartHoldMs, 1d)
|
||||
: 0d;
|
||||
|
||||
if (!string.IsNullOrEmpty(match.Result))
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
MatchEngine.ResultText(match),
|
||||
"Le match est termine.",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"--cube-hold-progress: 0");
|
||||
}
|
||||
|
||||
if (match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"Retour chrono",
|
||||
"La page cube est terminee.",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"--cube-hold-progress: 0");
|
||||
}
|
||||
|
||||
if (time is not null)
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"Temps enregistre",
|
||||
"Ce joueur a deja termine son cube.",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"--cube-hold-progress: 1");
|
||||
}
|
||||
|
||||
if (playerState.Running)
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"J'ai fini le cube",
|
||||
"Tape au moment exact ou le cube est resolu.",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"--cube-hold-progress: 1");
|
||||
}
|
||||
|
||||
if (holdReady)
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"Relachez pour demarrer",
|
||||
"Le chrono partira des que vous levez le doigt.",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"--cube-hold-progress: 1");
|
||||
}
|
||||
|
||||
if (holdArmed)
|
||||
{
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"Maintenez 2 s...",
|
||||
"Gardez le doigt pose 2 secondes, jusqu'a la fin de la barre.",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
$"--cube-hold-progress: {holdProgress.ToString("0.###", CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
return new CubeZoneView(
|
||||
MatchEngine.PlayerName(match, color),
|
||||
MatchEngine.FormatCubePlayerTime(match, color),
|
||||
MatchEngine.RenderCubeMeta(match, color),
|
||||
"Maintenir 2 s pour demarrer",
|
||||
"Maintenez la grande zone 2 secondes, puis relachez pour lancer votre chrono.",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"--cube-hold-progress: 0");
|
||||
}
|
||||
|
||||
private CubeResultView? BuildResultView()
|
||||
{
|
||||
var match = Match;
|
||||
if (match is null || match.Phase != MatchEngine.PhaseCube)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var white = match.Cube.Times.White;
|
||||
var black = match.Cube.Times.Black;
|
||||
if (white is null || black is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var whiteName = MatchEngine.PlayerName(match, MatchEngine.ColorWhite);
|
||||
var blackName = MatchEngine.PlayerName(match, MatchEngine.ColorBlack);
|
||||
|
||||
if (match.Config.Mode == MatchEngine.ModeTime)
|
||||
{
|
||||
var preview = MatchEngine.GetTimeAdjustmentPreview(match, white.Value, black.Value);
|
||||
if (preview is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CubeResultView(
|
||||
$"time:{match.BlockNumber}:{match.Cube.Round}:{white}:{black}",
|
||||
"Resume du cube",
|
||||
preview.Winner is not null ? MatchEngine.PlayerName(match, preview.Winner) : "Egalite",
|
||||
preview.BlockType == "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer",
|
||||
"Validez ce resume pour appliquer les impacts chrono puis revenir a la page chrono.",
|
||||
"Appliquer et ouvrir la page chrono",
|
||||
whiteName,
|
||||
blackName,
|
||||
$"Temps cube {MatchEngine.FormatStopwatch(white.Value)}",
|
||||
$"Temps cube {MatchEngine.FormatStopwatch(black.Value)}",
|
||||
$"Impact chrono {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)}",
|
||||
$"Impact chrono {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)}",
|
||||
$"Chrono apres {MatchEngine.FormatSignedClock(preview.WhiteAfter)}",
|
||||
$"Chrono apres {MatchEngine.FormatSignedClock(preview.BlackAfter)}",
|
||||
false);
|
||||
}
|
||||
|
||||
var winner = white < black ? MatchEngine.ColorWhite : black < white ? MatchEngine.ColorBlack : null;
|
||||
var tie = winner is null;
|
||||
return new CubeResultView(
|
||||
$"twice:{match.BlockNumber}:{match.Cube.Round}:{white}:{black}",
|
||||
tie ? "Egalite parfaite" : "Resume du cube",
|
||||
winner is not null ? MatchEngine.PlayerName(match, winner) : "Egalite parfaite",
|
||||
tie ? "Rejouer la phase cube" : $"{MatchEngine.PlayerName(match, winner!)} ouvrira le Block suivant",
|
||||
tie ? "Le reglement Twice impose de rejouer immediatement la phase cube." : "Validez ce resultat pour preparer le Block suivant.",
|
||||
tie ? "Rejouer la phase cube" : "Appliquer et ouvrir la page chrono",
|
||||
whiteName,
|
||||
blackName,
|
||||
$"Temps cube {MatchEngine.FormatStopwatch(white.Value)}",
|
||||
$"Temps cube {MatchEngine.FormatStopwatch(black.Value)}",
|
||||
winner == MatchEngine.ColorWhite ? "Gagne la phase cube" : tie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
|
||||
winner == MatchEngine.ColorBlack ? "Gagne la phase cube" : tie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
|
||||
"Aucun impact chrono en mode Twice",
|
||||
"Aucun impact chrono en mode Twice",
|
||||
tie);
|
||||
}
|
||||
|
||||
private void UpdateResultModalState()
|
||||
{
|
||||
var resultView = BuildResultView();
|
||||
if (resultView is null)
|
||||
{
|
||||
ShowResultModal = false;
|
||||
_resultModalKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_resultModalKey != resultView.Key)
|
||||
{
|
||||
_resultModalKey = resultView.Key;
|
||||
ShowResultModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsHoldReady(string color)
|
||||
=> _holdStates[color].Armed && MatchEngine.NowUnixMs() - _holdStates[color].StartedAt >= MatchEngine.CubeStartHoldMs;
|
||||
|
||||
private static string BoolString(bool value)
|
||||
=> value ? "true" : "false";
|
||||
|
||||
private sealed class CubeHoldState
|
||||
{
|
||||
public bool Armed { get; set; }
|
||||
public long StartedAt { get; set; }
|
||||
}
|
||||
|
||||
private sealed record CubeSummaryView(
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string CenterLabel,
|
||||
string CenterValue,
|
||||
string SpineLabel,
|
||||
string SpineHeadline,
|
||||
string SpineText,
|
||||
string PrimaryButtonText,
|
||||
string HelpStatus,
|
||||
bool PrimaryDisabled);
|
||||
|
||||
private sealed record CubeZoneView(
|
||||
string Name,
|
||||
string ResultText,
|
||||
string MetaText,
|
||||
string ButtonText,
|
||||
string Hint,
|
||||
bool Disabled,
|
||||
bool HoldArmed,
|
||||
bool HoldReady,
|
||||
string ProgressStyle);
|
||||
|
||||
private sealed record CubeResultView(
|
||||
string Key,
|
||||
string Title,
|
||||
string Winner,
|
||||
string Outcome,
|
||||
string Summary,
|
||||
string ActionLabel,
|
||||
string WhiteName,
|
||||
string BlackName,
|
||||
string WhiteTime,
|
||||
string BlackTime,
|
||||
string WhiteDetail,
|
||||
string BlackDetail,
|
||||
string WhiteClock,
|
||||
string BlackClock,
|
||||
bool ReplayRequired);
|
||||
}
|
||||
238
ChessCubing.App/Pages/Home.razor
Normal file
238
ChessCubing.App/Pages/Home.razor
Normal file
@@ -0,0 +1,238 @@
|
||||
@page "/"
|
||||
@page "/index.html"
|
||||
|
||||
<PageTitle>ChessCubing Arena | Accueil</PageTitle>
|
||||
<PageBody BodyClass="home-body" />
|
||||
|
||||
<div class="ambient ambient-left"></div>
|
||||
<div class="ambient ambient-right"></div>
|
||||
|
||||
<div class="rules-shell">
|
||||
<header class="hero hero-home">
|
||||
<div class="hero-copy">
|
||||
<a class="logo-lockup" href="index.html" aria-label="Accueil ChessCubing">
|
||||
<img class="hero-logo-icon" src="logo.png" alt="Icone ChessCubing" />
|
||||
<img class="hero-logo" src="transparent.png" alt="Logo ChessCubing" />
|
||||
</a>
|
||||
<p class="eyebrow">Association ChessCubing</p>
|
||||
<h1>Les echecs rencontrent le Rubik's Cube</h1>
|
||||
<p class="lead">
|
||||
ChessCubing propose un jeu hybride simple a comprendre, intense a
|
||||
vivre et tres plaisant a regarder. On joue une partie d'echecs, on
|
||||
passe par une phase cube obligatoire, puis la partie repart avec un
|
||||
nouveau rythme.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href="application.html">Ouvrir l'application</a>
|
||||
<a class="button secondary" href="reglement.html">Lire le reglement</a>
|
||||
<a class="button ghost" href="/ethan/">Ouvrir l'appli d'Ethan</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Le principe</p>
|
||||
<ol class="phase-list">
|
||||
<li>Deux joueurs disputent une partie d'echecs courte et nerveuse.</li>
|
||||
<li>Une phase cube coupe le rythme et relance la tension.</li>
|
||||
<li>Le resultat du cube influence immediatement la suite du match.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="preview-banner">
|
||||
<span class="mini-chip">Decouverte rapide</span>
|
||||
<strong>On comprend en quelques minutes</strong>
|
||||
<p>
|
||||
Le format donne envie de jouer meme quand on decouvre seulement
|
||||
l'un des deux univers. On peut venir pour les echecs, pour le
|
||||
cube, ou simplement pour l'energie du duel.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main class="rules-grid">
|
||||
<section class="panel panel-wide">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">L'association</p>
|
||||
<h2>Une passerelle entre deux passions</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
L'association ChessCubing veut creer des moments de rencontre, de
|
||||
decouverte et de jeu partage autour d'un format hybride.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="home-story-grid">
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">Partager</span>
|
||||
<strong>Faire decouvrir un format original</strong>
|
||||
<p>
|
||||
L'idee est simple : reunir des profils differents autour d'un
|
||||
jeu lisible, vivant et immediatement intriguant.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">Rassembler</span>
|
||||
<strong>Creer un terrain commun</strong>
|
||||
<p>
|
||||
Joueurs d'echecs, cubers, curieux, familles ou clubs peuvent se
|
||||
retrouver dans une experience facile a lancer et a commenter.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">Animer</span>
|
||||
<strong>Donner envie de revenir jouer</strong>
|
||||
<p>
|
||||
Le melange de reflexion, de vitesse et de rebondissements donne
|
||||
au format un cote spectaculaire qui marche tres bien en
|
||||
initiation comme en evenement.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-half">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Le jeu en simple</p>
|
||||
<h2>Comment se passe une partie</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-mini-grid">
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">1</span>
|
||||
<strong>On joue une partie d'echecs</strong>
|
||||
<p>
|
||||
La partie avance par sequences courtes, ce qui garde un tres bon
|
||||
rythme et rend l'action facile a suivre.
|
||||
</p>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">2</span>
|
||||
<strong>On resout un cube</strong>
|
||||
<p>
|
||||
Les deux joueurs enchainent avec une phase cube obligatoire qui
|
||||
sert de relance, de pression et de bascule.
|
||||
</p>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">3</span>
|
||||
<strong>Le match repart</strong>
|
||||
<p>
|
||||
Selon le mode choisi, la phase cube donne l'initiative ou agit
|
||||
sur les chronos. Le duel ne retombe jamais.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-half">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Pourquoi ca plait</p>
|
||||
<h2>Un format qui donne envie d'essayer</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-mini-grid two-columns">
|
||||
<article class="mini-panel">
|
||||
<strong>Tres lisible</strong>
|
||||
<p>On comprend vite quand la pression monte et pourquoi le cube change tout.</p>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<strong>Toujours relance</strong>
|
||||
<p>Chaque phase cube remet du suspense dans la partie et ouvre de nouvelles options.</p>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<strong>Convivial</strong>
|
||||
<p>Le format marche bien en initiation, en animation de club ou en demonstration publique.</p>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<strong>Memorable</strong>
|
||||
<p>On ressort d'une partie avec des coups, des temps et des retournements dont on se souvient.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Deux formats</p>
|
||||
<h2>Deux manieres de vivre le duel</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Le reglement officiel propose deux lectures du meme concept, l'une
|
||||
plus orientee initiative, l'autre plus orientee gestion du temps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rules-compare">
|
||||
<article class="format-card twice-card">
|
||||
<div class="format-head">
|
||||
<span class="mini-chip">ChessCubing Twice</span>
|
||||
<h3>Le cube donne l'elan</h3>
|
||||
<p>
|
||||
Le joueur le plus rapide sur la phase cube prend le depart de
|
||||
la partie suivante et peut meme obtenir un double coup dans
|
||||
certaines situations.
|
||||
</p>
|
||||
</div>
|
||||
<div class="format-badges">
|
||||
<span>Mode nerveux</span>
|
||||
<span>Initiative forte</span>
|
||||
<span>Effet immediat</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="format-card time-card">
|
||||
<div class="format-head">
|
||||
<span class="mini-chip">ChessCubing Time</span>
|
||||
<h3>Le cube agit sur les chronos</h3>
|
||||
<p>
|
||||
Ici, on garde le trait mais la performance sur le cube retire
|
||||
ou ajoute du temps selon l'alternance des blocs.
|
||||
</p>
|
||||
</div>
|
||||
<div class="format-badges">
|
||||
<span>Gestion de temps</span>
|
||||
<span>Blocs - et +</span>
|
||||
<span>Suspense permanent</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide cta-panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Envie de tester</p>
|
||||
<h2>Choisis ton entree</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Tu peux decouvrir le jeu tranquillement avec la page reglement ou
|
||||
aller directement vers l'application officielle de match.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="source-grid">
|
||||
<a class="source-card" href="application.html">
|
||||
<span class="micro-label">Application</span>
|
||||
<strong>Ouvrir la partie arbitrage</strong>
|
||||
<p>Configurer une rencontre, lancer le chrono et enchainer avec la phase cube.</p>
|
||||
</a>
|
||||
<a class="source-card" href="reglement.html">
|
||||
<span class="micro-label">Reglement</span>
|
||||
<strong>Comprendre les formats officiels</strong>
|
||||
<p>Retrouver une presentation claire du Twice, du Time et des regles de match.</p>
|
||||
</a>
|
||||
<a class="source-card" href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank">
|
||||
<span class="micro-label">PDF officiel</span>
|
||||
<strong>Lire le texte source</strong>
|
||||
<p>Consulter directement le document officiel si tu veux tous les details.</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
39
ChessCubing.App/Pages/NotFound.razor
Normal file
39
ChessCubing.App/Pages/NotFound.razor
Normal file
@@ -0,0 +1,39 @@
|
||||
@page "/not-found"
|
||||
|
||||
<PageTitle>ChessCubing Arena | Page introuvable</PageTitle>
|
||||
<PageBody BodyClass="home-body" />
|
||||
|
||||
<div class="ambient ambient-left"></div>
|
||||
<div class="ambient ambient-right"></div>
|
||||
|
||||
<div class="rules-shell">
|
||||
<section class="panel panel-wide cta-panel" style="margin-top: 2rem;">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Navigation</p>
|
||||
<h1>Page introuvable</h1>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Le lien demande n'existe pas ou n'est plus expose par l'application Blazor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="source-grid">
|
||||
<a class="source-card" href="index.html">
|
||||
<span class="micro-label">Accueil</span>
|
||||
<strong>Retourner au site</strong>
|
||||
<p>Revenir a la page d'accueil de ChessCubing Arena.</p>
|
||||
</a>
|
||||
<a class="source-card" href="application.html">
|
||||
<span class="micro-label">Application</span>
|
||||
<strong>Ouvrir l'arbitrage</strong>
|
||||
<p>Configurer un match et basculer vers les phases chrono et cube.</p>
|
||||
</a>
|
||||
<a class="source-card" href="reglement.html">
|
||||
<span class="micro-label">Reglement</span>
|
||||
<strong>Lire les formats officiels</strong>
|
||||
<p>Retrouver la synthese des reglements Twice et Time.</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
344
ChessCubing.App/Pages/RulesPage.razor
Normal file
344
ChessCubing.App/Pages/RulesPage.razor
Normal file
@@ -0,0 +1,344 @@
|
||||
@page "/reglement"
|
||||
@page "/reglement.html"
|
||||
|
||||
<PageTitle>ChessCubing Arena | Reglement officiel</PageTitle>
|
||||
<PageBody BodyClass="rules-body" />
|
||||
|
||||
<div class="ambient ambient-left"></div>
|
||||
<div class="ambient ambient-right"></div>
|
||||
|
||||
<div class="rules-shell">
|
||||
<header class="hero hero-rules">
|
||||
<div class="hero-copy">
|
||||
<a class="logo-lockup" href="index.html" aria-label="Accueil ChessCubing">
|
||||
<img class="hero-logo-icon" src="logo.png" alt="Icone ChessCubing" />
|
||||
<img class="hero-logo" src="transparent.png" alt="Logo ChessCubing" />
|
||||
</a>
|
||||
<p class="eyebrow">Referentiel officiel</p>
|
||||
<h1>Reglement ChessCubing</h1>
|
||||
<p class="lead">
|
||||
Cette page reprend les regles officielles en vigueur du
|
||||
ChessCubing Twice et du ChessCubing Time pour offrir une lecture
|
||||
rapide, claire et directement exploitable en club, en demonstration
|
||||
ou en arbitrage.
|
||||
</p>
|
||||
<div class="hero-pills">
|
||||
<span>Twice V2</span>
|
||||
<span>Time V1</span>
|
||||
<span>Entree en vigueur le 4 fevrier 2026</span>
|
||||
<span>Fin uniquement par mat ou abandon</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href="application.html">Ouvrir l'application</a>
|
||||
<a class="button secondary" href="index.html">Retour a l'accueil</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Reperes rapides</p>
|
||||
<div class="rule-metrics">
|
||||
<article class="metric-chip">
|
||||
<span>Partie</span>
|
||||
<strong>180 s</strong>
|
||||
</article>
|
||||
<article class="metric-chip">
|
||||
<span>Cube</span>
|
||||
<strong>Obligatoire</strong>
|
||||
</article>
|
||||
<article class="metric-chip">
|
||||
<span>Quotas</span>
|
||||
<strong>6 / 8 / 10</strong>
|
||||
</article>
|
||||
<article class="metric-chip">
|
||||
<span>Chrono cube</span>
|
||||
<strong>Cap 120 s en Time</strong>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-banner">
|
||||
<span class="mini-chip">Deux formats</span>
|
||||
<strong>Meme structure, effets differents</strong>
|
||||
<p>
|
||||
Le mode Twice donne l'initiative au gagnant du cube et peut
|
||||
declencher un double coup. Le mode Time ajuste les chronos sur
|
||||
des blocs - et des blocs +, sans priorite ni double coup.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main class="rules-grid">
|
||||
<section class="panel panel-wide">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Vue d'ensemble</p>
|
||||
<h2>Le match en 4 temps</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les deux reglements partagent la meme colonne vertebrale : un
|
||||
match en parties successives, interrompues par une phase cube
|
||||
obligatoire.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rules-stage-grid">
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">1. Avant le match</span>
|
||||
<strong>Installation et tirage</strong>
|
||||
<p>
|
||||
L'arbitre controle le materiel, les cubes, les caches, les
|
||||
melanges, puis le tirage au sort qui attribue Blancs ou Noirs.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">2. Partie d'echecs</span>
|
||||
<strong>180 secondes de jeu</strong>
|
||||
<p>
|
||||
Chaque partie comporte une phase d'echecs limitee par une duree
|
||||
fixe et par un quota de coups selon le format FAST, FREEZE ou
|
||||
MASTERS.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">3. Phase cube</span>
|
||||
<strong>Un cube identique par joueur</strong>
|
||||
<p>
|
||||
L'application designe le numero du cube, les deux joueurs
|
||||
resolvent le meme melange, et le resultat influe ensuite selon
|
||||
le mode choisi.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">4. Fin de partie</span>
|
||||
<strong>Jamais au temps</strong>
|
||||
<p>
|
||||
La partie se termine uniquement par echec et mat ou abandon,
|
||||
jamais par simple chute au temps ou depassement d'une partie.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-half">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Materiel commun</p>
|
||||
<h2>Base officielle</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="rule-list">
|
||||
<li>Un echiquier et un jeu de pieces reglementaires.</li>
|
||||
<li>Huit Rubik's Cubes 3x3, soit quatre par joueur.</li>
|
||||
<li>Des caches opaques numerotes de 1 a 4.</li>
|
||||
<li>Des melanges strictement identiques pour chaque numero.</li>
|
||||
<li>L'application officielle ChessCubing.</li>
|
||||
<li>Un arbitre pour piloter le match et les transitions.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-half">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Arbitrage</p>
|
||||
<h2>Check-list terrain</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="rule-list">
|
||||
<li>Verifier la presence des huit cubes et des caches numerotes.</li>
|
||||
<li>Confirmer des melanges identiques sous chaque numero.</li>
|
||||
<li>Preparer l'echiquier et la variante dans l'application.</li>
|
||||
<li>Controler le tirage au sort avant la premiere partie.</li>
|
||||
<li>Declencher chaque phase cube au bon moment.</li>
|
||||
<li>Surveiller le respect du plafond de 120 s en mode Time.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide" id="formats">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Formats officiels</p>
|
||||
<h2>Twice et Time, cote a cote</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les deux formats partagent les parties et la phase cube, mais leur
|
||||
logique d'avantage differe completement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rules-compare">
|
||||
<article class="format-card twice-card">
|
||||
<div class="format-head">
|
||||
<span class="mini-chip">Version V2</span>
|
||||
<h3>ChessCubing Twice</h3>
|
||||
<p>
|
||||
Le gagnant du cube obtient l'initiative sur la partie suivante,
|
||||
avec une regle de double coup encadree.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="format-badges">
|
||||
<span>Partie : 180 s</span>
|
||||
<span>Temps par coup : 20 s max</span>
|
||||
<span>FAST / FREEZE / MASTERS : 6 / 8 / 10</span>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Debut et fin des parties</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>Les Blancs commencent la partie 1.</li>
|
||||
<li>Aucun double coup n'est possible a la partie 1.</li>
|
||||
<li>Une partie s'arrete a 180 s ou quand les deux quotas sont atteints.</li>
|
||||
<li>Il est interdit de finir une partie avec un roi en echec.</li>
|
||||
<li>Si le dernier coup donne echec, les coups necessaires pour parer sont joues hors quota.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Phase cube</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>Le numero du cube est designe par l'application.</li>
|
||||
<li>Les deux joueurs recoivent un melange identique.</li>
|
||||
<li>Le joueur le plus rapide gagne la phase cube.</li>
|
||||
<li>En cas d'egalite parfaite, la phase cube est rejouee.</li>
|
||||
<li>Le gagnant du cube commence la partie suivante.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
<span class="micro-label">Double coup V2</span>
|
||||
<strong>Condition stricte</strong>
|
||||
<p>
|
||||
Le gagnant du cube ne doit pas avoir joue le dernier coup de
|
||||
la partie precedente. Le premier coup est gratuit, non compte,
|
||||
peut capturer mais ne peut pas donner echec. Le second compte
|
||||
comme premier coup de la partie, peut donner echec, mais ne peut
|
||||
capturer qu'un pion ou une piece mineure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Temps par coup et fin de partie</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>Chaque coup doit etre joue en 20 secondes maximum.</li>
|
||||
<li>En depassement, le coup est perdu et compte dans le quota.</li>
|
||||
<li>Sur le premier coup d'un double coup, le depassement annule l'avantage.</li>
|
||||
<li>Sur le second coup d'un double coup, le coup est perdu et comptabilise.</li>
|
||||
<li>La partie se termine uniquement par mat ou abandon.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="format-card time-card">
|
||||
<div class="format-head">
|
||||
<span class="mini-chip">Version V1</span>
|
||||
<h3>ChessCubing Time</h3>
|
||||
<p>
|
||||
Ici, la phase cube n'offre pas l'initiative mais modifie les
|
||||
chronos selon une alternance bloc - puis bloc +.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="format-badges">
|
||||
<span>Temps initial : 10 min / joueur</span>
|
||||
<span>Block : 180 s</span>
|
||||
<span>Cap cube pris en compte : 120 s</span>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Structure temporelle</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>La structure des Blocks est identique a celle du Twice.</li>
|
||||
<li>Les quotas de coups restent les memes : 6, 8 ou 10.</li>
|
||||
<li>Chaque Block est suivi d'une phase cube obligatoire.</li>
|
||||
<li>Le trait est conserve apres la phase cube.</li>
|
||||
<li>Aucun systeme de priorite ou de double coup n'existe.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="split-callouts">
|
||||
<article class="split-card minus-card">
|
||||
<span class="micro-label">Block impair</span>
|
||||
<strong>Bloc -</strong>
|
||||
<p>
|
||||
Le temps de resolution du cube est retire du chrono du
|
||||
joueur concerne, avec un plafond de 120 secondes.
|
||||
</p>
|
||||
</article>
|
||||
<article class="split-card plus-card">
|
||||
<span class="micro-label">Block pair</span>
|
||||
<strong>Bloc +</strong>
|
||||
<p>
|
||||
Le temps de resolution du cube est ajoute au chrono adverse,
|
||||
lui aussi plafonne a 120 secondes.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Exemples officiels</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>Bloc - : 35 s retire 35 s a son propre chrono.</li>
|
||||
<li>Bloc - : 110 s retire 110 s a son propre chrono.</li>
|
||||
<li>Bloc + : 25 s ajoute 25 s au chrono adverse.</li>
|
||||
<li>Bloc + : 150 s ajoute 120 s au chrono adverse.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
<span class="micro-label">Reprise du jeu</span>
|
||||
<strong>Pas de coup pendant le cube</strong>
|
||||
<p>
|
||||
Aucun coup ne peut etre joue pendant la phase cube. Des que
|
||||
les deux resolutions sont terminees, les chronos sont ajustes
|
||||
et la partie reprend immediatement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Fin de partie et vigilance</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>La partie s'arrete uniquement par mat ou abandon volontaire.</li>
|
||||
<li>L'arbitre surveille le chronometrage exact et le plafond de 120 s.</li>
|
||||
<li>L'absence de priorite et de double coup fait partie des points cles du mode.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Sources</p>
|
||||
<h2>Documents officiels</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Cette synthese reprend les versions officielles actuellement
|
||||
embarquees dans l'application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="source-grid">
|
||||
<a class="source-card" href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank">
|
||||
<span class="micro-label">PDF officiel</span>
|
||||
<strong>ChessCubing Twice V2</strong>
|
||||
<p>Version entree en vigueur le 4 fevrier 2026.</p>
|
||||
</a>
|
||||
<a class="source-card" href="ChessCubing_Time_Reglement_Officiel_V1-1.pdf" target="_blank">
|
||||
<span class="micro-label">PDF officiel</span>
|
||||
<strong>ChessCubing Time V1</strong>
|
||||
<p>Version entree en vigueur le 4 fevrier 2026.</p>
|
||||
</a>
|
||||
<a class="source-card" href="application.html">
|
||||
<span class="micro-label">Application</span>
|
||||
<strong>Retour a ChessCubing Arena</strong>
|
||||
<p>Revenir a la configuration du match et a l'arbitrage mobile.</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
Reference in New Issue
Block a user