fix(lobby): ready status anzeigen und Spielstart absichern

This commit is contained in:
Tim Krampitz
2026-09-10 21:52:21 +02:00
parent 01e62652f5
commit 01b6a41bc6
6 changed files with 43 additions and 6 deletions

View File

@@ -104,11 +104,18 @@ public class GameHubTests
var game = new Game { Id = 7, LobbyCode = "XYZ789", HostId = user.Id }; 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.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(); await _hub.CreateLobby();
_groups.Verify(g => g.AddToGroupAsync(ConnectionId, "XYZ789", default), Times.Once); _groups.Verify(g => g.AddToGroupAsync(ConnectionId, "XYZ789", default), Times.Once);
_callerProxy.Verify(p => p.SendCoreAsync("LobbyCreated", It.Is<object?[]>(a => a.Length == 1), default), Times.Once); _callerProxy.Verify(p => p.SendCoreAsync("LobbyCreated", It.Is<object?[]>(a => a.Length == 1), default), Times.Once);
_callerProxy.Verify(p => p.SendCoreAsync("PlayerJoined", It.Is<object?[]>(a => a.Length == 1), default), Times.Once);
} }
[Fact] [Fact]

View File

@@ -118,8 +118,9 @@ public class GameServiceTests
using var db = _factory.CreateDbContext(); using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db); var host = TestDbHelper.SeedUser(db);
var other = TestDbHelper.SeedUser(db, "other", "other@t.local"); var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
var (game, _) = TestDbHelper.SeedGame(db, host); var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
db.Players.Add(new Player { UserId = other.Id, GameId = game.Id }); hostPlayer.IsReady = true;
db.Players.Add(new Player { UserId = other.Id, GameId = game.Id, IsReady = true });
await db.SaveChangesAsync(); await db.SaveChangesAsync();
var result = await _sut.StartGameAsync(game.Id); var result = await _sut.StartGameAsync(game.Id);
@@ -131,6 +132,22 @@ public class GameServiceTests
Assert.Equal(RoundStatus.Waiting, round.Status); 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<InvalidOperationException>(() => _sut.StartGameAsync(game.Id));
Assert.Equal("All players must be ready to start the game", exception.Message);
}
[Fact] [Fact]
public async Task StartGameAsync_Throws_WhenGameNotFound() public async Task StartGameAsync_Throws_WhenGameNotFound()
{ {

View File

@@ -110,6 +110,8 @@ public class GameHub : Hub
await Groups.AddToGroupAsync(connectionId, game.LobbyCode); await Groups.AddToGroupAsync(connectionId, game.LobbyCode);
await Clients.Caller.SendAsync("LobbyCreated", new { GameId = game.Id, LobbyCode = 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); _logger.LogInformation("Lobby {LobbyCode} created by user {UserId}", game.LobbyCode, userId);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)

View File

@@ -81,6 +81,9 @@ public class GameService : IGameService
if (game.Players.Count < 2) if (game.Players.Count < 2)
throw new InvalidOperationException("At least 2 players are required to start the game"); 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.Status = GameStatus.InProgress;
game.StartedAt = DateTime.UtcNow; game.StartedAt = DateTime.UtcNow;

View File

@@ -42,6 +42,12 @@ public partial class LobbyViewModel : BaseViewModel,
[ObservableProperty] [ObservableProperty]
public partial int PendingSyncActions { get; set; } public partial int PendingSyncActions { get; set; }
[ObservableProperty]
public partial bool IsCurrentPlayerReady { get; set; }
[ObservableProperty]
public partial bool CanStartGame { get; set; }
public ObservableCollection<PlayerInfoDto> Players { get; } = []; public ObservableCollection<PlayerInfoDto> Players { get; } = [];
public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService) public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService)

View File

@@ -21,16 +21,18 @@
<Label Text="{Binding LobbyCode, StringFormat='Code: {0}'}" /> <Label Text="{Binding LobbyCode, StringFormat='Code: {0}'}" />
<Button Text="Bereit" Command="{Binding SetReadyCommand}" /> <Button Text="Bereit" Command="{Binding SetReadyCommand}" />
<Button Text="Spiel starten (Host)" Command="{Binding StartGameCommand}" IsVisible="{Binding IsHost}" /> <Label Text="Du bist bereit." TextColor="ForestGreen" IsVisible="{Binding IsCurrentPlayerReady}" />
<Button Text="Spiel starten (Host)" Command="{Binding StartGameCommand}" IsVisible="{Binding IsHost}" IsEnabled="{Binding CanStartGame}" />
<Label Text="Spieler" FontAttributes="Bold" /> <Label Text="Spieler" FontAttributes="Bold" />
<CollectionView ItemsSource="{Binding Players}"> <CollectionView ItemsSource="{Binding Players}">
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid ColumnDefinitions="*,Auto,Auto" Padding="4"> <Grid ColumnDefinitions="*,Auto,Auto,Auto" Padding="4">
<Label Grid.Column="0" Text="{Binding Username}" /> <Label Grid.Column="0" Text="{Binding Username}" />
<Label Grid.Column="1" Text="Score:" /> <Label Grid.Column="1" Text="Bereit" TextColor="ForestGreen" IsVisible="{Binding IsReady}" />
<Label Grid.Column="2" Text="{Binding Score}" /> <Label Grid.Column="2" Text="Score:" />
<Label Grid.Column="3" Text="{Binding Score}" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>