Game management
This commit is contained in:
@@ -12,6 +12,27 @@ public enum RuleSet
|
|||||||
Swiss
|
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
|
public class Game
|
||||||
{
|
{
|
||||||
[Key]
|
[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