From 01b6a41bc6b6c5d95940ebfeab7bcda5fc400a0e Mon Sep 17 00:00:00 2001 From: Tim Krampitz Date: Thu, 10 Sep 2026 21:52:21 +0200 Subject: [PATCH] fix(lobby): ready status anzeigen und Spielstart absichern --- SlipItIn.Server.Tests/GameHubTests.cs | 7 +++++++ SlipItIn.Server.Tests/GameServiceTests.cs | 21 +++++++++++++++++++-- SlipItIn.Server/Hubs/GameHub.cs | 2 ++ SlipItIn.Server/Services/GameService.cs | 3 +++ SlipItIn/ViewModels/LobbyViewModel.cs | 6 ++++++ SlipItIn/Views/LobbyPage.xaml | 10 ++++++---- 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/SlipItIn.Server.Tests/GameHubTests.cs b/SlipItIn.Server.Tests/GameHubTests.cs index 4c5bc44..878d653 100644 --- a/SlipItIn.Server.Tests/GameHubTests.cs +++ b/SlipItIn.Server.Tests/GameHubTests.cs @@ -104,11 +104,18 @@ public class GameHubTests var game = new Game { Id = 7, LobbyCode = "XYZ789", HostId = user.Id }; _gameService.Setup(s => s.CreateGameAsync(user.Id, ConnectionId)).ReturnsAsync(game); + _gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto + { + GameId = game.Id, + LobbyCode = game.LobbyCode, + Players = [new PlayerInfoDto { Username = user.Username }] + }); await _hub.CreateLobby(); _groups.Verify(g => g.AddToGroupAsync(ConnectionId, "XYZ789", default), Times.Once); _callerProxy.Verify(p => p.SendCoreAsync("LobbyCreated", It.Is(a => a.Length == 1), default), Times.Once); + _callerProxy.Verify(p => p.SendCoreAsync("PlayerJoined", It.Is(a => a.Length == 1), default), Times.Once); } [Fact] diff --git a/SlipItIn.Server.Tests/GameServiceTests.cs b/SlipItIn.Server.Tests/GameServiceTests.cs index 8b643b3..a97b32d 100644 --- a/SlipItIn.Server.Tests/GameServiceTests.cs +++ b/SlipItIn.Server.Tests/GameServiceTests.cs @@ -118,8 +118,9 @@ public class GameServiceTests using var db = _factory.CreateDbContext(); var host = TestDbHelper.SeedUser(db); var other = TestDbHelper.SeedUser(db, "other", "other@t.local"); - var (game, _) = TestDbHelper.SeedGame(db, host); - db.Players.Add(new Player { UserId = other.Id, GameId = game.Id }); + var (game, hostPlayer) = TestDbHelper.SeedGame(db, host); + hostPlayer.IsReady = true; + db.Players.Add(new Player { UserId = other.Id, GameId = game.Id, IsReady = true }); await db.SaveChangesAsync(); var result = await _sut.StartGameAsync(game.Id); @@ -131,6 +132,22 @@ public class GameServiceTests Assert.Equal(RoundStatus.Waiting, round.Status); } + [Fact] + public async Task StartGameAsync_Throws_WhenAnyPlayerIsNotReady() + { + using var db = _factory.CreateDbContext(); + var host = TestDbHelper.SeedUser(db); + var other = TestDbHelper.SeedUser(db, "other", "other@t.local"); + var (game, hostPlayer) = TestDbHelper.SeedGame(db, host); + hostPlayer.IsReady = true; + db.Players.Add(new Player { UserId = other.Id, GameId = game.Id, IsReady = false }); + await db.SaveChangesAsync(); + + var exception = await Assert.ThrowsAsync(() => _sut.StartGameAsync(game.Id)); + + Assert.Equal("All players must be ready to start the game", exception.Message); + } + [Fact] public async Task StartGameAsync_Throws_WhenGameNotFound() { diff --git a/SlipItIn.Server/Hubs/GameHub.cs b/SlipItIn.Server/Hubs/GameHub.cs index e81758d..d03a5b9 100644 --- a/SlipItIn.Server/Hubs/GameHub.cs +++ b/SlipItIn.Server/Hubs/GameHub.cs @@ -110,6 +110,8 @@ public class GameHub : Hub await Groups.AddToGroupAsync(connectionId, game.LobbyCode); await Clients.Caller.SendAsync("LobbyCreated", new { GameId = game.Id, LobbyCode = game.LobbyCode }); + var gameState = await _gameService.GetGameStateAsync(game.Id); + await Clients.Caller.SendAsync("PlayerJoined", gameState); _logger.LogInformation("Lobby {LobbyCode} created by user {UserId}", game.LobbyCode, userId); } catch (UnauthorizedAccessException) diff --git a/SlipItIn.Server/Services/GameService.cs b/SlipItIn.Server/Services/GameService.cs index d78b86b..b57f4ff 100644 --- a/SlipItIn.Server/Services/GameService.cs +++ b/SlipItIn.Server/Services/GameService.cs @@ -81,6 +81,9 @@ public class GameService : IGameService if (game.Players.Count < 2) throw new InvalidOperationException("At least 2 players are required to start the game"); + if (game.Players.Any(player => !player.IsReady)) + throw new InvalidOperationException("All players must be ready to start the game"); + game.Status = GameStatus.InProgress; game.StartedAt = DateTime.UtcNow; diff --git a/SlipItIn/ViewModels/LobbyViewModel.cs b/SlipItIn/ViewModels/LobbyViewModel.cs index 911aada..87184f1 100644 --- a/SlipItIn/ViewModels/LobbyViewModel.cs +++ b/SlipItIn/ViewModels/LobbyViewModel.cs @@ -42,6 +42,12 @@ public partial class LobbyViewModel : BaseViewModel, [ObservableProperty] public partial int PendingSyncActions { get; set; } + [ObservableProperty] + public partial bool IsCurrentPlayerReady { get; set; } + + [ObservableProperty] + public partial bool CanStartGame { get; set; } + public ObservableCollection Players { get; } = []; public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService) diff --git a/SlipItIn/Views/LobbyPage.xaml b/SlipItIn/Views/LobbyPage.xaml index 5db1539..89be937 100644 --- a/SlipItIn/Views/LobbyPage.xaml +++ b/SlipItIn/Views/LobbyPage.xaml @@ -21,16 +21,18 @@