Game management
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user