diff --git a/src/Koogle.Application/Games/DeathBox/DeathBoxGameDefinition.cs b/src/Koogle.Application/Games/DeathBox/DeathBoxGameDefinition.cs
index 52be374..2624ec4 100644
--- a/src/Koogle.Application/Games/DeathBox/DeathBoxGameDefinition.cs
+++ b/src/Koogle.Application/Games/DeathBox/DeathBoxGameDefinition.cs
@@ -1,3 +1,4 @@
+using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
namespace Koogle.Application.Games.DeathBox;
@@ -29,4 +30,13 @@ public class DeathBoxGameDefinition : IGameDefinition
///
public Type GameModelType => typeof(DeathBoxGameModel);
+
+ ///
+ public TeamMode TeamMode => TeamMode.None;
+
+ ///
+ public int MinTeams => 2;
+
+ ///
+ public int? MaxTeams => null;
}
diff --git a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs
index 5f98eac..72b7b69 100644
--- a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs
+++ b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs
@@ -1,4 +1,5 @@
using Koogle.Application.Games.Shit;
+using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
namespace Koogle.Application.Games.FoxHunt
@@ -28,5 +29,14 @@ namespace Koogle.Application.Games.FoxHunt
///
public Type GameModelType => typeof(FoxHuntGameModel);
+
+ ///
+ public TeamMode TeamMode => TeamMode.None;
+
+ ///
+ public int MinTeams => 2;
+
+ ///
+ public int? MaxTeams => null;
}
}
diff --git a/src/Koogle.Application/Games/GameTeam.cs b/src/Koogle.Application/Games/GameTeam.cs
new file mode 100644
index 0000000..5c31d20
--- /dev/null
+++ b/src/Koogle.Application/Games/GameTeam.cs
@@ -0,0 +1,17 @@
+namespace Koogle.Application.Games;
+
+///
+/// Represents a team in a game with assigned players.
+///
+public record GameTeam
+{
+ ///
+ /// Team name (editable by user).
+ ///
+ public string Name { get; init; } = "";
+
+ ///
+ /// Player IDs assigned to this team.
+ ///
+ public IReadOnlyList PlayerIds { get; init; } = [];
+}
diff --git a/src/Koogle.Application/Games/IGameSetupModel.cs b/src/Koogle.Application/Games/IGameSetupModel.cs
index 5d9a8f3..e2750e7 100644
--- a/src/Koogle.Application/Games/IGameSetupModel.cs
+++ b/src/Koogle.Application/Games/IGameSetupModel.cs
@@ -33,6 +33,12 @@ public interface IGameSetupModel
/// Game type identifier.
///
string GameType { get; }
+
+ ///
+ /// Team assignments for team-based games.
+ /// Null when game has no teams.
+ ///
+ IReadOnlyList? Teams { get; }
}
///
@@ -51,4 +57,7 @@ public abstract record GameSetupModelBase : IGameSetupModel
///
public abstract string GameType { get; }
+
+ ///
+ public IReadOnlyList? Teams { get; init; }
}
diff --git a/src/Koogle.Application/Games/Shit/ShitGameDefinition.cs b/src/Koogle.Application/Games/Shit/ShitGameDefinition.cs
index f4cab24..e812d68 100644
--- a/src/Koogle.Application/Games/Shit/ShitGameDefinition.cs
+++ b/src/Koogle.Application/Games/Shit/ShitGameDefinition.cs
@@ -1,3 +1,4 @@
+using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
namespace Koogle.Application.Games.Shit;
@@ -28,4 +29,13 @@ public class ShitGameDefinition : IGameDefinition
///
public Type GameModelType => typeof(ShitGameModel);
+
+ ///
+ public TeamMode TeamMode => TeamMode.None;
+
+ ///
+ public int MinTeams => 2;
+
+ ///
+ public int? MaxTeams => null;
}
diff --git a/src/Koogle.Application/Games/Training/TrainingGameDefinition.cs b/src/Koogle.Application/Games/Training/TrainingGameDefinition.cs
index e56a622..3d1ad7b 100644
--- a/src/Koogle.Application/Games/Training/TrainingGameDefinition.cs
+++ b/src/Koogle.Application/Games/Training/TrainingGameDefinition.cs
@@ -1,3 +1,4 @@
+using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
namespace Koogle.Application.Games.Training;
@@ -26,4 +27,13 @@ public class TrainingGameDefinition : IGameDefinition
///
public Type GameModelType => typeof(TrainingGameModel);
+
+ ///
+ public TeamMode TeamMode => TeamMode.Optional;
+
+ ///
+ public int MinTeams => 2;
+
+ ///
+ public int? MaxTeams => null;
}
diff --git a/src/Koogle.Domain/Enums/TeamMode.cs b/src/Koogle.Domain/Enums/TeamMode.cs
new file mode 100644
index 0000000..3a1840c
--- /dev/null
+++ b/src/Koogle.Domain/Enums/TeamMode.cs
@@ -0,0 +1,22 @@
+namespace Koogle.Domain.Enums;
+
+///
+/// Defines the team mode for a game type.
+///
+public enum TeamMode
+{
+ ///
+ /// No teams - individual play only.
+ ///
+ None,
+
+ ///
+ /// Teams are optional - can play with or without teams.
+ ///
+ Optional,
+
+ ///
+ /// Teams are required - game cannot start without team assignment.
+ ///
+ Required
+}
diff --git a/src/Koogle.Domain/Interfaces/IGameDefinition.cs b/src/Koogle.Domain/Interfaces/IGameDefinition.cs
index 1976806..fee2222 100644
--- a/src/Koogle.Domain/Interfaces/IGameDefinition.cs
+++ b/src/Koogle.Domain/Interfaces/IGameDefinition.cs
@@ -1,3 +1,5 @@
+using Koogle.Domain.Enums;
+
namespace Koogle.Domain.Interfaces;
///
@@ -34,4 +36,19 @@ public interface IGameDefinition
/// Type of the game-specific model class.
///
Type GameModelType { get; }
+
+ ///
+ /// Team mode for this game type.
+ ///
+ TeamMode TeamMode { get; }
+
+ ///
+ /// Minimum number of teams required (when TeamMode != None).
+ ///
+ int MinTeams { get; }
+
+ ///
+ /// Maximum number of teams allowed (null = unlimited).
+ ///
+ int? MaxTeams { get; }
}
diff --git a/src/Koogle.Web/Components/Game/GameSetupDialog.razor b/src/Koogle.Web/Components/Game/GameSetupDialog.razor
index f3d15d1..1b3f478 100644
--- a/src/Koogle.Web/Components/Game/GameSetupDialog.razor
+++ b/src/Koogle.Web/Components/Game/GameSetupDialog.razor
@@ -10,6 +10,8 @@
@using Koogle.Web.Store.GameState
@using MudBlazor
+@* Team support *@
+
@inject IState DayState
@inject IDispatcher Dispatcher
@inject GameDefinitionRegistry GameRegistry
@@ -32,7 +34,20 @@
@bind-SelectedParticipantIds="_selectedParticipantIds"
MinimumParticipants="1" />
-
+
+ @* Step 2b: Team Assignment (only if game supports teams) *@
+ @if (_selectedDefinition?.TeamMode != TeamMode.None)
+ {
+
+
+
+ }
+
@* Step 3: Game-specific Setup *@
Teilnehmer: @_selectedParticipantIds.Count Spieler
+ @if (_teams.Count > 0)
+ {
+
+ Teams: @_teams.Count (@string.Join(", ", _teams.Select(t => $"{t.Name}: {t.PlayerIds.Count}")))
+
+ }
Modus: @GetThrowModeLabel()
@@ -129,6 +150,7 @@
private IGameDefinition? _selectedDefinition;
private IReadOnlyList _availableParticipants = [];
private IReadOnlyList _selectedParticipantIds = [];
+ private IReadOnlyList _teams = [];
// private object? _gameSpecificSetupOptions;
// private ThrowMode _throwMode = ThrowMode.Reposition;
// private int _throwsPerRound = 3;
@@ -153,6 +175,7 @@
_selectedDefinition = definition;
// _gameSpecificSetupOptions = null;
_validationError = null;
+ _teams = []; // Reset teams when game type changes
if (definition != null)
{
@@ -160,6 +183,9 @@
}
}
+ private IReadOnlyList GetSelectedParticipants() =>
+ _availableParticipants.Where(p => _selectedParticipantIds.Contains(p.PersonId)).ToList();
+
private Dictionary GetSetupParameters()
{
return new Dictionary
@@ -183,6 +209,18 @@
if (_selectedParticipantIds.Count == 0)
return false;
+ // Validate teams if required
+ if (_selectedDefinition.TeamMode == TeamMode.Required)
+ {
+ if (_teams.Count < _selectedDefinition.MinTeams)
+ return false;
+
+ // Check all players are assigned
+ var assignedPlayers = _teams.SelectMany(t => t.PlayerIds).ToHashSet();
+ if (!_selectedParticipantIds.All(id => assignedPlayers.Contains(id)))
+ return false;
+ }
+
return true;
}
diff --git a/src/Koogle.Web/Components/Game/TeamAssignmentSelector.razor b/src/Koogle.Web/Components/Game/TeamAssignmentSelector.razor
new file mode 100644
index 0000000..454a7bc
--- /dev/null
+++ b/src/Koogle.Web/Components/Game/TeamAssignmentSelector.razor
@@ -0,0 +1,303 @@
+@using Koogle.Application.DTOs
+@using Koogle.Application.Games
+@using Koogle.Domain.Enums
+@using MudBlazor
+
+
+
+ Team-Einteilung
+
+
+ Zufällig
+
+
+ Team
+
+
+
+
+ @if (_teams.Count == 0)
+ {
+
+ Füge Teams hinzu, um Spieler zuzuweisen.
+
+ }
+ else
+ {
+ @* Unbalanced teams warning *@
+ @if (HasUnbalancedTeams())
+ {
+
+ Teams haben unterschiedliche Größen.
+
+ }
+
+
+ @for (int i = 0; i < _teams.Count; i++)
+ {
+ var teamIndex = i;
+ var team = _teams[teamIndex];
+
+
+
+
+
+
+
+ (@team.PlayerIds.Count)
+
+
+
+
+
+
+
+
+
+ @foreach (var participant in _availableParticipants)
+ {
+ var isInThisTeam = team.PlayerIds.Contains(participant.PersonId);
+ var isInAnyTeam = IsPlayerInAnyTeam(participant.PersonId);
+ TogglePlayerInTeam(teamIndex, participant.PersonId)">
+
+
+
+ @participant.PersonName
+
+
+
+ }
+
+
+
+
+ }
+
+ }
+
+ @* Summary *@
+ @if (_teams.Count > 0)
+ {
+ var unassigned = GetUnassignedPlayers().Count;
+
+ @if (unassigned > 0)
+ {
+ @($"{unassigned} Spieler nicht zugewiesen")
+ }
+ else
+ {
+ @("Alle Spieler zugewiesen")
+ }
+
+ }
+
+
+@code {
+ ///
+ /// List of available participants from the day.
+ ///
+ [Parameter]
+ public IReadOnlyList AvailableParticipants { get; set; } = [];
+
+ ///
+ /// Current team assignments.
+ ///
+ [Parameter]
+ public IReadOnlyList Teams { get; set; } = [];
+
+ ///
+ /// Callback when teams change.
+ ///
+ [Parameter]
+ public EventCallback> TeamsChanged { get; set; }
+
+ ///
+ /// Minimum number of teams required.
+ ///
+ [Parameter]
+ public int MinTeams { get; set; } = 2;
+
+ ///
+ /// Maximum number of teams allowed (null = unlimited).
+ ///
+ [Parameter]
+ public int? MaxTeams { get; set; }
+
+ private List _availableParticipants = [];
+ private List _teams = [];
+
+ private class MutableTeam
+ {
+ public string Name { get; set; } = "";
+ public HashSet PlayerIds { get; set; } = new();
+ }
+
+ protected override void OnParametersSet()
+ {
+ _availableParticipants = AvailableParticipants.ToList();
+
+ // Initialize teams from parameter
+ if (Teams.Count > 0 && _teams.Count == 0)
+ {
+ _teams = Teams.Select(t => new MutableTeam
+ {
+ Name = t.Name,
+ PlayerIds = t.PlayerIds.ToHashSet()
+ }).ToList();
+ }
+
+ // Auto-create minimum teams if none exist
+ if (_teams.Count == 0 && MinTeams > 0)
+ {
+ for (int i = 0; i < MinTeams; i++)
+ {
+ _teams.Add(new MutableTeam { Name = $"Team {i + 1}" });
+ }
+ }
+ }
+
+ private async Task AddTeam()
+ {
+ if (MaxTeams.HasValue && _teams.Count >= MaxTeams.Value)
+ return;
+
+ _teams.Add(new MutableTeam { Name = $"Team {_teams.Count + 1}" });
+ await NotifyTeamsChanged();
+ }
+
+ private async Task RemoveTeam(int index)
+ {
+ if (_teams.Count <= MinTeams)
+ return;
+
+ _teams.RemoveAt(index);
+ await NotifyTeamsChanged();
+ }
+
+ private async Task UpdateTeamName(int index, string name)
+ {
+ _teams[index].Name = name;
+ await NotifyTeamsChanged();
+ }
+
+ private async Task TogglePlayerInTeam(int teamIndex, Guid playerId)
+ {
+ var team = _teams[teamIndex];
+
+ // Remove from other teams first
+ foreach (var t in _teams)
+ {
+ if (t != team)
+ {
+ t.PlayerIds.Remove(playerId);
+ }
+ }
+
+ // Toggle in this team
+ if (team.PlayerIds.Contains(playerId))
+ {
+ team.PlayerIds.Remove(playerId);
+ }
+ else
+ {
+ team.PlayerIds.Add(playerId);
+ }
+
+ await NotifyTeamsChanged();
+ }
+
+ private async Task RandomizeTeams()
+ {
+ if (_teams.Count < 2)
+ return;
+
+ // Clear all teams
+ foreach (var team in _teams)
+ {
+ team.PlayerIds.Clear();
+ }
+
+ // Shuffle participants
+ var shuffled = _availableParticipants.OrderBy(_ => Random.Shared.Next()).ToList();
+
+ // Distribute evenly
+ for (int i = 0; i < shuffled.Count; i++)
+ {
+ var teamIndex = i % _teams.Count;
+ _teams[teamIndex].PlayerIds.Add(shuffled[i].PersonId);
+ }
+
+ await NotifyTeamsChanged();
+ }
+
+ private bool IsPlayerInAnyTeam(Guid playerId) =>
+ _teams.Any(t => t.PlayerIds.Contains(playerId));
+
+ private List GetUnassignedPlayers() =>
+ _availableParticipants.Where(p => !IsPlayerInAnyTeam(p.PersonId)).ToList();
+
+ private bool HasUnbalancedTeams()
+ {
+ if (_teams.Count < 2)
+ return false;
+
+ var counts = _teams.Select(t => t.PlayerIds.Count).ToList();
+ return counts.Max() - counts.Min() > 1;
+ }
+
+ private async Task NotifyTeamsChanged()
+ {
+ var result = _teams.Select(t => new GameTeam
+ {
+ Name = t.Name,
+ PlayerIds = t.PlayerIds.ToList()
+ }).ToList();
+
+ await TeamsChanged.InvokeAsync(result);
+ }
+
+ private string GetPlayerClass(bool isSelected) =>
+ isSelected
+ ? "mud-theme-primary"
+ : "mud-paper-outlined";
+
+ ///
+ /// Gets the current team assignments.
+ ///
+ public IReadOnlyList GetTeams() => _teams.Select(t => new GameTeam
+ {
+ Name = t.Name,
+ PlayerIds = t.PlayerIds.ToList()
+ }).ToList();
+
+ ///
+ /// Validates that all players are assigned to teams.
+ ///
+ public bool IsValid() => GetUnassignedPlayers().Count == 0 && _teams.Count >= MinTeams;
+}