Migre le projet vers Blazor WebAssembly en .NET 10
This commit is contained in:
6
ChessCubing.App/App.razor
Normal file
6
ChessCubing.App/App.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
28
ChessCubing.App/ChessCubing.App.csproj
Normal file
28
ChessCubing.App/ChessCubing.App.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
<RootNamespace>ChessCubing.App</RootNamespace>
|
||||
<AssemblyName>ChessCubing.App</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.2" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\favicon.png" Link="wwwroot/favicon.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\logo.png" Link="wwwroot/logo.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\transparent.png" Link="wwwroot/transparent.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\styles.css" Link="wwwroot/styles.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\site.webmanifest" Link="wwwroot/site.webmanifest" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\ChessCubing_Time_Reglement_Officiel_V1-1.pdf" Link="wwwroot/ChessCubing_Time_Reglement_Officiel_V1-1.pdf" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" Link="wwwroot/ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\ethan\**" Link="wwwroot/ethan/%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
28
ChessCubing.App/Components/PageBody.razor
Normal file
28
ChessCubing.App/Components/PageBody.razor
Normal file
@@ -0,0 +1,28 @@
|
||||
@inject BrowserBridge Browser
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? Page { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? BodyClass { get; set; }
|
||||
|
||||
private string? _lastSignature;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Browser.StartViewportAsync();
|
||||
}
|
||||
|
||||
var signature = $"{Page ?? string.Empty}|{BodyClass ?? string.Empty}";
|
||||
if (signature == _lastSignature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSignature = signature;
|
||||
await Browser.SetBodyStateAsync(Page, BodyClass);
|
||||
}
|
||||
}
|
||||
3
ChessCubing.App/Layout/MainLayout.razor
Normal file
3
ChessCubing.App/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,3 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
285
ChessCubing.App/Models/MatchModels.cs
Normal file
285
ChessCubing.App/Models/MatchModels.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using ChessCubing.App.Services;
|
||||
|
||||
namespace ChessCubing.App.Models;
|
||||
|
||||
public sealed class MatchConfig
|
||||
{
|
||||
[JsonPropertyName("matchLabel")]
|
||||
public string MatchLabel { get; set; } = "Rencontre ChessCubing";
|
||||
|
||||
[JsonPropertyName("competitionMode")]
|
||||
public bool CompetitionMode { get; set; }
|
||||
|
||||
[JsonPropertyName("mode")]
|
||||
public string Mode { get; set; } = MatchEngine.ModeTwice;
|
||||
|
||||
[JsonPropertyName("preset")]
|
||||
public string Preset { get; set; } = MatchEngine.PresetFast;
|
||||
|
||||
[JsonPropertyName("blockDurationMs")]
|
||||
public long BlockDurationMs { get; set; } = MatchEngine.DefaultBlockDurationMs;
|
||||
|
||||
[JsonPropertyName("moveLimitMs")]
|
||||
public long MoveLimitMs { get; set; } = MatchEngine.DefaultMoveLimitMs;
|
||||
|
||||
[JsonPropertyName("timeInitialMs")]
|
||||
public long TimeInitialMs { get; set; } = MatchEngine.TimeModeInitialClockMs;
|
||||
|
||||
[JsonPropertyName("whiteName")]
|
||||
public string WhiteName { get; set; } = "Blanc";
|
||||
|
||||
[JsonPropertyName("blackName")]
|
||||
public string BlackName { get; set; } = "Noir";
|
||||
|
||||
[JsonPropertyName("arbiterName")]
|
||||
public string ArbiterName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("eventName")]
|
||||
public string EventName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class MatchState
|
||||
{
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public int SchemaVersion { get; set; } = 3;
|
||||
|
||||
[JsonPropertyName("config")]
|
||||
public MatchConfig Config { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("phase")]
|
||||
public string Phase { get; set; } = MatchEngine.PhaseBlock;
|
||||
|
||||
[JsonPropertyName("running")]
|
||||
public bool Running { get; set; }
|
||||
|
||||
[JsonPropertyName("lastTickAt")]
|
||||
public long? LastTickAt { get; set; }
|
||||
|
||||
[JsonPropertyName("blockNumber")]
|
||||
public int BlockNumber { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("currentTurn")]
|
||||
public string CurrentTurn { get; set; } = MatchEngine.ColorWhite;
|
||||
|
||||
[JsonPropertyName("blockRemainingMs")]
|
||||
public long BlockRemainingMs { get; set; } = MatchEngine.DefaultBlockDurationMs;
|
||||
|
||||
[JsonPropertyName("moveRemainingMs")]
|
||||
public long MoveRemainingMs { get; set; } = MatchEngine.DefaultMoveLimitMs;
|
||||
|
||||
[JsonPropertyName("quota")]
|
||||
public int Quota { get; set; } = 6;
|
||||
|
||||
[JsonPropertyName("moves")]
|
||||
public PlayerIntPair Moves { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("clocks")]
|
||||
public PlayerLongPair? Clocks { get; set; }
|
||||
|
||||
[JsonPropertyName("lastMover")]
|
||||
public string? LastMover { get; set; }
|
||||
|
||||
[JsonPropertyName("awaitingBlockClosure")]
|
||||
public bool AwaitingBlockClosure { get; set; }
|
||||
|
||||
[JsonPropertyName("closureReason")]
|
||||
public string ClosureReason { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("result")]
|
||||
public string? Result { get; set; }
|
||||
|
||||
[JsonPropertyName("cube")]
|
||||
public CubeState Cube { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("doubleCoup")]
|
||||
public DoubleCoupState DoubleCoup { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("history")]
|
||||
public List<MatchHistoryEntry> History { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class PlayerIntPair
|
||||
{
|
||||
[JsonPropertyName("white")]
|
||||
public int White { get; set; }
|
||||
|
||||
[JsonPropertyName("black")]
|
||||
public int Black { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayerLongPair
|
||||
{
|
||||
[JsonPropertyName("white")]
|
||||
public long White { get; set; }
|
||||
|
||||
[JsonPropertyName("black")]
|
||||
public long Black { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayerNullableLongPair
|
||||
{
|
||||
[JsonPropertyName("white")]
|
||||
public long? White { get; set; }
|
||||
|
||||
[JsonPropertyName("black")]
|
||||
public long? Black { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CubeState
|
||||
{
|
||||
[JsonPropertyName("number")]
|
||||
public int? Number { get; set; }
|
||||
|
||||
[JsonPropertyName("running")]
|
||||
public bool Running { get; set; }
|
||||
|
||||
[JsonPropertyName("startedAt")]
|
||||
public long? StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("elapsedMs")]
|
||||
public long ElapsedMs { get; set; }
|
||||
|
||||
[JsonPropertyName("phaseAlertPending")]
|
||||
public bool PhaseAlertPending { get; set; }
|
||||
|
||||
[JsonPropertyName("times")]
|
||||
public PlayerNullableLongPair Times { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("playerState")]
|
||||
public CubePlayerStates PlayerState { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("round")]
|
||||
public int Round { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("history")]
|
||||
public List<CubeHistoryEntry> History { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class CubePlayerStates
|
||||
{
|
||||
[JsonPropertyName("white")]
|
||||
public CubePlayerState White { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("black")]
|
||||
public CubePlayerState Black { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CubePlayerState
|
||||
{
|
||||
[JsonPropertyName("running")]
|
||||
public bool Running { get; set; }
|
||||
|
||||
[JsonPropertyName("startedAt")]
|
||||
public long? StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("elapsedMs")]
|
||||
public long ElapsedMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DoubleCoupState
|
||||
{
|
||||
[JsonPropertyName("eligible")]
|
||||
public bool Eligible { get; set; }
|
||||
|
||||
[JsonPropertyName("step")]
|
||||
public int Step { get; set; }
|
||||
|
||||
[JsonPropertyName("starter")]
|
||||
public string Starter { get; set; } = MatchEngine.ColorWhite;
|
||||
}
|
||||
|
||||
public sealed class MatchHistoryEntry
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("time")]
|
||||
public string Time { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CubeHistoryEntry
|
||||
{
|
||||
[JsonPropertyName("blockNumber")]
|
||||
public int BlockNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("number")]
|
||||
public int? Number { get; set; }
|
||||
|
||||
[JsonPropertyName("white")]
|
||||
public long? White { get; set; }
|
||||
|
||||
[JsonPropertyName("black")]
|
||||
public long? Black { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SetupFormModel
|
||||
{
|
||||
public bool CompetitionMode { get; set; }
|
||||
public string MatchLabel { get; set; } = string.Empty;
|
||||
public string Mode { get; set; } = MatchEngine.ModeTwice;
|
||||
public string Preset { get; set; } = MatchEngine.PresetFast;
|
||||
public int BlockMinutes { get; set; } = 3;
|
||||
public int MoveSeconds { get; set; } = 20;
|
||||
public int TimeInitialMinutes { get; set; } = 10;
|
||||
public string WhiteName { get; set; } = "Blanc";
|
||||
public string BlackName { get; set; } = "Noir";
|
||||
public string ArbiterName { get; set; } = string.Empty;
|
||||
public string EventName { get; set; } = string.Empty;
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
|
||||
public MatchConfig ToMatchConfig()
|
||||
{
|
||||
return new MatchConfig
|
||||
{
|
||||
MatchLabel = MatchEngine.SanitizeText(MatchLabel) is { Length: > 0 } label ? label : "Rencontre ChessCubing",
|
||||
CompetitionMode = CompetitionMode,
|
||||
Mode = Mode,
|
||||
Preset = Preset,
|
||||
BlockDurationMs = MatchEngine.NormalizeDurationMs(BlockMinutes * 60_000L, MatchEngine.DefaultBlockDurationMs),
|
||||
MoveLimitMs = MatchEngine.NormalizeDurationMs(MoveSeconds * 1_000L, MatchEngine.DefaultMoveLimitMs),
|
||||
TimeInitialMs = MatchEngine.NormalizeDurationMs(TimeInitialMinutes * 60_000L, MatchEngine.TimeModeInitialClockMs),
|
||||
WhiteName = MatchEngine.SanitizeText(WhiteName) is { Length: > 0 } white ? white : "Blanc",
|
||||
BlackName = MatchEngine.SanitizeText(BlackName) is { Length: > 0 } black ? black : "Noir",
|
||||
ArbiterName = MatchEngine.SanitizeText(ArbiterName),
|
||||
EventName = MatchEngine.SanitizeText(EventName),
|
||||
Notes = MatchEngine.SanitizeText(Notes),
|
||||
};
|
||||
}
|
||||
|
||||
public static SetupFormModel CreateDemo()
|
||||
{
|
||||
return new SetupFormModel
|
||||
{
|
||||
CompetitionMode = true,
|
||||
MatchLabel = "Demo officielle ChessCubing",
|
||||
Mode = MatchEngine.ModeTwice,
|
||||
Preset = MatchEngine.PresetFreeze,
|
||||
BlockMinutes = 3,
|
||||
MoveSeconds = 20,
|
||||
TimeInitialMinutes = 10,
|
||||
WhiteName = "Nora",
|
||||
BlackName = "Leo",
|
||||
ArbiterName = "Arbitre demo",
|
||||
EventName = "Session telephone",
|
||||
Notes = "8 cubes verifies, variante prete, tirage au sort effectue.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MatchPresetInfo(string Label, int Quota, string Description);
|
||||
|
||||
public sealed record MatchModeInfo(string Label, string Subtitle);
|
||||
|
||||
public sealed record TimeAdjustmentPreview(
|
||||
string BlockType,
|
||||
string? Winner,
|
||||
long CappedWhite,
|
||||
long CappedBlack,
|
||||
long WhiteDelta,
|
||||
long BlackDelta,
|
||||
long WhiteAfter,
|
||||
long BlackAfter);
|
||||
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>
|
||||
14
ChessCubing.App/Program.cs
Normal file
14
ChessCubing.App/Program.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using ChessCubing.App;
|
||||
using ChessCubing.App.Services;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<BrowserBridge>();
|
||||
builder.Services.AddScoped<MatchStore>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
15
ChessCubing.App/Properties/launchSettings.json
Normal file
15
ChessCubing.App/Properties/launchSettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5263",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
ChessCubing.App/Services/BrowserBridge.cs
Normal file
30
ChessCubing.App/Services/BrowserBridge.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace ChessCubing.App.Services;
|
||||
|
||||
public sealed class BrowserBridge(IJSRuntime jsRuntime)
|
||||
{
|
||||
public ValueTask StartViewportAsync()
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingViewport.start");
|
||||
|
||||
public ValueTask SetBodyStateAsync(string? page, string? bodyClass)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingPage.setBodyState", page, bodyClass ?? string.Empty);
|
||||
|
||||
public ValueTask<string?> ReadMatchJsonAsync()
|
||||
=> jsRuntime.InvokeAsync<string?>("chesscubingStorage.getMatchState", MatchStore.StorageKey, MatchStore.WindowNameKey);
|
||||
|
||||
public ValueTask WriteMatchJsonAsync(string json)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.setMatchState", MatchStore.StorageKey, MatchStore.WindowNameKey, json);
|
||||
|
||||
public ValueTask ClearMatchAsync()
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.clearMatchState", MatchStore.StorageKey);
|
||||
|
||||
public ValueTask<bool> PlayCubePhaseAlertAsync()
|
||||
=> jsRuntime.InvokeAsync<bool>("chesscubingAudio.playCubePhaseAlert");
|
||||
|
||||
public ValueTask PrimeAudioAsync()
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingAudio.prime");
|
||||
|
||||
public ValueTask ForceRefreshAsync(string path)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingBrowser.forceRefresh", path);
|
||||
}
|
||||
1020
ChessCubing.App/Services/MatchEngine.cs
Normal file
1020
ChessCubing.App/Services/MatchEngine.cs
Normal file
File diff suppressed because it is too large
Load Diff
106
ChessCubing.App/Services/MatchStore.cs
Normal file
106
ChessCubing.App/Services/MatchStore.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ChessCubing.App.Models;
|
||||
|
||||
namespace ChessCubing.App.Services;
|
||||
|
||||
public sealed class MatchStore(BrowserBridge browser)
|
||||
{
|
||||
public const string StorageKey = "chesscubing-arena-state-v2";
|
||||
public const string WindowNameKey = "chesscubing-arena-state-v2:";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private bool _dirty;
|
||||
private long _lastPersistedAt;
|
||||
|
||||
public MatchState? Current { get; private set; }
|
||||
|
||||
public bool IsLoaded { get; private set; }
|
||||
|
||||
public async Task EnsureLoadedAsync()
|
||||
{
|
||||
if (IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var raw = await browser.ReadMatchJsonAsync();
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<MatchState>(raw, JsonOptions);
|
||||
if (parsed is not null && MatchEngine.IsSupportedSchemaVersion(parsed.SchemaVersion))
|
||||
{
|
||||
MatchEngine.NormalizeRecoveredMatch(parsed);
|
||||
Current = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Current = null;
|
||||
}
|
||||
|
||||
IsLoaded = true;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
}
|
||||
|
||||
public void SetCurrent(MatchState? match)
|
||||
{
|
||||
Current = match;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void MarkDirty()
|
||||
=> _dirty = true;
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Current is null)
|
||||
{
|
||||
await browser.ClearMatchAsync();
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
return;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(Current, JsonOptions);
|
||||
await browser.WriteMatchJsonAsync(json);
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
}
|
||||
|
||||
public async Task FlushIfDueAsync(long minimumIntervalMs = 1_000)
|
||||
{
|
||||
if (!_dirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MatchEngine.NowUnixMs() - _lastPersistedAt < minimumIntervalMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SaveAsync();
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
Current = null;
|
||||
IsLoaded = true;
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
await browser.ClearMatchAsync();
|
||||
}
|
||||
}
|
||||
11
ChessCubing.App/_Imports.razor
Normal file
11
ChessCubing.App/_Imports.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@using System.Globalization
|
||||
@using System.Net.Http
|
||||
@using ChessCubing.App
|
||||
@using ChessCubing.App.Components
|
||||
@using ChessCubing.App.Models
|
||||
@using ChessCubing.App.Services
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
BIN
ChessCubing.App/wwwroot/icon-192.png
Normal file
BIN
ChessCubing.App/wwwroot/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
45
ChessCubing.App/wwwroot/index.html
Normal file
45
ChessCubing.App/wwwroot/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta name="description" content="Application officielle ChessCubing Arena en Blazor .NET 10." />
|
||||
<title>ChessCubing Arena</title>
|
||||
<base href="/" />
|
||||
<link rel="preload" id="webassembly" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="shortcut icon" href="favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link href="ChessCubing.App.styles.css" rel="stylesheet" />
|
||||
<script src="js/chesscubing-interop.js"></script>
|
||||
<script type="importmap"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div style="min-height: 100dvh; display: grid; place-items: center; padding: 2rem; color: #f5f7fb;">
|
||||
<div style="text-align: center;">
|
||||
<strong style="display: block; margin-bottom: 0.75rem;">ChessCubing Arena</strong>
|
||||
<span>Chargement de l'application Blazor...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
Une erreur inattendue est survenue.
|
||||
<a href="." class="reload">Recharger</a>
|
||||
<span class="dismiss">x</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
200
ChessCubing.App/wwwroot/js/chesscubing-interop.js
Normal file
200
ChessCubing.App/wwwroot/js/chesscubing-interop.js
Normal file
@@ -0,0 +1,200 @@
|
||||
(() => {
|
||||
const assetTokenStorageKey = "chesscubing-arena-asset-token";
|
||||
let viewportStarted = false;
|
||||
let audioContext = null;
|
||||
|
||||
function syncViewportHeight() {
|
||||
const visibleHeight = window.visualViewport?.height ?? window.innerHeight;
|
||||
const viewportTopOffset = window.visualViewport?.offsetTop ?? 0;
|
||||
const viewportHeight = Math.max(
|
||||
visibleHeight + viewportTopOffset,
|
||||
window.innerHeight,
|
||||
document.documentElement.clientHeight,
|
||||
);
|
||||
|
||||
document.documentElement.style.setProperty("--app-visible-height", `${Math.round(visibleHeight)}px`);
|
||||
document.documentElement.style.setProperty("--app-viewport-top", `${Math.round(viewportTopOffset)}px`);
|
||||
document.documentElement.style.setProperty("--app-viewport-height", `${Math.round(viewportHeight)}px`);
|
||||
}
|
||||
|
||||
function startViewport() {
|
||||
if (viewportStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewportStarted = true;
|
||||
syncViewportHeight();
|
||||
window.addEventListener("load", syncViewportHeight);
|
||||
window.addEventListener("resize", syncViewportHeight);
|
||||
window.addEventListener("scroll", syncViewportHeight, { passive: true });
|
||||
window.addEventListener("pageshow", syncViewportHeight);
|
||||
window.addEventListener("orientationchange", syncViewportHeight);
|
||||
window.visualViewport?.addEventListener("resize", syncViewportHeight);
|
||||
window.visualViewport?.addEventListener("scroll", syncViewportHeight);
|
||||
window.setTimeout(syncViewportHeight, 0);
|
||||
window.setTimeout(syncViewportHeight, 150);
|
||||
window.setTimeout(syncViewportHeight, 400);
|
||||
window.requestAnimationFrame(() => window.requestAnimationFrame(syncViewportHeight));
|
||||
}
|
||||
|
||||
function getAudioContext() {
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContextClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!audioContext) {
|
||||
try {
|
||||
audioContext = new AudioContextClass();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function primeAudio() {
|
||||
const context = getAudioContext();
|
||||
if (!context || context.state === "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
context.resume().catch(() => undefined);
|
||||
}
|
||||
|
||||
function playCubePhaseAlert() {
|
||||
primeAudio();
|
||||
const context = getAudioContext();
|
||||
if (!context || context.state !== "running") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pattern = [
|
||||
{ frequency: 740, offset: 0, duration: 0.12, gain: 0.035 },
|
||||
{ frequency: 988, offset: 0.18, duration: 0.12, gain: 0.04 },
|
||||
{ frequency: 1318, offset: 0.36, duration: 0.2, gain: 0.05 },
|
||||
];
|
||||
const startAt = context.currentTime + 0.02;
|
||||
|
||||
pattern.forEach(({ frequency, offset, duration, gain }) => {
|
||||
const oscillator = context.createOscillator();
|
||||
const envelope = context.createGain();
|
||||
const toneStartAt = startAt + offset;
|
||||
|
||||
oscillator.type = "triangle";
|
||||
oscillator.frequency.setValueAtTime(frequency, toneStartAt);
|
||||
envelope.gain.setValueAtTime(0.0001, toneStartAt);
|
||||
envelope.gain.exponentialRampToValueAtTime(gain, toneStartAt + 0.02);
|
||||
envelope.gain.exponentialRampToValueAtTime(0.0001, toneStartAt + duration);
|
||||
|
||||
oscillator.connect(envelope);
|
||||
envelope.connect(context.destination);
|
||||
oscillator.start(toneStartAt);
|
||||
oscillator.stop(toneStartAt + duration + 0.03);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
window.chesscubingPage = {
|
||||
setBodyState(page, bodyClass) {
|
||||
if (page) {
|
||||
document.body.dataset.page = page;
|
||||
} else {
|
||||
delete document.body.dataset.page;
|
||||
}
|
||||
|
||||
document.body.className = bodyClass || "";
|
||||
},
|
||||
};
|
||||
|
||||
window.chesscubingViewport = {
|
||||
start: startViewport,
|
||||
};
|
||||
|
||||
window.chesscubingStorage = {
|
||||
getMatchState(storageKey, windowNameKey) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
if (raw) {
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!window.name || !window.name.startsWith(windowNameKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.name.slice(windowNameKey.length);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setMatchState(storageKey, windowNameKey, value) {
|
||||
try {
|
||||
window.name = `${windowNameKey}${value}`;
|
||||
} catch {
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, value);
|
||||
} catch {
|
||||
}
|
||||
},
|
||||
|
||||
clearMatchState(storageKey) {
|
||||
try {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
} catch {
|
||||
}
|
||||
|
||||
try {
|
||||
window.name = "";
|
||||
} catch {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.chesscubingAudio = {
|
||||
prime: primeAudio,
|
||||
playCubePhaseAlert,
|
||||
};
|
||||
|
||||
window.chesscubingBrowser = {
|
||||
async forceRefresh(path) {
|
||||
const refreshToken = `${Date.now()}`;
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(assetTokenStorageKey, refreshToken);
|
||||
} catch {
|
||||
}
|
||||
|
||||
if ("caches" in window) {
|
||||
try {
|
||||
const cacheKeys = await window.caches.keys();
|
||||
await Promise.all(cacheKeys.map((cacheKey) => window.caches.delete(cacheKey)));
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map((registration) => registration.update().catch(() => undefined)));
|
||||
await Promise.all(registrations.map((registration) => registration.unregister().catch(() => undefined)));
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
const targetUrl = new URL(path, window.location.href);
|
||||
targetUrl.searchParams.set("refresh", refreshToken);
|
||||
window.location.replace(targetUrl.toString());
|
||||
},
|
||||
};
|
||||
|
||||
startViewport();
|
||||
})();
|
||||
Reference in New Issue
Block a user