Game management

This commit is contained in:
2026-05-06 10:19:19 +02:00
parent 89c7f5a233
commit f72735197d
4 changed files with 444 additions and 0 deletions
+21
View File
@@ -12,6 +12,27 @@ public enum RuleSet
Swiss
}
public static class RuleSetExtensions
{
public static string ToDisplayName(this RuleSet ruleSet) => ruleSet switch
{
RuleSet.DoubleElimination => "Double Elimination",
RuleSet.SingleElimination => "Single Elimination",
RuleSet.RoundRobin => "Round Robin",
RuleSet.Swiss => "Swiss",
_ => ruleSet.ToString()
};
public static RuleSet? FromDisplayName(string? displayName) => displayName switch
{
"Double Elimination" => RuleSet.DoubleElimination,
"Single Elimination" => RuleSet.SingleElimination,
"Round Robin" => RuleSet.RoundRobin,
"Swiss" => RuleSet.Swiss,
_ => null
};
}
public class Game
{
[Key]
@@ -0,0 +1,275 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using TournamentOrganizer.Models;
namespace TournamentOrganizer.ViewModels;
public partial class GamesViewModel : ViewModelBase
{
private readonly TournamentContext _context;
[ObservableProperty]
private ObservableCollection<GameDisplay> _games = [];
[ObservableProperty]
private GameDisplay? _selectedGame;
[ObservableProperty]
private string _gameName = string.Empty;
[ObservableProperty]
private string _gameDescription = string.Empty;
[ObservableProperty]
private RuleSetOption? _selectedS1RuleSet;
[ObservableProperty]
private int? _s1Groups;
[ObservableProperty]
private int? _s1GroupAdvances;
[ObservableProperty]
private RuleSetOption? _selectedS2RuleSet;
[ObservableProperty]
private string _filterGameName = string.Empty;
[ObservableProperty]
private bool _isEditing;
[ObservableProperty]
private string _statusMessage = string.Empty;
public ObservableCollection<RuleSetOption> S1RuleSetOptions { get; } = [];
public ObservableCollection<RuleSetOption> S2RuleSetOptions { get; } = [new RuleSetOption(null, "(None)")];
public bool ShowStage1GroupSettings => SelectedS2RuleSet?.Value != null;
public GamesViewModel()
{
_context = new TournamentContext();
foreach (var rs in Enum.GetValues<RuleSet>())
{
S1RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName()));
S2RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName()));
}
SelectedS1RuleSet = S1RuleSetOptions.First();
SelectedS2RuleSet = S2RuleSetOptions.First();
}
partial void OnSelectedS2RuleSetChanged(RuleSetOption? value)
{
OnPropertyChanged(nameof(ShowStage1GroupSettings));
if (value?.Value == null)
{
S1Groups = null;
S1GroupAdvances = null;
}
}
partial void OnFilterGameNameChanged(string value) => ApplyFilters();
private async void ApplyFilters()
{
await LoadGames();
}
public async Task LoadGames()
{
var allGames = await _context.Games
.Include(g => g.Tournaments)
.ThenInclude(t => t.Event)
.ToListAsync();
var filtered = allGames.AsEnumerable();
if (!string.IsNullOrWhiteSpace(FilterGameName))
{
var filter = FilterGameName.ToLower();
filtered = filtered.Where(g => g.Name.ToLower().Contains(filter));
}
Games.Clear();
foreach (var game in filtered)
{
Games.Add(new GameDisplay(game));
}
SelectedGame = null;
IsEditing = false;
}
[RelayCommand]
private async Task RefreshGames()
{
await LoadGames();
}
[RelayCommand]
private void CreateNewGame()
{
GameName = "New Game";
GameDescription = string.Empty;
SelectedS1RuleSet = S1RuleSetOptions.FirstOrDefault(o => o.Value == RuleSet.SingleElimination);
S1Groups = null;
S1GroupAdvances = null;
SelectedS2RuleSet = S2RuleSetOptions.First();
IsEditing = true;
SelectedGame = null;
StatusMessage = "Creating new game";
}
[RelayCommand]
private async Task SaveGame()
{
if (string.IsNullOrWhiteSpace(GameName))
{
StatusMessage = "Game name is required";
return;
}
Game? game;
if (SelectedGame != null && SelectedGame.Id > 0)
{
game = await _context.Games.FirstOrDefaultAsync(g => g.Id == SelectedGame.Id);
if (game == null)
{
StatusMessage = "Game not found";
return;
}
game.Name = GameName;
game.Description = GameDescription;
game.S1RuleSet = SelectedS1RuleSet!.Value!.Value;
game.S1Groups = ShowStage1GroupSettings ? S1Groups : null;
game.S1GroupAdvances = ShowStage1GroupSettings ? S1GroupAdvances : null;
game.S2RuleSet = SelectedS2RuleSet?.Value;
}
else
{
game = new Game
{
Name = GameName,
Description = GameDescription,
S1RuleSet = SelectedS1RuleSet!.Value!.Value,
S1Groups = ShowStage1GroupSettings ? S1Groups : null,
S1GroupAdvances = ShowStage1GroupSettings ? S1GroupAdvances : null,
S2RuleSet = SelectedS2RuleSet?.Value,
Tournaments = []
};
_context.Games.Add(game);
}
await _context.SaveChangesAsync();
StatusMessage = $"Game '{GameName}' saved successfully";
await LoadGames();
}
[RelayCommand]
private async Task DeleteGame()
{
if (SelectedGame == null || SelectedGame.Id == 0)
{
StatusMessage = "Select a game to delete";
return;
}
var game = await _context.Games.FirstOrDefaultAsync(g => g.Id == SelectedGame.Id);
if (game == null)
{
StatusMessage = "Game not found";
return;
}
_context.Games.Remove(game);
await _context.SaveChangesAsync();
StatusMessage = $"Game '{game.Name}' deleted";
await LoadGames();
}
partial void OnSelectedGameChanged(GameDisplay? value)
{
if (value == null)
{
IsEditing = false;
return;
}
GameName = value.Name;
GameDescription = value.Description;
SelectedS1RuleSet = S1RuleSetOptions.FirstOrDefault(o => o.Value == value.S1RuleSet) ?? S1RuleSetOptions.First();
S1Groups = value.S1Groups;
S1GroupAdvances = value.S1GroupAdvances;
SelectedS2RuleSet = S2RuleSetOptions.FirstOrDefault(o => o.Value == value.S2RuleSet) ?? S2RuleSetOptions.First();
IsEditing = true;
StatusMessage = $"Editing game '{value.Name}'";
}
}
public class GameDisplay
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public RuleSet S1RuleSet { get; set; }
public string S1RuleSetName => S1RuleSet.ToDisplayName();
public int? S1Groups { get; set; }
public int? S1GroupAdvances { get; set; }
public RuleSet? S2RuleSet { get; set; }
public string? S2RuleSetName => S2RuleSet?.ToDisplayName();
public List<string> AssociatedEvents { get; set; } = [];
public GameDisplay() { }
public GameDisplay(Game game)
{
Id = game.Id;
Name = game.Name;
Description = game.Description;
S1RuleSet = game.S1RuleSet;
S1Groups = game.S1Groups;
S1GroupAdvances = game.S1GroupAdvances;
S2RuleSet = game.S2RuleSet;
var events = new HashSet<string>();
if (game.Tournaments != null)
{
foreach (var t in game.Tournaments)
{
if (t.Event != null)
{
events.Add(t.Event.Name);
}
}
}
AssociatedEvents = events.ToList();
}
}
public class RuleSetOption
{
public RuleSet? Value { get; set; }
public string Label { get; set; } = string.Empty;
public RuleSetOption() { }
public RuleSetOption(RuleSet? value, string label)
{
Value = value;
Label = label;
}
public override string ToString() => Label;
}
+137
View File
@@ -0,0 +1,137 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:TournamentOrganizer.ViewModels"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="600"
x:Class="TournamentOrganizer.Views.GamesView"
x:DataType="vm:GamesViewModel">
<Design.DataContext>
<vm:GamesViewModel />
</Design.DataContext>
<UserControl.Styles>
<Style Selector="Button.dropdown-item">
<Setter Property="Padding" Value="8,4"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style Selector="Button.dropdown-item:pointerover">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundListLowBrush}"/>
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<TextBlock Grid.ColumnSpan="2" Grid.Row="0" Text="Games Management" FontSize="20" FontWeight="Bold" Margin="16,16,16,8"/>
<!-- Left Panel: Game List and Filter -->
<Border Grid.Column="0" Grid.Row="1" BorderBrush="Gray" BorderThickness="1" Margin="8" CornerRadius="4" Padding="8">
<DockPanel>
<!-- Header with New Game button -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<Button Command="{Binding CreateNewGameCommand}" DockPanel.Dock="Right" Padding="8,4">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="+" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text="New Game" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<TextBlock Text="Games" FontSize="16" FontWeight="SemiBold" VerticalAlignment="Center"/>
</DockPanel>
<!-- Filter -->
<StackPanel DockPanel.Dock="Top" Spacing="6" Margin="0,0,0,8">
<TextBlock Text="Filters" FontWeight="SemiBold" FontSize="12"/>
<TextBox Watermark="Filter by game name..." Text="{Binding FilterGameName}"/>
</StackPanel>
<!-- Game List -->
<ListBox ItemsSource="{Binding Games}"
SelectedItem="{Binding SelectedGame}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:GameDisplay">
<StackPanel Spacing="2">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding S1RuleSetName, StringFormat='Stage 1: {0}'}" FontSize="11" Foreground="Gray"/>
<TextBlock Text="{Binding S2RuleSetName, StringFormat='Stage 2: {0}'}" FontSize="11" Foreground="Gray" IsVisible="{Binding S2RuleSet, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<ItemsControl ItemsSource="{Binding AssociatedEvents}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{Binding, StringFormat='Event: {0}'}" FontSize="10" Foreground="#1976D2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<!-- Right Panel: Game Editor -->
<Border Grid.Column="1" Grid.Row="1" BorderBrush="Gray" BorderThickness="1" Margin="8" CornerRadius="4" Padding="8">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Game Details" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,8"/>
<StackPanel DockPanel.Dock="Bottom" Spacing="6" Margin="0,8,0,0">
<Button Content="Save Game" Command="{Binding SaveGameCommand}" HorizontalAlignment="Stretch" IsEnabled="{Binding IsEditing}"/>
<Button Content="Delete Game" Command="{Binding DeleteGameCommand}" HorizontalAlignment="Stretch" IsEnabled="{Binding IsEditing}"/>
</StackPanel>
<ScrollViewer>
<StackPanel Spacing="8" IsEnabled="{Binding IsEditing}">
<!-- Game Name -->
<StackPanel Spacing="4">
<TextBlock Text="Game Name" FontWeight="SemiBold"/>
<TextBox Text="{Binding GameName}"/>
</StackPanel>
<!-- Description -->
<StackPanel Spacing="4">
<TextBlock Text="Description" FontWeight="SemiBold"/>
<TextBox Text="{Binding GameDescription}" AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"/>
</StackPanel>
<!-- Stage 1 -->
<StackPanel Spacing="4">
<TextBlock Text="Stage 1 Rule Set" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding S1RuleSetOptions}" SelectedItem="{Binding SelectedS1RuleSet}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:RuleSetOption">
<TextBlock Text="{Binding Label}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<!-- Stage 2 (optional) -->
<StackPanel Spacing="4">
<TextBlock Text="Stage 2 Rule Set (optional)" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding S2RuleSetOptions}" SelectedItem="{Binding SelectedS2RuleSet}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:RuleSetOption">
<TextBlock Text="{Binding Label}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<!-- Stage 1 Group Settings (only shown when Stage 2 is enabled) -->
<StackPanel Spacing="4" IsVisible="{Binding ShowStage1GroupSettings}">
<TextBlock Text="Stage 1 Groups" FontWeight="SemiBold"/>
<NumericUpDown Value="{Binding S1Groups}" Minimum="1" Maximum="100"/>
</StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding ShowStage1GroupSettings}">
<TextBlock Text="Stage 1 Group Advances" FontWeight="SemiBold"/>
<NumericUpDown Value="{Binding S1GroupAdvances}" Minimum="1" Maximum="100"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</DockPanel>
</Border>
<!-- Status Bar -->
<TextBlock Grid.ColumnSpan="2" Grid.Row="2" Text="{Binding StatusMessage}" Margin="8" Foreground="Gray" FontSize="12"/>
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace TournamentOrganizer.Views;
public partial class GamesView : UserControl
{
public GamesView()
{
InitializeComponent();
}
}