Lobby: include HostUserId in GameStateDto and let client derive host status; update tests #7

Merged
tim merged 6 commits from feature/host-userid-lobby-start into master 2026-10-04 16:12:24 +00:00
8 changed files with 45 additions and 8 deletions

View File

@@ -22,19 +22,30 @@ jobs:
- name: Install MAUI workload
run: dotnet workload install maui
- name: Restore
- name: Restore (Windows)
if: runner.os == 'Windows'
run: dotnet restore SlipItIn.slnx
- name: Build Server-Stack
- name: Restore (non-Windows)
if: runner.os != 'Windows'
# Skip restoring the Windows target frameworks to avoid requiring Windows runtime packs in non-Windows CI/tooling environments
run: dotnet restore SlipItIn.slnx -p:SkipWindowsTargetFramework=true
- name: Build Server-Stack (core projects)
run: |
dotnet build SlipItIn.Shared/SlipItIn.Shared.csproj --no-restore --configuration Release
dotnet build SlipItIn.ServiceDefaults/SlipItIn.ServiceDefaults.csproj --no-restore --configuration Release
dotnet build SlipItIn.Server/SlipItIn.Server.csproj --no-restore --configuration Release
dotnet build SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj --no-restore --configuration Release
- name: Build AppHost
if: runner.os == 'Windows'
run: |
dotnet build SlipItIn.AppHost/SlipItIn.AppHost.csproj --no-restore --configuration Release
- name: Build MAUI Client
run: dotnet build SlipItIn/SlipItIn.csproj --no-restore --configuration Debug
# Build the MAUI project for Android only in CI to avoid requiring Windows runtime packs
run: dotnet build SlipItIn/SlipItIn.csproj --no-restore --configuration Debug -f net10.0-android
- name: Run Server Tests
run: dotnet test SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj --no-restore --configuration Release

View File

@@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SlipItIN\SlipItIn.csproj">
<ProjectReference Include="..\SlipItIn\SlipItIn.csproj">
<SetTargetFramework>TargetFramework=net10.0-windows10.0.19041.0</SetTargetFramework>
</ProjectReference>
<ProjectReference Include="..\SlipItIn.Server\SlipItIn.Server.csproj" />

View File

@@ -108,6 +108,7 @@ public class GameHubTests
{
GameId = game.Id,
LobbyCode = game.LobbyCode,
HostUserId = game.HostId,
Players = [new PlayerInfoDto { Username = user.Username }]
});
@@ -151,7 +152,7 @@ public class GameHubTests
var game = new Game { Id = 3, LobbyCode = "JOIN42", HostId = 99 };
_gameService.Setup(s => s.JoinGameAsync("JOIN42", user.Id, ConnectionId)).ReturnsAsync(game);
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = 3 });
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = 3, HostUserId = 99 });
await _hub.JoinLobby("JOIN42");
@@ -205,7 +206,7 @@ public class GameHubTests
_gameService.Setup(s => s.SetPlayerReadyAsync(game.Id, player.Id))
.ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = game.Id });
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = game.Id, HostUserId = game.HostId });
await _hub.PlayerReady(game.Id, player.Id);
@@ -540,7 +541,9 @@ public class GameHubTests
SeedGameWithPlayers((user, ConnectionId));
var gameId = db.Games.Single().Id;
_gameService.Setup(s => s.GetGameStateAsync(gameId)).ReturnsAsync(new GameStateDto { GameId = gameId });
var hostId = db.Games.Single().HostId;
_gameService.Setup(s => s.GetGameStateAsync(gameId)).ReturnsAsync(new GameStateDto { GameId = gameId, HostUserId = hostId });
await _hub.RequestGameState(gameId);

View File

@@ -40,6 +40,18 @@ public class GameServiceTests
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateGameAsync(999));
}
[Fact]
public async Task GetGameStateAsync_IncludesHostUserId()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db);
var (game, _) = TestDbHelper.SeedGame(db, host);
var dto = await _sut.GetGameStateAsync(game.Id);
Assert.Equal(game.HostId, dto.HostUserId);
}
// ---------- JoinGameAsync ----------
[Fact]

View File

@@ -293,6 +293,7 @@ public class GameService : IGameService
LobbyCode = game.LobbyCode,
Status = game.Status.ToString(),
Players = playerInfos,
HostUserId = game.HostId,
CurrentRound = currentRound?.RoundNumber ?? 0,
RoundTimeRemaining = roundTimeRemaining
};

View File

@@ -6,6 +6,8 @@ public class GameStateDto
public string LobbyCode { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public List<PlayerInfoDto> Players { get; set; } = [];
// Host user id to let clients determine whether the current user is the host
public int HostUserId { get; set; }
public int CurrentRound { get; set; }
public int RoundTimeRemaining { get; set; }
}

View File

@@ -3,7 +3,9 @@
<PropertyGroup>
<TargetFrameworks>net10.0-android</TargetFrameworks>
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('linux'))">$(TargetFrameworks);</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Windows target framework can be skipped by setting SkipWindowsTargetFramework=true during restore/build (useful for CI or tooling without Windows runtime packs).
Include the Windows TFM by default so restores which evaluate OS differently still generate assets for the Windows TFM unless explicitly skipped. -->
<TargetFrameworks Condition="'$(SkipWindowsTargetFramework)' != 'true'">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<LangVersion>preview</LangVersion>
<!-- Note for MacCatalyst:

View File

@@ -144,8 +144,14 @@ public partial class LobbyViewModel : BaseViewModel,
Players.Add(player);
var currentUsername = _authSession.CurrentUser?.Username;
var currentUserId = _authSession.CurrentUser?.UserId ?? 0;
IsCurrentPlayerReady = message.Value.Players.Any(p =>
p.IsReady && string.Equals(p.Username, currentUsername, StringComparison.OrdinalIgnoreCase));
// Determine whether the current user is the host based on server-provided HostUserId
IsHost = currentUserId > 0 && message.Value.HostUserId > 0 && currentUserId == message.Value.HostUserId;
CanStartGame = message.Value.Players.Count >= 2 && message.Value.Players.All(p => p.IsReady);
});
}