Add Training Game (Phase H4)
- TrainingGameDefinition with game type metadata - TrainingGameModel + TrainingPlayerStats for stats tracking - TrainingGameLogicService implementing game logic - TrainingSetup.razor for config (ThrowMode, ThrowsPerRound, ParticipantsMode) - TrainingBoard.razor showing player stats table with totals/averages - Game type registration in Program.cs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8f4cc740a1
commit
8824162cd9
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Koogle.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace Koogle.Application.Games.Training;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game definition for Kegel-Training game type.
|
||||||
|
/// </summary>
|
||||||
|
public class TrainingGameDefinition : IGameDefinition
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "Training";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DisplayName => "Kegel-Training";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Type SetupComponentType => Type.GetType(
|
||||||
|
"Koogle.Web.Components.Game.Training.TrainingSetup, Koogle.Web")!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Type BoardComponentType => Type.GetType(
|
||||||
|
"Koogle.Web.Components.Game.Training.TrainingBoard, Koogle.Web")!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Type GameLogicServiceType => typeof(TrainingGameLogicService);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Type GameModelType => typeof(TrainingGameModel);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Koogle.Application.Games.Training;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game logic service for Training game type.
|
||||||
|
/// Training is a practice mode where players track their throw statistics.
|
||||||
|
/// </summary>
|
||||||
|
public class TrainingGameLogicService : IGameLogicService
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object CreateInitialModel(Guid[] playerIds, object? setupOptions)
|
||||||
|
{
|
||||||
|
var model = new TrainingGameModel
|
||||||
|
{
|
||||||
|
PlayerStatistics = playerIds.ToDictionary(
|
||||||
|
id => id,
|
||||||
|
_ => new TrainingPlayerStats())
|
||||||
|
};
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow)
|
||||||
|
{
|
||||||
|
var model = CastModel(gameModel);
|
||||||
|
var playerId = afterThrow.CurrentPlayerId;
|
||||||
|
|
||||||
|
if (!model.PlayerStatistics.TryGetValue(playerId, out var stats))
|
||||||
|
{
|
||||||
|
stats = new TrainingPlayerStats();
|
||||||
|
model = model with
|
||||||
|
{
|
||||||
|
PlayerStatistics = new Dictionary<Guid, TrainingPlayerStats>(model.PlayerStatistics)
|
||||||
|
{
|
||||||
|
[playerId] = stats
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
stats.ThrowCount++;
|
||||||
|
stats.PinCount += afterThrow.PinsKnocked;
|
||||||
|
|
||||||
|
if (afterThrow.IsCircle)
|
||||||
|
{
|
||||||
|
stats.CircleCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterThrow.IsStrike)
|
||||||
|
{
|
||||||
|
stats.StrikeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterThrow.IsGutter)
|
||||||
|
{
|
||||||
|
stats.GutterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if player should rotate (round complete)
|
||||||
|
var shouldRotate = afterThrow.ThrowPanel.IsRoundComplete();
|
||||||
|
|
||||||
|
var result = new ThrowResult
|
||||||
|
{
|
||||||
|
PointsScored = afterThrow.PinsKnocked,
|
||||||
|
ShouldRotatePlayer = shouldRotate,
|
||||||
|
IsGameOver = false, // Training never auto-ends
|
||||||
|
WinnerId = null,
|
||||||
|
Triggers = []
|
||||||
|
};
|
||||||
|
|
||||||
|
return (model, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel)
|
||||||
|
{
|
||||||
|
// Training mode never auto-ends - must be manually ended
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyDictionary<Guid, PlayerStatsSummary> GetPlayerStats(object gameModel)
|
||||||
|
{
|
||||||
|
var model = CastModel(gameModel);
|
||||||
|
|
||||||
|
return model.PlayerStatistics.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => new PlayerStatsSummary
|
||||||
|
{
|
||||||
|
ThrowCount = kvp.Value.ThrowCount,
|
||||||
|
PinCount = kvp.Value.PinCount,
|
||||||
|
CircleCount = kvp.Value.CircleCount,
|
||||||
|
StrikeCount = kvp.Value.StrikeCount,
|
||||||
|
CurrentPoints = kvp.Value.PinCount, // Total pins = "points" in training
|
||||||
|
CustomStats = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["GutterCount"] = kvp.Value.GutterCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public GameSetupValidationResult ValidateSetup(object? setupOptions)
|
||||||
|
{
|
||||||
|
if (setupOptions is null)
|
||||||
|
{
|
||||||
|
// Default options are valid
|
||||||
|
return GameSetupValidationResult.Valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
TrainingSetupOptions options;
|
||||||
|
if (setupOptions is TrainingSetupOptions typedOptions)
|
||||||
|
{
|
||||||
|
options = typedOptions;
|
||||||
|
}
|
||||||
|
else if (setupOptions is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
options = JsonSerializer.Deserialize<TrainingSetupOptions>(
|
||||||
|
jsonElement.GetRawText(),
|
||||||
|
GameModelFactory.JsonSerializerOptions)!;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return GameSetupValidationResult.Invalid("Ungültige Setup-Optionen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GameSetupValidationResult.Invalid("Ungültige Setup-Optionen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (options.ThrowsPerRound < 1 || options.ThrowsPerRound > 5)
|
||||||
|
{
|
||||||
|
errors.Add("Würfe pro Runde muss zwischen 1 und 5 liegen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Count > 0
|
||||||
|
? GameSetupValidationResult.Invalid(errors.ToArray())
|
||||||
|
: GameSetupValidationResult.Valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TrainingGameModel CastModel(object gameModel)
|
||||||
|
{
|
||||||
|
if (gameModel is TrainingGameModel model)
|
||||||
|
{
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameModel is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<TrainingGameModel>(
|
||||||
|
jsonElement.GetRawText(),
|
||||||
|
GameModelFactory.JsonSerializerOptions)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Expected TrainingGameModel but got {gameModel.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
using Koogle.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Koogle.Application.Games.Training;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game model for Training game type. Stores player statistics.
|
||||||
|
/// </summary>
|
||||||
|
public record TrainingGameModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics for each player in the game.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<Guid, TrainingPlayerStats> PlayerStatistics { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics for a single player in a training game.
|
||||||
|
/// </summary>
|
||||||
|
public record TrainingPlayerStats
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of throws made.
|
||||||
|
/// </summary>
|
||||||
|
public int ThrowCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of pins knocked down.
|
||||||
|
/// </summary>
|
||||||
|
public int PinCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of circles (Kranz) scored - 8 outer pins down, center standing.
|
||||||
|
/// </summary>
|
||||||
|
public int CircleCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of strikes scored - all 9 pins knocked down.
|
||||||
|
/// </summary>
|
||||||
|
public int StrikeCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of gutters (Rinne) - no pins knocked down.
|
||||||
|
/// </summary>
|
||||||
|
public int GutterCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the average pins per throw.
|
||||||
|
/// </summary>
|
||||||
|
public double Average => ThrowCount > 0 ? (double)PinCount / ThrowCount : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setup options for Training game.
|
||||||
|
/// </summary>
|
||||||
|
public record TrainingSetupOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Throw mode: Reposition (in die Vollen) or Decrease (Abräumen).
|
||||||
|
/// </summary>
|
||||||
|
public ThrowMode ThrowMode { get; init; } = ThrowMode.Reposition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of throws per round (1-5, default 3).
|
||||||
|
/// </summary>
|
||||||
|
public int ThrowsPerRound { get; init; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mode for player rotation.
|
||||||
|
/// </summary>
|
||||||
|
public ParticipantsMode ParticipantsMode { get; init; } = ParticipantsMode.GameLogic;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
@using Fluxor
|
||||||
|
@using Koogle.Application.DTOs
|
||||||
|
@using Koogle.Application.Games
|
||||||
|
@using Koogle.Application.Games.Training
|
||||||
|
@using Koogle.Web.Store.GameState
|
||||||
|
@using Koogle.Web.Store.PersonState
|
||||||
|
@using MudBlazor
|
||||||
|
|
||||||
|
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
@inject IState<GameState> GameState
|
||||||
|
@inject IState<PersonState> PersonState
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-4">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.TableChart" Class="mr-2" />
|
||||||
|
Training - Tafel
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
@if (_playerStats.Count == 0)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info">
|
||||||
|
Noch keine Statistiken vorhanden. Starte mit dem Werfen!
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudTable Items="@_playerStats"
|
||||||
|
Dense="true"
|
||||||
|
Hover="true"
|
||||||
|
Striped="true"
|
||||||
|
Bordered="true"
|
||||||
|
Class="mb-4">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Spieler</MudTh>
|
||||||
|
<MudTh Style="text-align: right">Würfe</MudTh>
|
||||||
|
<MudTh Style="text-align: right">Kegel</MudTh>
|
||||||
|
<MudTh Style="text-align: right">Kränze</MudTh>
|
||||||
|
<MudTh Style="text-align: right">Strikes</MudTh>
|
||||||
|
<MudTh Style="text-align: right">Rinnen</MudTh>
|
||||||
|
<MudTh Style="text-align: right">⌀</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
@if (context.IsCurrentPlayer)
|
||||||
|
{
|
||||||
|
<MudBadge Color="Color.Primary" Dot="true" Overlap="true">
|
||||||
|
<MudText Typo="Typo.body1" Style="font-weight: 600">
|
||||||
|
@context.PlayerName
|
||||||
|
</MudText>
|
||||||
|
</MudBadge>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1">@context.PlayerName</MudText>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd Style="text-align: right">@context.ThrowCount</MudTd>
|
||||||
|
<MudTd Style="text-align: right">@context.PinCount</MudTd>
|
||||||
|
<MudTd Style="text-align: right">
|
||||||
|
@if (context.CircleCount > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">
|
||||||
|
@context.CircleCount
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>0</span>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd Style="text-align: right">
|
||||||
|
@if (context.StrikeCount > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled">
|
||||||
|
@context.StrikeCount
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>0</span>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd Style="text-align: right">
|
||||||
|
@if (context.GutterCount > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Filled">
|
||||||
|
@context.GutterCount
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>0</span>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd Style="text-align: right; font-weight: 600">
|
||||||
|
@context.Average.ToString("F1")
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
@* Summary row *@
|
||||||
|
<MudPaper Class="pa-3 mt-4" Elevation="0" Style="background-color: var(--mud-palette-background-grey);">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
<strong>Gesamt:</strong> @_totalThrows Würfe, @_totalPins Kegel
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
<strong>Team-⌀:</strong> @_teamAverage.ToString("F2")
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<PlayerStatsRow> _playerStats = [];
|
||||||
|
private int _totalThrows;
|
||||||
|
private int _totalPins;
|
||||||
|
private double _teamAverage;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
GameState.StateChanged += OnGameStateChanged;
|
||||||
|
UpdateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGameStateChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateStats();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStats()
|
||||||
|
{
|
||||||
|
_playerStats.Clear();
|
||||||
|
_totalThrows = 0;
|
||||||
|
_totalPins = 0;
|
||||||
|
|
||||||
|
var gameState = GameState.Value;
|
||||||
|
if (gameState.GameModel is not TrainingGameModel model)
|
||||||
|
{
|
||||||
|
// Try to deserialize if it's a JsonElement
|
||||||
|
if (gameState.GameModel is System.Text.Json.JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
model = System.Text.Json.JsonSerializer.Deserialize<TrainingGameModel>(
|
||||||
|
jsonElement.GetRawText(),
|
||||||
|
GameModelFactory.JsonSerializerOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model?.PlayerStatistics == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPlayerId = gameState.Participants.CurrentPlayerId;
|
||||||
|
var persons = PersonState.Value.Persons;
|
||||||
|
|
||||||
|
foreach (var (playerId, stats) in model.PlayerStatistics)
|
||||||
|
{
|
||||||
|
var person = persons.FirstOrDefault(p => p.Id == playerId);
|
||||||
|
var playerName = person?.Name ?? "Unbekannt";
|
||||||
|
|
||||||
|
_playerStats.Add(new PlayerStatsRow
|
||||||
|
{
|
||||||
|
PlayerId = playerId,
|
||||||
|
PlayerName = playerName,
|
||||||
|
ThrowCount = stats.ThrowCount,
|
||||||
|
PinCount = stats.PinCount,
|
||||||
|
CircleCount = stats.CircleCount,
|
||||||
|
StrikeCount = stats.StrikeCount,
|
||||||
|
GutterCount = stats.GutterCount,
|
||||||
|
Average = stats.Average,
|
||||||
|
IsCurrentPlayer = playerId == currentPlayerId
|
||||||
|
});
|
||||||
|
|
||||||
|
_totalThrows += stats.ThrowCount;
|
||||||
|
_totalPins += stats.PinCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by pin count descending (best player first)
|
||||||
|
_playerStats = _playerStats.OrderByDescending(p => p.PinCount).ToList();
|
||||||
|
|
||||||
|
_teamAverage = _totalThrows > 0 ? (double)_totalPins / _totalThrows : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GameState.StateChanged -= OnGameStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PlayerStatsRow
|
||||||
|
{
|
||||||
|
public Guid PlayerId { get; init; }
|
||||||
|
public string PlayerName { get; init; } = "";
|
||||||
|
public int ThrowCount { get; init; }
|
||||||
|
public int PinCount { get; init; }
|
||||||
|
public int CircleCount { get; init; }
|
||||||
|
public int StrikeCount { get; init; }
|
||||||
|
public int GutterCount { get; init; }
|
||||||
|
public double Average { get; init; }
|
||||||
|
public bool IsCurrentPlayer { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
@using Koogle.Application.Games.Training
|
||||||
|
@using Koogle.Domain.Enums
|
||||||
|
@using MudBlazor
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-4">Training-Einstellungen</MudText>
|
||||||
|
|
||||||
|
<MudStack Spacing="4">
|
||||||
|
<MudSelect T="ThrowMode"
|
||||||
|
@bind-Value="_options.ThrowMode"
|
||||||
|
Label="Wurf-Modus"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HelperText="In die Vollen: Kegel werden nach jedem Wurf aufgestellt. Abräumen: Kegel bleiben liegen.">
|
||||||
|
<MudSelectItem Value="ThrowMode.Reposition">In die Vollen</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ThrowMode.Decrease">Abräumen</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudNumericField T="int"
|
||||||
|
@bind-Value="_options.ThrowsPerRound"
|
||||||
|
Label="Würfe pro Runde"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Min="1"
|
||||||
|
Max="5"
|
||||||
|
HelperText="Anzahl Würfe bevor der nächste Spieler an der Reihe ist (1-5)" />
|
||||||
|
|
||||||
|
<MudSelect T="ParticipantsMode"
|
||||||
|
@bind-Value="_options.ParticipantsMode"
|
||||||
|
Label="Spieler-Rotation"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HelperText="Automatisch: Spieler wechseln nach der Runde. Frei wählbar: Jeder kann werfen.">
|
||||||
|
<MudSelectItem Value="ParticipantsMode.GameLogic">Automatisch</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ParticipantsMode.FreeToChoose">Frei wählbar</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||||
|
Training ist ein freies Übungsspiel. Es gibt keinen Gewinner - nur Statistiken.
|
||||||
|
Das Spiel muss manuell beendet werden.
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>
|
||||||
|
/// Callback when setup options change.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<object> OnOptionsChanged { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initial setup options.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public object? InitialOptions { get; set; }
|
||||||
|
|
||||||
|
private TrainingSetupOptionsInternal _options = new();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (InitialOptions is TrainingSetupOptions options)
|
||||||
|
{
|
||||||
|
_options = new TrainingSetupOptionsInternal
|
||||||
|
{
|
||||||
|
ThrowMode = options.ThrowMode,
|
||||||
|
ThrowsPerRound = options.ThrowsPerRound,
|
||||||
|
ParticipantsMode = options.ParticipantsMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
await NotifyOptionsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyOptionsChanged()
|
||||||
|
{
|
||||||
|
var options = new TrainingSetupOptions
|
||||||
|
{
|
||||||
|
ThrowMode = _options.ThrowMode,
|
||||||
|
ThrowsPerRound = _options.ThrowsPerRound,
|
||||||
|
ParticipantsMode = _options.ParticipantsMode
|
||||||
|
};
|
||||||
|
await OnOptionsChanged.InvokeAsync(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current setup options.
|
||||||
|
/// </summary>
|
||||||
|
public TrainingSetupOptions GetOptions() => new()
|
||||||
|
{
|
||||||
|
ThrowMode = _options.ThrowMode,
|
||||||
|
ThrowsPerRound = _options.ThrowsPerRound,
|
||||||
|
ParticipantsMode = _options.ParticipantsMode
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal mutable class for two-way binding
|
||||||
|
private class TrainingSetupOptionsInternal
|
||||||
|
{
|
||||||
|
private ThrowMode _throwMode = ThrowMode.Reposition;
|
||||||
|
private int _throwsPerRound = 3;
|
||||||
|
private ParticipantsMode _participantsMode = ParticipantsMode.GameLogic;
|
||||||
|
|
||||||
|
public ThrowMode ThrowMode
|
||||||
|
{
|
||||||
|
get => _throwMode;
|
||||||
|
set => _throwMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ThrowsPerRound
|
||||||
|
{
|
||||||
|
get => _throwsPerRound;
|
||||||
|
set => _throwsPerRound = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParticipantsMode ParticipantsMode
|
||||||
|
{
|
||||||
|
get => _participantsMode;
|
||||||
|
set => _participantsMode = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ using System.Reflection;
|
||||||
using Fluxor;
|
using Fluxor;
|
||||||
using Fluxor.Blazor.Web.ReduxDevTools;
|
using Fluxor.Blazor.Web.ReduxDevTools;
|
||||||
using Koogle.Application;
|
using Koogle.Application;
|
||||||
|
using Koogle.Application.Games;
|
||||||
|
using Koogle.Application.Games.Training;
|
||||||
using Koogle.Infrastructure;
|
using Koogle.Infrastructure;
|
||||||
using Koogle.Infrastructure.Security;
|
using Koogle.Infrastructure.Security;
|
||||||
using Koogle.Web.Components;
|
using Koogle.Web.Components;
|
||||||
|
|
@ -19,6 +21,9 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
// Application Layer (Services, AutoMapper, Validators)
|
// Application Layer (Services, AutoMapper, Validators)
|
||||||
builder.Services.AddApplication();
|
builder.Services.AddApplication();
|
||||||
|
|
||||||
|
// Game Types
|
||||||
|
builder.Services.AddGameType<TrainingGameDefinition, TrainingGameLogicService>();
|
||||||
|
|
||||||
|
|
||||||
// FluentValidation
|
// FluentValidation
|
||||||
//builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
//builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
@ -80,6 +85,14 @@ app.UseAuthorization();
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
await IdentityRoleSeeder.SeedAsync(scope.ServiceProvider);
|
await IdentityRoleSeeder.SeedAsync(scope.ServiceProvider);
|
||||||
|
|
||||||
|
// Initialize game types
|
||||||
|
var registry = scope.ServiceProvider.GetRequiredService<GameDefinitionRegistry>();
|
||||||
|
var initializers = scope.ServiceProvider.GetServices<IGameTypeInitializer>();
|
||||||
|
foreach (var initializer in initializers)
|
||||||
|
{
|
||||||
|
initializer.Initialize(registry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await BootstrapSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
|
await BootstrapSeeder.SeedAsync(app.Services, app.Configuration, app.Environment);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue