Slip- und Beschuldigungslogik überarbeitet
All checks were successful
Build / build (push) Successful in 2m19s
All checks were successful
Build / build (push) Successful in 2m19s
Slips werden jetzt 30 Sekunden vorgehalten und anschließend automatisch abgeschlossen; direkte Spieler-Beschuldigungen, Win-Status, SignalR-Events, MAUI-UI, EF-Mapping/Migrationen sowie Tests wurden an die neue Spiellogik angepasst.
This commit is contained in:
@@ -244,7 +244,7 @@ public class GameServiceTests
|
|||||||
// ---------- SubmitSlipAsync ----------
|
// ---------- SubmitSlipAsync ----------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SubmitSlipAsync_MarksCardAsUsed()
|
public async Task SubmitSlipAsync_HoldsCardFor30Seconds_ThenCompletes()
|
||||||
{
|
{
|
||||||
using var db = _factory.CreateDbContext();
|
using var db = _factory.CreateDbContext();
|
||||||
var host = TestDbHelper.SeedUser(db);
|
var host = TestDbHelper.SeedUser(db);
|
||||||
@@ -260,7 +260,15 @@ public class GameServiceTests
|
|||||||
|
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
using var verify = _factory.CreateDbContext();
|
using var verify = _factory.CreateDbContext();
|
||||||
Assert.True(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
|
var verifyCard = verify.PlayerCards.Single(c => c.Id == card.Id);
|
||||||
|
Assert.NotNull(verifyCard.SlippedAt);
|
||||||
|
Assert.False(verifyCard.IsUsed); // Für 30 Sekunden vorgehalten!
|
||||||
|
|
||||||
|
// CompleteSlipAsync vollendet nach Ablauf der Zeit
|
||||||
|
var completed = await _sut.CompleteSlipAsync(game.Id, card.Id, force: true);
|
||||||
|
Assert.True(completed);
|
||||||
|
using var verify2 = _factory.CreateDbContext();
|
||||||
|
Assert.True(verify2.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -307,11 +315,11 @@ public class GameServiceTests
|
|||||||
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
db.GameRounds.Add(round);
|
db.GameRounds.Add(round);
|
||||||
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, IsUsed = true };
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
|
||||||
db.PlayerCards.Add(card);
|
db.PlayerCards.Add(card);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id);
|
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id);
|
||||||
|
|
||||||
Assert.Equal(round.Id, challenge.GameRoundId);
|
Assert.Equal(round.Id, challenge.GameRoundId);
|
||||||
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
|
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
|
||||||
@@ -321,9 +329,37 @@ public class GameServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateChallengeAsync_Throws_WhenTargetCardNotFound()
|
public async Task CreateChallengeAsync_ChallengesPlayerDirectly()
|
||||||
{
|
{
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(1, 1, 999));
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id);
|
||||||
|
|
||||||
|
Assert.Equal(round.Id, challenge.GameRoundId);
|
||||||
|
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
|
||||||
|
Assert.Equal(otherPlayer.Id, challenge.TargetPlayerId);
|
||||||
|
Assert.Equal(ChallengeStatus.Pending, challenge.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateChallengeAsync_Throws_WhenTargetPlayerNotFound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, 999));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -331,7 +367,10 @@ 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 (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting };
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting };
|
||||||
db.GameRounds.Add(round);
|
db.GameRounds.Add(round);
|
||||||
@@ -339,7 +378,7 @@ public class GameServiceTests
|
|||||||
db.PlayerCards.Add(card);
|
db.PlayerCards.Add(card);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id));
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -361,7 +400,7 @@ public class GameServiceTests
|
|||||||
|
|
||||||
// outsider ist nicht in diesem Spiel und versucht eine Challenge anzulegen
|
// outsider ist nicht in diesem Spiel und versucht eine Challenge anzulegen
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||||
() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id + 1000, card.Id));
|
() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id + 1000, otherPlayer.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -382,15 +421,15 @@ public class GameServiceTests
|
|||||||
db.PlayerCards.Add(cardB);
|
db.PlayerCards.Add(cardB);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// hostPlayerA gehört zu gameA, versucht aber eine Karte aus gameB zu challengen
|
// hostPlayerA gehört zu gameA, versucht aber einen Spieler aus gameB zu challengen
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||||||
() => _sut.CreateChallengeAsync(gameB.Id, hostPlayerA.Id, cardB.Id));
|
() => _sut.CreateChallengeAsync(gameB.Id, hostPlayerA.Id, otherPlayerB.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- ResolveChallengeAsync ----------
|
// ---------- ResolveChallengeAsync ----------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResolveChallengeAsync_Approved_TransfersCardToChallenger()
|
public async Task ResolveChallengeAsync_Approved_DiscardsCardAndDrawsTwoNewCards()
|
||||||
{
|
{
|
||||||
using var db = _factory.CreateDbContext();
|
using var db = _factory.CreateDbContext();
|
||||||
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
@@ -400,8 +439,8 @@ public class GameServiceTests
|
|||||||
db.Players.Add(otherPlayer);
|
db.Players.Add(otherPlayer);
|
||||||
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
db.GameRounds.Add(round);
|
db.GameRounds.Add(round);
|
||||||
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
var phrases = TestDbHelper.SeedPhrases(db, 5);
|
||||||
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, IsUsed = true };
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrases[0].Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
|
||||||
db.PlayerCards.Add(card);
|
db.PlayerCards.Add(card);
|
||||||
var challenge = new SlipChallenge
|
var challenge = new SlipChallenge
|
||||||
{
|
{
|
||||||
@@ -418,13 +457,19 @@ public class GameServiceTests
|
|||||||
|
|
||||||
Assert.Equal(ChallengeStatus.Approved, result.Status);
|
Assert.Equal(ChallengeStatus.Approved, result.Status);
|
||||||
Assert.NotNull(result.ResolvedAt);
|
Assert.NotNull(result.ResolvedAt);
|
||||||
// Berechtigte Beschuldigung: Karte geht an den Beschuldiger
|
|
||||||
using var verify = _factory.CreateDbContext();
|
using var verify = _factory.CreateDbContext();
|
||||||
Assert.Equal(hostPlayer.Id, verify.PlayerCards.Single(c => c.Id == card.Id).PlayerId);
|
// Vorgehaltene Karte zählt nicht und wird abgelegt (IsUsed = true)
|
||||||
|
Assert.True(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
|
||||||
|
// Beschuldigter erhält 2 neue Karten (insgesamt 3 Karten für otherPlayer)
|
||||||
|
Assert.Equal(3, verify.PlayerCards.Count(c => c.PlayerId == otherPlayer.Id));
|
||||||
|
// 2 davon aktiv
|
||||||
|
Assert.Equal(2, verify.PlayerCards.Count(c => c.PlayerId == otherPlayer.Id && !c.IsUsed));
|
||||||
|
// Challenger erhält +1 Score
|
||||||
|
Assert.Equal(1, verify.Players.Single(p => p.Id == hostPlayer.Id).Score);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResolveChallengeAsync_Rejected_KeepsCardWithTargetPlayer()
|
public async Task ResolveChallengeAsync_Rejected_WithdrawsChallengeWithoutPenalty()
|
||||||
{
|
{
|
||||||
using var db = _factory.CreateDbContext();
|
using var db = _factory.CreateDbContext();
|
||||||
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
@@ -434,8 +479,8 @@ public class GameServiceTests
|
|||||||
db.Players.Add(otherPlayer);
|
db.Players.Add(otherPlayer);
|
||||||
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
db.GameRounds.Add(round);
|
db.GameRounds.Add(round);
|
||||||
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
var phrases = TestDbHelper.SeedPhrases(db, 3);
|
||||||
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, IsUsed = true };
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrases[0].Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
|
||||||
db.PlayerCards.Add(card);
|
db.PlayerCards.Add(card);
|
||||||
var challenge = new SlipChallenge
|
var challenge = new SlipChallenge
|
||||||
{
|
{
|
||||||
@@ -452,9 +497,11 @@ public class GameServiceTests
|
|||||||
|
|
||||||
Assert.Equal(ChallengeStatus.Rejected, result.Status);
|
Assert.Equal(ChallengeStatus.Rejected, result.Status);
|
||||||
Assert.NotNull(result.ResolvedAt);
|
Assert.NotNull(result.ResolvedAt);
|
||||||
// Falsche Beschuldigung: Karte bleibt beim Beschuldigten
|
|
||||||
using var verify = _factory.CreateDbContext();
|
using var verify = _factory.CreateDbContext();
|
||||||
Assert.Equal(otherPlayer.Id, verify.PlayerCards.Single(c => c.Id == card.Id).PlayerId);
|
// Keine Karten abgelegt oder neu gezogen
|
||||||
|
Assert.Single(verify.PlayerCards.Where(c => c.PlayerId == otherPlayer.Id));
|
||||||
|
Assert.False(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
|
||||||
|
Assert.Equal(0, verify.Players.Single(p => p.Id == hostPlayer.Id).Score);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -463,6 +510,33 @@ public class GameServiceTests
|
|||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.ResolveChallengeAsync(999, true));
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.ResolveChallengeAsync(999, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Win Condition (Point 3) ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CompleteSlipAsync_PlayerWins_WhenNoCardsRemain()
|
||||||
|
{
|
||||||
|
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, GameStatus.InProgress);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, SlippedAt = DateTime.UtcNow.AddSeconds(-31) };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var completed = await _sut.CompleteSlipAsync(game.Id, card.Id);
|
||||||
|
|
||||||
|
Assert.True(completed);
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
var verifyGame = verify.Games.Single(g => g.Id == game.Id);
|
||||||
|
Assert.Equal(GameStatus.Completed, verifyGame.Status);
|
||||||
|
Assert.Equal(hostPlayer.Id, verifyGame.WinnerPlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- GetGameStateAsync ----------
|
// ---------- GetGameStateAsync ----------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public class SlipItInDbContext : DbContext
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(g => g.HostId)
|
.HasForeignKey(g => g.HostId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<Game>()
|
||||||
|
.HasOne(g => g.WinnerPlayer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(g => g.WinnerPlayerId)
|
||||||
|
.IsRequired(false)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
modelBuilder.Entity<Player>()
|
modelBuilder.Entity<Player>()
|
||||||
@@ -92,6 +98,7 @@ public class SlipItInDbContext : DbContext
|
|||||||
.HasOne(sc => sc.TargetCard)
|
.HasOne(sc => sc.TargetCard)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(sc => sc.TargetCardId)
|
.HasForeignKey(sc => sc.TargetCardId)
|
||||||
|
.IsRequired(false)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
SlipItIn.Server/Data/SlipItInDbContextFactory.cs
Normal file
14
SlipItIn.Server/Data/SlipItInDbContextFactory.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Data;
|
||||||
|
|
||||||
|
public class SlipItInDbContextFactory : IDesignTimeDbContextFactory<SlipItInDbContext>
|
||||||
|
{
|
||||||
|
public SlipItInDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<SlipItInDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql("Host=localhost;Database=slipitindb;Username=postgres;Password=postgres");
|
||||||
|
return new SlipItInDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -242,6 +242,35 @@ public class GameHub : Hub
|
|||||||
Success = result
|
Success = result
|
||||||
});
|
});
|
||||||
await Clients.Caller.SendAsync("PlayerHandUpdated", hand);
|
await Clients.Caller.SendAsync("PlayerHandUpdated", hand);
|
||||||
|
|
||||||
|
// Nach 30 Sekunden Vorhaltezeit automatisch vollenden (falls keine Beschuldigung dazwischen kam)
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Kleiner Puffer, da Task.Delay minimal früher zurückkehren kann als die 30s-Prüfung
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30.5));
|
||||||
|
var completed = await _gameService.CompleteSlipAsync(gameId, cardId);
|
||||||
|
if (completed)
|
||||||
|
{
|
||||||
|
var updatedHand = await _gameService.GetPlayerHandAsync(playerId);
|
||||||
|
var updatedGameState = await _gameService.GetGameStateAsync(gameId);
|
||||||
|
|
||||||
|
using var db = _contextFactory.CreateDbContext();
|
||||||
|
var targetP = await db.Players.FindAsync(playerId);
|
||||||
|
if (targetP?.ConnectionId != null)
|
||||||
|
{
|
||||||
|
await Clients.Client(targetP.ConnectionId).SendAsync("PlayerHandUpdated", updatedHand);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("GameStateUpdated", updatedGameState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error completing slip for card {CardId}", cardId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -254,8 +283,59 @@ public class GameHub : Hub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ChallengePlayer(int gameId, int challengingPlayerId, int targetPlayerId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidatePlayerAccessAsync(challengingPlayerId, userId);
|
||||||
|
|
||||||
|
var challenge = await _gameService.CreateChallengeAsync(gameId, challengingPlayerId, targetPlayerId);
|
||||||
|
var game = await _gameService.GetGameAsync(gameId);
|
||||||
|
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var challengingPlayer = await context.Players
|
||||||
|
.Include(p => p.User)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == challengingPlayerId);
|
||||||
|
|
||||||
|
var targetPlayer = await context.Players
|
||||||
|
.Include(p => p.User)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == targetPlayerId);
|
||||||
|
|
||||||
|
var challengeDto = new SlipChallengeDto
|
||||||
|
{
|
||||||
|
ChallengeId = challenge.Id,
|
||||||
|
ChallengingPlayerId = challengingPlayerId,
|
||||||
|
ChallengingPlayerName = challengingPlayer?.User.Username ?? string.Empty,
|
||||||
|
TargetPlayerId = challenge.TargetPlayerId,
|
||||||
|
TargetPlayerName = targetPlayer?.User.Username ?? string.Empty,
|
||||||
|
TargetCardId = challenge.TargetCardId,
|
||||||
|
Status = challenge.Status.ToString(),
|
||||||
|
CreatedAt = challenge.CreatedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("SlipChallenged", challengeDto);
|
||||||
|
|
||||||
|
// Sende direkte Notification an den beschuldigten Spieler
|
||||||
|
if (targetPlayer?.ConnectionId != null)
|
||||||
|
{
|
||||||
|
await Clients.Client(targetPlayer.ConnectionId).SendAsync("ChallengeReceived", challengeDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error challenging player");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while challenging player." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ChallengeSlip(int gameId, int challengingPlayerId, int targetCardId)
|
public async Task ChallengeSlip(int gameId, int challengingPlayerId, int targetCardId)
|
||||||
{
|
{
|
||||||
|
// Kompatibilitätsmethode
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = GetAuthenticatedUserId();
|
var userId = GetAuthenticatedUserId();
|
||||||
@@ -269,13 +349,18 @@ public class GameHub : Hub
|
|||||||
.Include(p => p.User)
|
.Include(p => p.User)
|
||||||
.FirstOrDefaultAsync(p => p.Id == challengingPlayerId);
|
.FirstOrDefaultAsync(p => p.Id == challengingPlayerId);
|
||||||
|
|
||||||
|
var targetPlayer = await context.Players
|
||||||
|
.Include(p => p.User)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == challenge.TargetPlayerId);
|
||||||
|
|
||||||
var challengeDto = new SlipChallengeDto
|
var challengeDto = new SlipChallengeDto
|
||||||
{
|
{
|
||||||
ChallengeId = challenge.Id,
|
ChallengeId = challenge.Id,
|
||||||
ChallengingPlayerId = challengingPlayerId,
|
ChallengingPlayerId = challengingPlayerId,
|
||||||
ChallengingPlayerName = challengingPlayer?.User.Username ?? string.Empty,
|
ChallengingPlayerName = challengingPlayer?.User.Username ?? string.Empty,
|
||||||
TargetPlayerId = challenge.TargetPlayerId,
|
TargetPlayerId = challenge.TargetPlayerId,
|
||||||
TargetCardId = targetCardId,
|
TargetPlayerName = targetPlayer?.User.Username ?? string.Empty,
|
||||||
|
TargetCardId = challenge.TargetCardId,
|
||||||
Status = challenge.Status.ToString(),
|
Status = challenge.Status.ToString(),
|
||||||
CreatedAt = challenge.CreatedAt
|
CreatedAt = challenge.CreatedAt
|
||||||
};
|
};
|
||||||
@@ -283,7 +368,6 @@ public class GameHub : Hub
|
|||||||
await Clients.Group(game.LobbyCode).SendAsync("SlipChallenged", challengeDto);
|
await Clients.Group(game.LobbyCode).SendAsync("SlipChallenged", challengeDto);
|
||||||
|
|
||||||
// Sende direkte Notification an den beschuldigten Spieler
|
// Sende direkte Notification an den beschuldigten Spieler
|
||||||
var targetPlayer = await context.Players.FindAsync(challenge.TargetPlayerId);
|
|
||||||
if (targetPlayer?.ConnectionId != null)
|
if (targetPlayer?.ConnectionId != null)
|
||||||
{
|
{
|
||||||
await Clients.Client(targetPlayer.ConnectionId).SendAsync("ChallengeReceived", challengeDto);
|
await Clients.Client(targetPlayer.ConnectionId).SendAsync("ChallengeReceived", challengeDto);
|
||||||
@@ -300,15 +384,22 @@ public class GameHub : Hub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ResolveChallenge(int challengeId, bool approved)
|
public async Task ResolveChallenge(int challengeId, bool approved, int? discardCardId = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = GetAuthenticatedUserId();
|
var userId = GetAuthenticatedUserId();
|
||||||
await ValidateChallengeTargetAccessAsync(challengeId, userId);
|
await ValidateChallengeTargetAccessAsync(challengeId, userId);
|
||||||
|
|
||||||
var result = await _gameService.ResolveChallengeAsync(challengeId, approved);
|
var result = await _gameService.ResolveChallengeAsync(challengeId, approved, discardCardId);
|
||||||
var game = await _gameService.GetGameAsync(result.GameRound.GameId);
|
int gameId = result.GameRound?.GameId ?? 0;
|
||||||
|
if (gameId == 0)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var round = await context.GameRounds.FindAsync(result.GameRoundId);
|
||||||
|
gameId = round?.GameId ?? 0;
|
||||||
|
}
|
||||||
|
var game = await _gameService.GetGameAsync(gameId);
|
||||||
|
|
||||||
await Clients.Group(game.LobbyCode).SendAsync("ChallengeResolved", new
|
await Clients.Group(game.LobbyCode).SendAsync("ChallengeResolved", new
|
||||||
{
|
{
|
||||||
@@ -319,16 +410,19 @@ public class GameHub : Hub
|
|||||||
// Aktualisierte Handkarten an betroffene Spieler senden
|
// Aktualisierte Handkarten an betroffene Spieler senden
|
||||||
var challengingHand = await _gameService.GetPlayerHandAsync(result.ChallengingPlayerId);
|
var challengingHand = await _gameService.GetPlayerHandAsync(result.ChallengingPlayerId);
|
||||||
var targetHand = await _gameService.GetPlayerHandAsync(result.TargetPlayerId);
|
var targetHand = await _gameService.GetPlayerHandAsync(result.TargetPlayerId);
|
||||||
|
var gameState = await _gameService.GetGameStateAsync(gameId);
|
||||||
|
|
||||||
using var context = _contextFactory.CreateDbContext();
|
using var db = _contextFactory.CreateDbContext();
|
||||||
var challengingPlayer = await context.Players.FindAsync(result.ChallengingPlayerId);
|
var challengingPlayer = await db.Players.FindAsync(result.ChallengingPlayerId);
|
||||||
var targetPlayer = await context.Players.FindAsync(result.TargetPlayerId);
|
var targetPlayer = await db.Players.FindAsync(result.TargetPlayerId);
|
||||||
|
|
||||||
if (challengingPlayer?.ConnectionId != null)
|
if (challengingPlayer?.ConnectionId != null)
|
||||||
await Clients.Client(challengingPlayer.ConnectionId).SendAsync("PlayerHandUpdated", challengingHand);
|
await Clients.Client(challengingPlayer.ConnectionId).SendAsync("PlayerHandUpdated", challengingHand);
|
||||||
|
|
||||||
if (targetPlayer?.ConnectionId != null)
|
if (targetPlayer?.ConnectionId != null)
|
||||||
await Clients.Client(targetPlayer.ConnectionId).SendAsync("PlayerHandUpdated", targetHand);
|
await Clients.Client(targetPlayer.ConnectionId).SendAsync("PlayerHandUpdated", targetHand);
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("GameStateUpdated", gameState);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -354,6 +448,17 @@ public class GameHub : Hub
|
|||||||
|
|
||||||
var gameState = await _gameService.GetGameStateAsync(gameId);
|
var gameState = await _gameService.GetGameStateAsync(gameId);
|
||||||
await Clients.Caller.SendAsync("GameStateUpdated", gameState);
|
await Clients.Caller.SendAsync("GameStateUpdated", gameState);
|
||||||
|
|
||||||
|
// Abgelaufene Slips wurden beim Laden des Zustands abgeschlossen; Hand des Anfragenden mitsenden
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var playerId = await context.Players
|
||||||
|
.Where(p => p.GameId == gameId && p.UserId == userId)
|
||||||
|
.Select(p => p.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (playerId > 0)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("PlayerHandUpdated", await _gameService.GetPlayerHandAsync(playerId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -389,43 +494,11 @@ public class GameHub : Hub
|
|||||||
.Select(g => g.LobbyCode)
|
.Select(g => g.LobbyCode)
|
||||||
.FirstAsync();
|
.FirstAsync();
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, lobbyCode);
|
await Groups.AddToGroupAsync(Context.ConnectionId, lobbyCode);
|
||||||
|
|
||||||
// Der Client bekommt nach einem Reconnect keinen vollständigen Zustand
|
|
||||||
// mehr aus der Queue gespielt — er muss RequestGameState aufrufen.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
// Nicht authentifiziert — ConnectionId wird nicht gespeichert.
|
// Nicht authentifiziert — ConnectionId wird nicht gespeichert.
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to register connection for authenticated user");
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnConnectedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Client {ConnectionId} disconnected", Context.ConnectionId);
|
|
||||||
|
|
||||||
// ConnectionId zurücksetzen, damit keine Nachrichten an eine tote Verbindung gehen.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _contextFactory.CreateDbContext();
|
|
||||||
var players = await context.Players
|
|
||||||
.Where(p => p.ConnectionId == Context.ConnectionId)
|
|
||||||
.ToListAsync();
|
|
||||||
foreach (var player in players)
|
|
||||||
player.ConnectionId = null;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to clear connection id on disconnect");
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
446
SlipItIn.Server/Migrations/20261004162039_UpdateGameRules.Designer.cs
generated
Normal file
446
SlipItIn.Server/Migrations/20261004162039_UpdateGameRules.Designer.cs
generated
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SlipItInDbContext))]
|
||||||
|
[Migration("20261004162039_UpdateGameRules")]
|
||||||
|
partial class UpdateGameRules
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.12")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Game", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("HostId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("LobbyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("MaxPlayers")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RoundDurationSeconds")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WinnerPlayerId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("HostId");
|
||||||
|
|
||||||
|
b.HasIndex("LobbyCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("WinnerPlayerId");
|
||||||
|
|
||||||
|
b.ToTable("Games");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.GameRound", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("GameId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RoundNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GameId");
|
||||||
|
|
||||||
|
b.ToTable("GameRounds");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Phrase", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("CreatorId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.ToTable("Phrases");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Player", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ConnectionId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("FailedSlips")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("GameId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsReady")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("JoinedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SuccessfulSlips")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("GameId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Players");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.PlayerCard", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("AssignedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("GameRoundId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("PhraseId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("PlayerId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("SlippedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GameRoundId");
|
||||||
|
|
||||||
|
b.HasIndex("PhraseId");
|
||||||
|
|
||||||
|
b.HasIndex("PlayerId");
|
||||||
|
|
||||||
|
b.ToTable("PlayerCards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.SlipChallenge", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ChallengingPlayerId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("GameRoundId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ResolvedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("TargetCardId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TargetPlayerId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChallengingPlayerId");
|
||||||
|
|
||||||
|
b.HasIndex("GameRoundId");
|
||||||
|
|
||||||
|
b.HasIndex("TargetCardId");
|
||||||
|
|
||||||
|
b.HasIndex("TargetPlayerId");
|
||||||
|
|
||||||
|
b.ToTable("SlipChallenges");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Game", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.User", "Host")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("HostId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Player", "WinnerPlayer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WinnerPlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Host");
|
||||||
|
|
||||||
|
b.Navigation("WinnerPlayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.GameRound", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Game", "Game")
|
||||||
|
.WithMany("Rounds")
|
||||||
|
.HasForeignKey("GameId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Game");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Phrase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.User", "Creator")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Player", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Game", "Game")
|
||||||
|
.WithMany("Players")
|
||||||
|
.HasForeignKey("GameId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.User", "User")
|
||||||
|
.WithMany("Players")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Game");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.PlayerCard", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.GameRound", "GameRound")
|
||||||
|
.WithMany("ActiveCards")
|
||||||
|
.HasForeignKey("GameRoundId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Phrase", "Phrase")
|
||||||
|
.WithMany("PlayerCards")
|
||||||
|
.HasForeignKey("PhraseId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Player", "Player")
|
||||||
|
.WithMany("Cards")
|
||||||
|
.HasForeignKey("PlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("GameRound");
|
||||||
|
|
||||||
|
b.Navigation("Phrase");
|
||||||
|
|
||||||
|
b.Navigation("Player");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.SlipChallenge", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Player", "ChallengingPlayer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChallengingPlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.GameRound", "GameRound")
|
||||||
|
.WithMany("Challenges")
|
||||||
|
.HasForeignKey("GameRoundId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.PlayerCard", "TargetCard")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TargetCardId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Player", "TargetPlayer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TargetPlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ChallengingPlayer");
|
||||||
|
|
||||||
|
b.Navigation("GameRound");
|
||||||
|
|
||||||
|
b.Navigation("TargetCard");
|
||||||
|
|
||||||
|
b.Navigation("TargetPlayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Game", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Players");
|
||||||
|
|
||||||
|
b.Navigation("Rounds");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.GameRound", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ActiveCards");
|
||||||
|
|
||||||
|
b.Navigation("Challenges");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Phrase", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayerCards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.Player", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Cards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SlipItIn.Shared.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Players");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
SlipItIn.Server/Migrations/20261004162039_UpdateGameRules.cs
Normal file
78
SlipItIn.Server/Migrations/20261004162039_UpdateGameRules.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateGameRules : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "TargetCardId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "SlippedAt",
|
||||||
|
table: "PlayerCards",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WinnerPlayerId",
|
||||||
|
table: "Games",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Games_WinnerPlayerId",
|
||||||
|
table: "Games",
|
||||||
|
column: "WinnerPlayerId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Games_Players_WinnerPlayerId",
|
||||||
|
table: "Games",
|
||||||
|
column: "WinnerPlayerId",
|
||||||
|
principalTable: "Players",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Games_Players_WinnerPlayerId",
|
||||||
|
table: "Games");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Games_WinnerPlayerId",
|
||||||
|
table: "Games");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SlippedAt",
|
||||||
|
table: "PlayerCards");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WinnerPlayerId",
|
||||||
|
table: "Games");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "TargetCardId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace SlipItIn.Server.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.10")
|
.HasAnnotation("ProductVersion", "10.0.12")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -55,6 +55,9 @@ namespace SlipItIn.Server.Migrations
|
|||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WinnerPlayerId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("HostId");
|
b.HasIndex("HostId");
|
||||||
@@ -62,6 +65,8 @@ namespace SlipItIn.Server.Migrations
|
|||||||
b.HasIndex("LobbyCode")
|
b.HasIndex("LobbyCode")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("WinnerPlayerId");
|
||||||
|
|
||||||
b.ToTable("Games");
|
b.ToTable("Games");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,6 +193,9 @@ namespace SlipItIn.Server.Migrations
|
|||||||
b.Property<int>("PlayerId")
|
b.Property<int>("PlayerId")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("SlippedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("GameRoundId");
|
b.HasIndex("GameRoundId");
|
||||||
@@ -222,7 +230,7 @@ namespace SlipItIn.Server.Migrations
|
|||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<int>("TargetCardId")
|
b.Property<int?>("TargetCardId")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<int>("TargetPlayerId")
|
b.Property<int>("TargetPlayerId")
|
||||||
@@ -289,7 +297,14 @@ namespace SlipItIn.Server.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SlipItIn.Shared.Models.Player", "WinnerPlayer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WinnerPlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
b.Navigation("Host");
|
b.Navigation("Host");
|
||||||
|
|
||||||
|
b.Navigation("WinnerPlayer");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SlipItIn.Shared.Models.GameRound", b =>
|
modelBuilder.Entity("SlipItIn.Shared.Models.GameRound", b =>
|
||||||
@@ -377,8 +392,7 @@ namespace SlipItIn.Server.Migrations
|
|||||||
b.HasOne("SlipItIn.Shared.Models.PlayerCard", "TargetCard")
|
b.HasOne("SlipItIn.Shared.Models.PlayerCard", "TargetCard")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TargetCardId")
|
.HasForeignKey("TargetCardId")
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("SlipItIn.Shared.Models.Player", "TargetPlayer")
|
b.HasOne("SlipItIn.Shared.Models.Player", "TargetPlayer")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SlipItIn.Server.Data;
|
using SlipItIn.Server.Data;
|
||||||
using SlipItIn.Shared.DTOs;
|
using SlipItIn.Shared.DTOs;
|
||||||
using SlipItIn.Shared.Models;
|
using SlipItIn.Shared.Models;
|
||||||
@@ -30,18 +30,11 @@ public class GameService : IGameService
|
|||||||
Status = GameStatus.Lobby
|
Status = GameStatus.Lobby
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Host wird automatisch als erster Spieler hinzugefügt
|
|
||||||
var hostPlayer = new Player { UserId = host.Id, ConnectionId = connectionId };
|
var hostPlayer = new Player { UserId = host.Id, ConnectionId = connectionId };
|
||||||
|
|
||||||
// Spieler direkt zur Liste des Spiels hinzufügen
|
|
||||||
game.Players.Add(hostPlayer);
|
game.Players.Add(hostPlayer);
|
||||||
|
|
||||||
// Es reicht, nur das Game hinzuzufügen — EF Core fügt den Player automatisch mit hinzu
|
|
||||||
context.Games.Add(game);
|
context.Games.Add(game);
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +49,6 @@ public class GameService : IGameService
|
|||||||
|
|
||||||
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId) ?? throw new InvalidOperationException("User not found");
|
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId) ?? throw new InvalidOperationException("User not found");
|
||||||
|
|
||||||
// Prüfen ob User bereits im Spiel ist
|
|
||||||
if (game.Players.Any(p => p.UserId == userId))
|
if (game.Players.Any(p => p.UserId == userId))
|
||||||
throw new InvalidOperationException("User already in game");
|
throw new InvalidOperationException("User already in game");
|
||||||
|
|
||||||
@@ -165,13 +157,69 @@ public class GameService : IGameService
|
|||||||
if (card.GameRound.Status != RoundStatus.Active) throw new InvalidOperationException("Round is not active");
|
if (card.GameRound.Status != RoundStatus.Active) throw new InvalidOperationException("Round is not active");
|
||||||
if (card.IsUsed) throw new InvalidOperationException("Card has already been used");
|
if (card.IsUsed) throw new InvalidOperationException("Card has already been used");
|
||||||
|
|
||||||
card.IsUsed = true;
|
// Karte wird für 30 Sekunden vorgehalten
|
||||||
|
card.SlippedAt = DateTime.UtcNow;
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetCardId)
|
public async Task<bool> CompleteSlipAsync(int gameId, int cardId, bool force = false)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var card = await context.PlayerCards
|
||||||
|
.Include(pc => pc.GameRound)
|
||||||
|
.ThenInclude(gr => gr.Game)
|
||||||
|
.Include(pc => pc.Player)
|
||||||
|
.FirstOrDefaultAsync(pc => pc.Id == cardId);
|
||||||
|
|
||||||
|
if (card == null || card.IsUsed || !card.SlippedAt.HasValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (card.GameRound.GameId != gameId || card.GameRound.Status != RoundStatus.Active)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
{
|
||||||
|
var elapsed = (DateTime.UtcNow - card.SlippedAt.Value).TotalSeconds;
|
||||||
|
if (elapsed < 30)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls eine Beschuldigung gegen diesen Spieler aussteht, kann der Slip nicht abgeschlossen werden
|
||||||
|
var hasPendingChallenge = await context.SlipChallenges
|
||||||
|
.AnyAsync(c => c.GameRoundId == card.GameRoundId &&
|
||||||
|
c.TargetPlayerId == card.PlayerId &&
|
||||||
|
c.Status == ChallengeStatus.Pending);
|
||||||
|
if (hasPendingChallenge)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
card.IsUsed = true;
|
||||||
|
card.SlippedAt = null;
|
||||||
|
card.Player.SuccessfulSlips += 1;
|
||||||
|
card.Player.Score += 1;
|
||||||
|
|
||||||
|
// Prüfung: Wenn der Spieler keine ungespielten Karten mehr hat, hat er gewonnen!
|
||||||
|
var remainingCards = await context.PlayerCards
|
||||||
|
.CountAsync(c => c.PlayerId == card.PlayerId && c.GameRoundId == card.GameRoundId && !c.IsUsed && c.Id != card.Id);
|
||||||
|
|
||||||
|
if (remainingCards == 0)
|
||||||
|
{
|
||||||
|
var game = card.GameRound.Game;
|
||||||
|
game.Status = GameStatus.Completed;
|
||||||
|
game.EndedAt = DateTime.UtcNow;
|
||||||
|
game.WinnerPlayerId = card.PlayerId;
|
||||||
|
|
||||||
|
card.GameRound.Status = RoundStatus.Completed;
|
||||||
|
card.GameRound.EndedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetPlayerId)
|
||||||
{
|
{
|
||||||
using var context = _contextFactory.CreateDbContext();
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
|
||||||
@@ -184,27 +232,32 @@ public class GameService : IGameService
|
|||||||
if (!challengerBelongsToGame)
|
if (!challengerBelongsToGame)
|
||||||
throw new UnauthorizedAccessException("Challenger does not belong to this game");
|
throw new UnauthorizedAccessException("Challenger does not belong to this game");
|
||||||
|
|
||||||
var targetCard = await context.PlayerCards
|
var targetPlayer = await context.Players
|
||||||
.FirstOrDefaultAsync(pc => pc.Id == targetCardId) ?? throw new InvalidOperationException("Target card not found");
|
.FirstOrDefaultAsync(p => p.Id == targetPlayerId && p.GameId == gameId)
|
||||||
|
?? throw new InvalidOperationException("Target player not found in this game");
|
||||||
|
|
||||||
if (targetCard.GameRoundId != currentRound.Id)
|
if (targetPlayerId == challengingPlayerId)
|
||||||
throw new InvalidOperationException("Card does not belong to the active round");
|
throw new InvalidOperationException("Cannot challenge yourself");
|
||||||
if (targetCard.PlayerId == challengingPlayerId)
|
|
||||||
throw new InvalidOperationException("Cannot challenge your own card");
|
// Eventuell aktuell vorgehaltene Karte finden
|
||||||
if (!targetCard.IsUsed)
|
var slippingCard = await context.PlayerCards
|
||||||
throw new InvalidOperationException("Card has not been played yet");
|
.Where(pc => pc.PlayerId == targetPlayerId && pc.GameRoundId == currentRound.Id && pc.SlippedAt.HasValue && !pc.IsUsed)
|
||||||
|
.OrderByDescending(pc => pc.SlippedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
var pendingExists = await context.SlipChallenges
|
var pendingExists = await context.SlipChallenges
|
||||||
.AnyAsync(c => c.TargetCardId == targetCardId && c.Status == ChallengeStatus.Pending);
|
.AnyAsync(c => c.GameRoundId == currentRound.Id &&
|
||||||
|
c.TargetPlayerId == targetPlayerId &&
|
||||||
|
c.Status == ChallengeStatus.Pending);
|
||||||
if (pendingExists)
|
if (pendingExists)
|
||||||
throw new InvalidOperationException("A challenge for this card is already pending");
|
throw new InvalidOperationException("A challenge for this player is already pending");
|
||||||
|
|
||||||
var challenge = new SlipChallenge
|
var challenge = new SlipChallenge
|
||||||
{
|
{
|
||||||
GameRoundId = currentRound.Id,
|
GameRoundId = currentRound.Id,
|
||||||
ChallengingPlayerId = challengingPlayerId,
|
ChallengingPlayerId = challengingPlayerId,
|
||||||
TargetPlayerId = targetCard.PlayerId,
|
TargetPlayerId = targetPlayerId,
|
||||||
TargetCardId = targetCardId,
|
TargetCardId = slippingCard?.Id,
|
||||||
Status = ChallengeStatus.Pending
|
Status = ChallengeStatus.Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,36 +267,88 @@ public class GameService : IGameService
|
|||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved)
|
public async Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved, int? discardCardId = null)
|
||||||
{
|
{
|
||||||
using var context = _contextFactory.CreateDbContext();
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
|
||||||
var challenge = await context.SlipChallenges
|
var challenge = await context.SlipChallenges
|
||||||
.Include(sc => sc.GameRound)
|
.Include(sc => sc.GameRound)
|
||||||
.Include(sc => sc.TargetCard)
|
.ThenInclude(gr => gr.Game)
|
||||||
.Include(sc => sc.ChallengingPlayer)
|
.Include(sc => sc.ChallengingPlayer)
|
||||||
.Include(sc => sc.TargetPlayer)
|
.Include(sc => sc.TargetPlayer)
|
||||||
|
.ThenInclude(p => p.Cards)
|
||||||
.FirstOrDefaultAsync(sc => sc.Id == challengeId) ?? throw new InvalidOperationException("Challenge not found");
|
.FirstOrDefaultAsync(sc => sc.Id == challengeId) ?? throw new InvalidOperationException("Challenge not found");
|
||||||
|
|
||||||
if (challenge.Status != ChallengeStatus.Pending)
|
if (challenge.Status != ChallengeStatus.Pending)
|
||||||
throw new InvalidOperationException("Challenge has already been resolved");
|
throw new InvalidOperationException("Challenge has already been resolved");
|
||||||
|
|
||||||
// approved = Beschuldigung stimmt (Phrase war ein unberechtigtes SlipIn des Beschuldigten)
|
|
||||||
// rejected = Beschuldigung stimmt nicht (Phrase wurde regelkonform gespielt)
|
|
||||||
|
|
||||||
if (approved)
|
if (approved)
|
||||||
{
|
{
|
||||||
// Berechtigte Beschuldigung: Beschuldigter muss die Phrase an den Beschuldiger abgeben
|
// Beschuldigung angenommen:
|
||||||
challenge.TargetCard.PlayerId = challenge.ChallengingPlayerId;
|
// 1. Beschuldiger erhält Punkt
|
||||||
challenge.ChallengingPlayer.Score += 1;
|
challenge.ChallengingPlayer.Score += 1;
|
||||||
challenge.TargetPlayer.FailedSlips += 1;
|
challenge.TargetPlayer.FailedSlips += 1;
|
||||||
|
|
||||||
|
// 2. Vorgehaltene Karte zählt nicht!
|
||||||
|
var slippingCards = challenge.TargetPlayer.Cards
|
||||||
|
.Where(c => c.GameRoundId == challenge.GameRoundId && c.SlippedAt.HasValue && !c.IsUsed)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 3. Eine Karte ablegen
|
||||||
|
PlayerCard? cardToDiscard = null;
|
||||||
|
if (discardCardId.HasValue)
|
||||||
|
{
|
||||||
|
cardToDiscard = challenge.TargetPlayer.Cards
|
||||||
|
.FirstOrDefault(c => c.Id == discardCardId.Value && !c.IsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardToDiscard ??= slippingCards.FirstOrDefault()
|
||||||
|
?? challenge.TargetPlayer.Cards.FirstOrDefault(c => c.GameRoundId == challenge.GameRoundId && !c.IsUsed);
|
||||||
|
|
||||||
|
if (cardToDiscard != null)
|
||||||
|
{
|
||||||
|
cardToDiscard.IsUsed = true;
|
||||||
|
cardToDiscard.SlippedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Andere vorgehaltene Slips zurücksetzen ("zählt nicht")
|
||||||
|
foreach (var slipping in slippingCards.Where(c => c.Id != cardToDiscard?.Id))
|
||||||
|
{
|
||||||
|
slipping.SlippedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Zwei neue Karten ziehen
|
||||||
|
var heldPhraseIds = challenge.TargetPlayer.Cards.Select(c => c.PhraseId).ToHashSet();
|
||||||
|
var availablePhrases = await context.Phrases
|
||||||
|
.Where(p => p.IsActive && !heldPhraseIds.Contains(p.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (availablePhrases.Count < 2)
|
||||||
|
{
|
||||||
|
availablePhrases = await context.Phrases.Where(p => p.IsActive).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPhrases = availablePhrases
|
||||||
|
.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue))
|
||||||
|
.Take(2)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var phrase in newPhrases)
|
||||||
|
{
|
||||||
|
var newCard = new PlayerCard
|
||||||
|
{
|
||||||
|
PlayerId = challenge.TargetPlayerId,
|
||||||
|
PhraseId = phrase.Id,
|
||||||
|
GameRoundId = challenge.GameRoundId,
|
||||||
|
IsUsed = false
|
||||||
|
};
|
||||||
|
context.PlayerCards.Add(newCard);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Falsche Beschuldigung: Phrase bleibt beim Beschuldigten
|
// Anfechten: "dann wird die Beschuldigung zurückgenommen."
|
||||||
challenge.TargetPlayer.SuccessfulSlips += 1;
|
// Status wird auf Rejected gesetzt, keine Strafen.
|
||||||
challenge.TargetPlayer.Score += 1;
|
|
||||||
challenge.ChallengingPlayer.FailedSlips += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
challenge.Status = approved ? ChallengeStatus.Approved : ChallengeStatus.Rejected;
|
challenge.Status = approved ? ChallengeStatus.Approved : ChallengeStatus.Rejected;
|
||||||
@@ -257,12 +362,16 @@ public class GameService : IGameService
|
|||||||
{
|
{
|
||||||
using var context = _contextFactory.CreateDbContext();
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
await CompleteExpiredSlipsInternalAsync(context, gameId);
|
||||||
|
|
||||||
var game = await context.Games
|
var game = await context.Games
|
||||||
.Include(g => g.Players)
|
.Include(g => g.Players)
|
||||||
.ThenInclude(p => p.User)
|
.ThenInclude(p => p.User)
|
||||||
.Include(g => g.Players)
|
.Include(g => g.Players)
|
||||||
.ThenInclude(p => p.Cards)
|
.ThenInclude(p => p.Cards)
|
||||||
.Include(g => g.Rounds)
|
.Include(g => g.Rounds)
|
||||||
|
.Include(g => g.WinnerPlayer)
|
||||||
|
.ThenInclude(wp => wp!.User)
|
||||||
.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found");
|
.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found");
|
||||||
|
|
||||||
var playerInfos = game.Players.Select(p => new PlayerInfoDto
|
var playerInfos = game.Players.Select(p => new PlayerInfoDto
|
||||||
@@ -271,7 +380,7 @@ public class GameService : IGameService
|
|||||||
Username = p.User.Username,
|
Username = p.User.Username,
|
||||||
Score = p.Score,
|
Score = p.Score,
|
||||||
IsReady = p.IsReady,
|
IsReady = p.IsReady,
|
||||||
CardCount = p.Cards.Count(c => !c.IsUsed) // Nur Anzahl, nicht die Karten selbst!
|
CardCount = p.Cards.Count(c => !c.IsUsed)
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var currentRound = game.Rounds
|
var currentRound = game.Rounds
|
||||||
@@ -287,6 +396,12 @@ public class GameService : IGameService
|
|||||||
roundTimeRemaining = Math.Max(0, roundDurationSeconds - elapsedSeconds);
|
roundTimeRemaining = Math.Max(0, roundDurationSeconds - elapsedSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string? winnerUsername = game.WinnerPlayer?.User?.Username;
|
||||||
|
if (winnerUsername == null && game.WinnerPlayerId.HasValue)
|
||||||
|
{
|
||||||
|
winnerUsername = game.Players.FirstOrDefault(p => p.Id == game.WinnerPlayerId.Value)?.User?.Username;
|
||||||
|
}
|
||||||
|
|
||||||
return new GameStateDto
|
return new GameStateDto
|
||||||
{
|
{
|
||||||
GameId = game.Id,
|
GameId = game.Id,
|
||||||
@@ -295,7 +410,9 @@ public class GameService : IGameService
|
|||||||
Players = playerInfos,
|
Players = playerInfos,
|
||||||
HostUserId = game.HostId,
|
HostUserId = game.HostId,
|
||||||
CurrentRound = currentRound?.RoundNumber ?? 0,
|
CurrentRound = currentRound?.RoundNumber ?? 0,
|
||||||
RoundTimeRemaining = roundTimeRemaining
|
RoundTimeRemaining = roundTimeRemaining,
|
||||||
|
WinnerPlayerId = game.WinnerPlayerId,
|
||||||
|
WinnerUsername = winnerUsername
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,8 +420,12 @@ public class GameService : IGameService
|
|||||||
{
|
{
|
||||||
using var context = _contextFactory.CreateDbContext();
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
|
||||||
// Nur Karten der aktuell aktiven Runde zurückgeben — sonst wächst die Hand
|
var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||||
// über Runden hinweg unbegrenzt und enthält veraltete Karten.
|
if (player != null)
|
||||||
|
{
|
||||||
|
await CompleteExpiredSlipsInternalAsync(context, player.GameId);
|
||||||
|
}
|
||||||
|
|
||||||
var activeRoundIds = await context.GameRounds
|
var activeRoundIds = await context.GameRounds
|
||||||
.Where(gr => gr.Status == RoundStatus.Active &&
|
.Where(gr => gr.Status == RoundStatus.Active &&
|
||||||
gr.Game.Players.Any(p => p.Id == playerId))
|
gr.Game.Players.Any(p => p.Id == playerId))
|
||||||
@@ -314,19 +435,32 @@ public class GameService : IGameService
|
|||||||
var cards = await context.PlayerCards
|
var cards = await context.PlayerCards
|
||||||
.Include(pc => pc.Phrase)
|
.Include(pc => pc.Phrase)
|
||||||
.Where(pc => pc.PlayerId == playerId && activeRoundIds.Contains(pc.GameRoundId))
|
.Where(pc => pc.PlayerId == playerId && activeRoundIds.Contains(pc.GameRoundId))
|
||||||
.Select(pc => new PlayerCardDto
|
.ToListAsync();
|
||||||
|
|
||||||
|
var cardDtos = cards.Select(pc =>
|
||||||
|
{
|
||||||
|
int remainingHold = 0;
|
||||||
|
if (pc.SlippedAt.HasValue && !pc.IsUsed)
|
||||||
|
{
|
||||||
|
var elapsed = (int)(DateTime.UtcNow - pc.SlippedAt.Value).TotalSeconds;
|
||||||
|
remainingHold = Math.Max(0, 30 - elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayerCardDto
|
||||||
{
|
{
|
||||||
CardId = pc.Id,
|
CardId = pc.Id,
|
||||||
PhraseId = pc.PhraseId,
|
PhraseId = pc.PhraseId,
|
||||||
Text = pc.Phrase.Text,
|
Text = pc.Phrase.Text,
|
||||||
IsUsed = pc.IsUsed
|
IsUsed = pc.IsUsed,
|
||||||
})
|
SlippedAt = pc.SlippedAt,
|
||||||
.ToListAsync();
|
SlippedHoldSecondsRemaining = remainingHold
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
return new PlayerHandDto
|
return new PlayerHandDto
|
||||||
{
|
{
|
||||||
PlayerId = playerId,
|
PlayerId = playerId,
|
||||||
Cards = cards
|
Cards = cardDtos
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +471,65 @@ public class GameService : IGameService
|
|||||||
?? throw new InvalidOperationException("Game not found");
|
?? throw new InvalidOperationException("Game not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task CompleteExpiredSlipsInternalAsync(SlipItInDbContext context, int gameId)
|
||||||
|
{
|
||||||
|
var expiredThreshold = DateTime.UtcNow.AddSeconds(-30);
|
||||||
|
var candidateCards = await context.PlayerCards
|
||||||
|
.Include(pc => pc.GameRound)
|
||||||
|
.ThenInclude(gr => gr.Game)
|
||||||
|
.Include(pc => pc.Player)
|
||||||
|
.Where(pc => pc.GameRound.GameId == gameId &&
|
||||||
|
pc.GameRound.Status == RoundStatus.Active &&
|
||||||
|
!pc.IsUsed &&
|
||||||
|
pc.SlippedAt.HasValue &&
|
||||||
|
pc.SlippedAt.Value <= expiredThreshold)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (candidateCards.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pendingChallengeTargetIds = await context.SlipChallenges
|
||||||
|
.Where(sc => sc.GameRound.GameId == gameId && sc.Status == ChallengeStatus.Pending)
|
||||||
|
.Select(sc => sc.TargetPlayerId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
bool stateChanged = false;
|
||||||
|
foreach (var card in candidateCards)
|
||||||
|
{
|
||||||
|
if (pendingChallengeTargetIds.Contains(card.PlayerId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
card.IsUsed = true;
|
||||||
|
card.SlippedAt = null;
|
||||||
|
card.Player.SuccessfulSlips += 1;
|
||||||
|
card.Player.Score += 1;
|
||||||
|
stateChanged = true;
|
||||||
|
|
||||||
|
var remainingCount = await context.PlayerCards
|
||||||
|
.CountAsync(c => c.PlayerId == card.PlayerId && c.GameRoundId == card.GameRoundId && !c.IsUsed && c.Id != card.Id);
|
||||||
|
|
||||||
|
if (remainingCount == 0)
|
||||||
|
{
|
||||||
|
var game = card.GameRound.Game;
|
||||||
|
if (game.Status != GameStatus.Completed)
|
||||||
|
{
|
||||||
|
game.Status = GameStatus.Completed;
|
||||||
|
game.EndedAt = DateTime.UtcNow;
|
||||||
|
game.WinnerPlayerId = card.PlayerId;
|
||||||
|
|
||||||
|
card.GameRound.Status = RoundStatus.Completed;
|
||||||
|
card.GameRound.EndedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateChanged)
|
||||||
|
{
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string GenerateLobbyCode()
|
private static string GenerateLobbyCode()
|
||||||
{
|
{
|
||||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ public interface IGameService
|
|||||||
Task<Game> SetPlayerReadyAsync(int gameId, int playerId);
|
Task<Game> SetPlayerReadyAsync(int gameId, int playerId);
|
||||||
Task<List<PlayerCard>> DealCardsAsync(int gameId);
|
Task<List<PlayerCard>> DealCardsAsync(int gameId);
|
||||||
Task<bool> SubmitSlipAsync(int gameId, int playerId, int cardId);
|
Task<bool> SubmitSlipAsync(int gameId, int playerId, int cardId);
|
||||||
Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetCardId);
|
Task<bool> CompleteSlipAsync(int gameId, int cardId, bool force = false);
|
||||||
Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved);
|
Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetPlayerId);
|
||||||
|
Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved, int? discardCardId = null);
|
||||||
Task<GameStateDto> GetGameStateAsync(int gameId);
|
Task<GameStateDto> GetGameStateAsync(int gameId);
|
||||||
Task<PlayerHandDto> GetPlayerHandAsync(int playerId);
|
Task<PlayerHandDto> GetPlayerHandAsync(int playerId);
|
||||||
Task<Game> GetGameAsync(int gameId);
|
Task<Game> GetGameAsync(int gameId);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public class GameStateDto
|
|||||||
public int HostUserId { get; set; }
|
public int HostUserId { get; set; }
|
||||||
public int CurrentRound { get; set; }
|
public int CurrentRound { get; set; }
|
||||||
public int RoundTimeRemaining { get; set; }
|
public int RoundTimeRemaining { get; set; }
|
||||||
|
public int? WinnerPlayerId { get; set; }
|
||||||
|
public string? WinnerUsername { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PlayerInfoDto
|
public class PlayerInfoDto
|
||||||
@@ -29,12 +31,55 @@ public class PlayerHandDto
|
|||||||
public List<PlayerCardDto> Cards { get; set; } = [];
|
public List<PlayerCardDto> Cards { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PlayerCardDto
|
public class PlayerCardDto : System.ComponentModel.INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
|
private bool _isUsed;
|
||||||
|
private DateTime? _slippedAt;
|
||||||
|
private int _slippedHoldSecondsRemaining;
|
||||||
|
|
||||||
|
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
public int CardId { get; set; }
|
public int CardId { get; set; }
|
||||||
public int PhraseId { get; set; }
|
public int PhraseId { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
public bool IsUsed { get; set; }
|
|
||||||
|
public bool IsUsed
|
||||||
|
{
|
||||||
|
get => _isUsed;
|
||||||
|
set { _isUsed = value; NotifyStateChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime? SlippedAt
|
||||||
|
{
|
||||||
|
get => _slippedAt;
|
||||||
|
set { _slippedAt = value; NotifyStateChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SlippedHoldSecondsRemaining
|
||||||
|
{
|
||||||
|
get => _slippedHoldSecondsRemaining;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_slippedHoldSecondsRemaining = value;
|
||||||
|
Raise(nameof(SlippedHoldSecondsRemaining));
|
||||||
|
Raise(nameof(SlipButtonText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPendingSlip => SlippedAt.HasValue && !IsUsed;
|
||||||
|
public bool CanSlip => !IsPendingSlip && !IsUsed;
|
||||||
|
public string SlipButtonText => IsPendingSlip ? $"Vorgehalten ({SlippedHoldSecondsRemaining}s)" : "Slip";
|
||||||
|
|
||||||
|
private void NotifyStateChanged()
|
||||||
|
{
|
||||||
|
Raise(nameof(IsUsed));
|
||||||
|
Raise(nameof(SlippedAt));
|
||||||
|
Raise(nameof(IsPendingSlip));
|
||||||
|
Raise(nameof(CanSlip));
|
||||||
|
Raise(nameof(SlipButtonText));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Raise(string name) => PropertyChanged?.Invoke(this, new(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SlipChallengeDto
|
public class SlipChallengeDto
|
||||||
@@ -43,7 +88,8 @@ public class SlipChallengeDto
|
|||||||
public int ChallengingPlayerId { get; set; }
|
public int ChallengingPlayerId { get; set; }
|
||||||
public string ChallengingPlayerName { get; set; } = string.Empty;
|
public string ChallengingPlayerName { get; set; } = string.Empty;
|
||||||
public int TargetPlayerId { get; set; }
|
public int TargetPlayerId { get; set; }
|
||||||
public int TargetCardId { get; set; }
|
public string TargetPlayerName { get; set; } = string.Empty;
|
||||||
|
public int? TargetCardId { get; set; }
|
||||||
public string Status { get; set; } = string.Empty;
|
public string Status { get; set; } = string.Empty;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ public class Game
|
|||||||
public DateTime? EndedAt { get; set; }
|
public DateTime? EndedAt { get; set; }
|
||||||
public int MaxPlayers { get; set; } = 8;
|
public int MaxPlayers { get; set; } = 8;
|
||||||
public int RoundDurationSeconds { get; set; } = 45;
|
public int RoundDurationSeconds { get; set; } = 45;
|
||||||
|
public int? WinnerPlayerId { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public User Host { get; set; } = null!;
|
public User Host { get; set; } = null!;
|
||||||
|
public Player? WinnerPlayer { get; set; }
|
||||||
public ICollection<Player> Players { get; set; } = [];
|
public ICollection<Player> Players { get; set; } = [];
|
||||||
public ICollection<GameRound> Rounds { get; set; } = [];
|
public ICollection<GameRound> Rounds { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public class PlayerCard
|
|||||||
public int PhraseId { get; set; }
|
public int PhraseId { get; set; }
|
||||||
public int GameRoundId { get; set; }
|
public int GameRoundId { get; set; }
|
||||||
public bool IsUsed { get; set; } = false;
|
public bool IsUsed { get; set; } = false;
|
||||||
|
public DateTime? SlippedAt { get; set; }
|
||||||
public DateTime AssignedAt { get; set; } = DateTime.UtcNow;
|
public DateTime AssignedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ namespace SlipItIn.Shared.Models;
|
|||||||
public enum ChallengeStatus
|
public enum ChallengeStatus
|
||||||
{
|
{
|
||||||
Pending, // Beschuldigung eingegangen, wartet auf Bestätigung/Ablehnung
|
Pending, // Beschuldigung eingegangen, wartet auf Bestätigung/Ablehnung
|
||||||
Approved, // Beschuldigung war berechtigt → Phrase bleibt beim Beschuldiger
|
Approved, // Beschuldigung war berechtigt -> Beschuldigter muss Karte ablegen und 2 neue ziehen
|
||||||
Rejected // Beschuldigung falsch → Beschuldiger erhält die Phrase
|
Rejected // Beschuldigung angefochten -> Beschuldigung zurückgenommen
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SlipChallenge
|
public class SlipChallenge
|
||||||
@@ -13,7 +13,7 @@ public class SlipChallenge
|
|||||||
public int GameRoundId { get; set; }
|
public int GameRoundId { get; set; }
|
||||||
public int ChallengingPlayerId { get; set; } // Wer beschuldigt
|
public int ChallengingPlayerId { get; set; } // Wer beschuldigt
|
||||||
public int TargetPlayerId { get; set; } // Wer beschuldigt wird
|
public int TargetPlayerId { get; set; } // Wer beschuldigt wird
|
||||||
public int TargetCardId { get; set; } // Welche Phrase wird als falsch behauptet
|
public int? TargetCardId { get; set; } // Optional: Angefochtene Karte falls bekannt/zugeordnet
|
||||||
public ChallengeStatus Status { get; set; } = ChallengeStatus.Pending;
|
public ChallengeStatus Status { get; set; } = ChallengeStatus.Pending;
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime? ResolvedAt { get; set; }
|
public DateTime? ResolvedAt { get; set; }
|
||||||
@@ -22,5 +22,5 @@ public class SlipChallenge
|
|||||||
public GameRound GameRound { get; set; } = null!;
|
public GameRound GameRound { get; set; } = null!;
|
||||||
public Player ChallengingPlayer { get; set; } = null!; // Der Beschuldiger
|
public Player ChallengingPlayer { get; set; } = null!; // Der Beschuldiger
|
||||||
public Player TargetPlayer { get; set; } = null!; // Der Beschuldigte
|
public Player TargetPlayer { get; set; } = null!; // Der Beschuldigte
|
||||||
public PlayerCard TargetCard { get; set; } = null!; // Die angefochtene Phrase
|
public PlayerCard? TargetCard { get; set; } // Die angefochtene Phrase (optional)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ public interface ISignalRService
|
|||||||
Task PlayerReadyAsync(int gameId, int playerId);
|
Task PlayerReadyAsync(int gameId, int playerId);
|
||||||
Task StartGameAsync(int gameId);
|
Task StartGameAsync(int gameId);
|
||||||
Task SubmitSlipAsync(int gameId, int playerId, int cardId);
|
Task SubmitSlipAsync(int gameId, int playerId, int cardId);
|
||||||
|
Task ChallengePlayerAsync(int gameId, int challengingPlayerId, int targetPlayerId);
|
||||||
Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId);
|
Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId);
|
||||||
Task ResolveChallengeAsync(int challengeId, bool approved);
|
Task ResolveChallengeAsync(int challengeId, bool approved, int? discardCardId = null);
|
||||||
Task RequestGameStateAsync(int gameId);
|
Task RequestGameStateAsync(int gameId);
|
||||||
|
|
||||||
Task SendQueuedActionAsync(QueuedGameAction action);
|
Task SendQueuedActionAsync(QueuedGameAction action);
|
||||||
|
|||||||
@@ -120,11 +120,14 @@ public class SignalRService : ISignalRService, IAsyncDisposable
|
|||||||
public Task SubmitSlipAsync(int gameId, int playerId, int cardId)
|
public Task SubmitSlipAsync(int gameId, int playerId, int cardId)
|
||||||
=> InvokeAsync("SubmitSlip", gameId, playerId, cardId);
|
=> InvokeAsync("SubmitSlip", gameId, playerId, cardId);
|
||||||
|
|
||||||
|
public Task ChallengePlayerAsync(int gameId, int challengingPlayerId, int targetPlayerId)
|
||||||
|
=> InvokeAsync("ChallengePlayer", gameId, challengingPlayerId, targetPlayerId);
|
||||||
|
|
||||||
public Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId)
|
public Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId)
|
||||||
=> InvokeAsync("ChallengeSlip", gameId, challengingPlayerId, targetCardId);
|
=> InvokeAsync("ChallengeSlip", gameId, challengingPlayerId, targetCardId);
|
||||||
|
|
||||||
public Task ResolveChallengeAsync(int challengeId, bool approved)
|
public Task ResolveChallengeAsync(int challengeId, bool approved, int? discardCardId = null)
|
||||||
=> InvokeAsync("ResolveChallenge", challengeId, approved);
|
=> InvokeAsync("ResolveChallenge", challengeId, approved, discardCardId);
|
||||||
|
|
||||||
public Task RequestGameStateAsync(int gameId) => InvokeAsync("RequestGameState", gameId);
|
public Task RequestGameStateAsync(int gameId) => InvokeAsync("RequestGameState", gameId);
|
||||||
|
|
||||||
@@ -249,9 +252,7 @@ public class SignalRService : ISignalRService, IAsyncDisposable
|
|||||||
return element.ValueKind switch
|
return element.ValueKind switch
|
||||||
{
|
{
|
||||||
JsonValueKind.String => element.GetString(),
|
JsonValueKind.String => element.GetString(),
|
||||||
JsonValueKind.Number when element.TryGetInt32(out var i) => i,
|
JsonValueKind.Number => element.TryGetInt32(out var i) ? i : element.GetDouble(),
|
||||||
JsonValueKind.Number when element.TryGetInt64(out var l) => l,
|
|
||||||
JsonValueKind.Number when element.TryGetDouble(out var d) => d,
|
|
||||||
JsonValueKind.True => true,
|
JsonValueKind.True => true,
|
||||||
JsonValueKind.False => false,
|
JsonValueKind.False => false,
|
||||||
JsonValueKind.Null => null,
|
JsonValueKind.Null => null,
|
||||||
@@ -262,6 +263,20 @@ public class SignalRService : ISignalRService, IAsyncDisposable
|
|||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_hubConnection is not null)
|
if (_hubConnection is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
await _hubConnection.DisposeAsync();
|
await _hubConnection.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort
|
||||||
|
}
|
||||||
|
|
||||||
|
_hubConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectLock.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
private partial int GameId { get; set; }
|
private partial int GameId { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private partial int CurrentRound { get; set; }
|
public partial int CurrentRound { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private partial int RoundTimeRemaining { get; set; }
|
public partial int RoundTimeRemaining { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private partial bool IsConnected { get; set; }
|
public partial bool IsConnected { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private partial bool IsSyncing { get; set; }
|
public partial bool IsSyncing { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsChallengeVisible { get; set; }
|
public partial bool IsChallengeVisible { get; set; }
|
||||||
@@ -49,6 +49,18 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial int PendingSyncActions { get; set; }
|
public partial int PendingSyncActions { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsGameOver { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string GameOverMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial PlayerInfoDto? SelectedOtherPlayer { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial PlayerCardDto? SelectedCardToDiscard { get; set; }
|
||||||
|
|
||||||
public ObservableCollection<PlayerCardDto> MyCards { get; } = [];
|
public ObservableCollection<PlayerCardDto> MyCards { get; } = [];
|
||||||
public ObservableCollection<PlayerInfoDto> OtherPlayers { get; } = [];
|
public ObservableCollection<PlayerInfoDto> OtherPlayers { get; } = [];
|
||||||
|
|
||||||
@@ -71,9 +83,8 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
|
|
||||||
private void OnRoundTimerTick(object? sender, EventArgs e)
|
private void OnRoundTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (RoundTimeRemaining <= 0)
|
if (RoundTimeRemaining > 0)
|
||||||
return;
|
{
|
||||||
|
|
||||||
var elapsed = (int)(DateTime.UtcNow - _lastStateUtc).TotalSeconds;
|
var elapsed = (int)(DateTime.UtcNow - _lastStateUtc).TotalSeconds;
|
||||||
if (elapsed > 0)
|
if (elapsed > 0)
|
||||||
{
|
{
|
||||||
@@ -82,6 +93,37 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 30-Sekunden Countdown für vorgehaltene Slips aktualisieren
|
||||||
|
foreach (var card in MyCards)
|
||||||
|
{
|
||||||
|
if (card.SlippedAt.HasValue && !card.IsUsed)
|
||||||
|
{
|
||||||
|
var elapsedSlip = (int)(DateTime.UtcNow - card.SlippedAt.Value).TotalSeconds;
|
||||||
|
var remaining = Math.Max(0, 30 - elapsedSlip);
|
||||||
|
if (card.SlippedHoldSecondsRemaining != remaining)
|
||||||
|
{
|
||||||
|
card.SlippedHoldSecondsRemaining = remaining;
|
||||||
|
// Zeit abgelaufen: Server-Zustand und Hand neu laden, damit die Karte abgelegt erscheint
|
||||||
|
if (remaining == 0 && _signalR.IsConnected && GameId > 0)
|
||||||
|
_ = RefreshAfterHoldAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshAfterHoldAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
await _signalR.RequestGameStateAsync(GameId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Nächster Timer-Tick/Refresh versucht es erneut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task RefreshStateAsync()
|
private Task RefreshStateAsync()
|
||||||
{
|
{
|
||||||
@@ -94,7 +136,7 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SubmitSlipAsync(PlayerCardDto? card)
|
private async Task SubmitSlipAsync(PlayerCardDto? card)
|
||||||
{
|
{
|
||||||
if (card is null)
|
if (card is null || IsGameOver || card.IsPendingSlip)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await RunSafeAsync(async () =>
|
await RunSafeAsync(async () =>
|
||||||
@@ -103,6 +145,10 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
if (playerId <= 0 || GameId <= 0)
|
if (playerId <= 0 || GameId <= 0)
|
||||||
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
||||||
|
|
||||||
|
// Lokales Feedback sofort setzen: 30s Vorhaltezeit
|
||||||
|
card.SlippedAt = DateTime.UtcNow;
|
||||||
|
card.SlippedHoldSecondsRemaining = 30;
|
||||||
|
|
||||||
if (_signalR.IsConnected)
|
if (_signalR.IsConnected)
|
||||||
{
|
{
|
||||||
await _signalR.SubmitSlipAsync(GameId, playerId, card.CardId);
|
await _signalR.SubmitSlipAsync(GameId, playerId, card.CardId);
|
||||||
@@ -116,9 +162,9 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ChallengeCardAsync(PlayerCardDto? card)
|
private async Task ChallengePlayerAsync(PlayerInfoDto? targetPlayer)
|
||||||
{
|
{
|
||||||
if (card is null)
|
if (targetPlayer is null || IsGameOver)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await RunSafeAsync(async () =>
|
await RunSafeAsync(async () =>
|
||||||
@@ -129,16 +175,24 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
|
|
||||||
if (_signalR.IsConnected)
|
if (_signalR.IsConnected)
|
||||||
{
|
{
|
||||||
await _signalR.ChallengeSlipAsync(GameId, playerId, card.CardId);
|
await _signalR.ChallengePlayerAsync(GameId, playerId, targetPlayer.PlayerId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _gameStateService.EnqueueActionAsync("ChallengeSlip", GameId, playerId, card.CardId);
|
await _gameStateService.EnqueueActionAsync("ChallengePlayer", GameId, playerId, targetPlayer.PlayerId);
|
||||||
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusMessage = $"Beschuldigung gegen {targetPlayer.Username} eingereicht!";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task ChallengeSelectedPlayerAsync()
|
||||||
|
{
|
||||||
|
return ChallengePlayerAsync(SelectedOtherPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ApproveChallengeAsync()
|
private async Task ApproveChallengeAsync()
|
||||||
{
|
{
|
||||||
@@ -146,18 +200,21 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var challengeId = PendingChallenge.ChallengeId;
|
var challengeId = PendingChallenge.ChallengeId;
|
||||||
|
var discardId = SelectedCardToDiscard?.CardId;
|
||||||
|
|
||||||
PendingChallenge = null;
|
PendingChallenge = null;
|
||||||
IsChallengeVisible = false;
|
IsChallengeVisible = false;
|
||||||
|
SelectedCardToDiscard = null;
|
||||||
|
|
||||||
await RunSafeAsync(async () =>
|
await RunSafeAsync(async () =>
|
||||||
{
|
{
|
||||||
if (_signalR.IsConnected)
|
if (_signalR.IsConnected)
|
||||||
{
|
{
|
||||||
await _signalR.ResolveChallengeAsync(challengeId, true);
|
await _signalR.ResolveChallengeAsync(challengeId, true, discardId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _gameStateService.EnqueueActionAsync("ResolveChallenge", challengeId, true);
|
await _gameStateService.EnqueueActionAsync("ResolveChallenge", challengeId, true, discardId);
|
||||||
StatusMessage = "Offline: Antwort wurde zwischengespeichert.";
|
StatusMessage = "Offline: Antwort wurde zwischengespeichert.";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -172,6 +229,7 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
var challengeId = PendingChallenge.ChallengeId;
|
var challengeId = PendingChallenge.ChallengeId;
|
||||||
PendingChallenge = null;
|
PendingChallenge = null;
|
||||||
IsChallengeVisible = false;
|
IsChallengeVisible = false;
|
||||||
|
SelectedCardToDiscard = null;
|
||||||
|
|
||||||
await RunSafeAsync(async () =>
|
await RunSafeAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -196,6 +254,19 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
RoundTimeRemaining = message.Value.RoundTimeRemaining;
|
RoundTimeRemaining = message.Value.RoundTimeRemaining;
|
||||||
_lastStateUtc = DateTime.UtcNow;
|
_lastStateUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (string.Equals(message.Value.Status, "Completed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
IsGameOver = true;
|
||||||
|
GameOverMessage = !string.IsNullOrEmpty(message.Value.WinnerUsername)
|
||||||
|
? $"🎉 {message.Value.WinnerUsername} hat alle Phrasen abgelegt und gewonnen! 🎉"
|
||||||
|
: "🎉 Das Spiel ist beendet! 🎉";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsGameOver = false;
|
||||||
|
GameOverMessage = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
var me = _authSession.CurrentUser?.Username;
|
var me = _authSession.CurrentUser?.Username;
|
||||||
var others = message.Value.Players
|
var others = message.Value.Players
|
||||||
.Where(p => !string.Equals(p.Username, me, StringComparison.OrdinalIgnoreCase))
|
.Where(p => !string.Equals(p.Username, me, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -204,7 +275,18 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
OtherPlayers.Clear();
|
OtherPlayers.Clear();
|
||||||
foreach (var player in others)
|
foreach (var player in others)
|
||||||
OtherPlayers.Add(player);
|
OtherPlayers.Add(player);
|
||||||
|
|
||||||
|
if (SelectedOtherPlayer != null && !otherPlayersExist(SelectedOtherPlayer.PlayerId))
|
||||||
|
{
|
||||||
|
SelectedOtherPlayer = OtherPlayers.FirstOrDefault();
|
||||||
|
}
|
||||||
|
else if (SelectedOtherPlayer == null && OtherPlayers.Count > 0)
|
||||||
|
{
|
||||||
|
SelectedOtherPlayer = OtherPlayers.FirstOrDefault();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool otherPlayersExist(int id) => message.Value.Players.Any(p => p.PlayerId == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(PlayerHandChangedMessage message)
|
public void Receive(PlayerHandChangedMessage message)
|
||||||
@@ -213,7 +295,14 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
{
|
{
|
||||||
MyCards.Clear();
|
MyCards.Clear();
|
||||||
foreach (var card in message.Value.Cards.Where(card => !card.IsUsed))
|
foreach (var card in message.Value.Cards.Where(card => !card.IsUsed))
|
||||||
|
{
|
||||||
MyCards.Add(card);
|
MyCards.Add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectedCardToDiscard == null && MyCards.Count > 0)
|
||||||
|
{
|
||||||
|
SelectedCardToDiscard = MyCards.FirstOrDefault(c => c.IsPendingSlip) ?? MyCards.FirstOrDefault();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +311,7 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
MainThread.BeginInvokeOnMainThread(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
PendingChallenge = message.Value;
|
PendingChallenge = message.Value;
|
||||||
|
SelectedCardToDiscard = MyCards.FirstOrDefault(c => c.IsPendingSlip) ?? MyCards.FirstOrDefault();
|
||||||
IsChallengeVisible = true;
|
IsChallengeVisible = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -233,12 +323,12 @@ public partial class GameBoardViewModel : BaseViewModel,
|
|||||||
|
|
||||||
public void Receive(SyncStateChangedMessage message)
|
public void Receive(SyncStateChangedMessage message)
|
||||||
{
|
{
|
||||||
MainThread.BeginInvokeOnMainThread((Action)(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
IsSyncing = message.Value.IsSyncing;
|
IsSyncing = message.Value.IsSyncing;
|
||||||
SyncStatusText = message.Value.StatusText;
|
SyncStatusText = message.Value.StatusText;
|
||||||
PendingSyncActions = message.Value.PendingActions;
|
PendingSyncActions = message.Value.PendingActions;
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(ConnectionStateChangedMessage message)
|
public void Receive(ConnectionStateChangedMessage message)
|
||||||
|
|||||||
@@ -6,14 +6,32 @@
|
|||||||
x:DataType="vm:GameBoardViewModel"
|
x:DataType="vm:GameBoardViewModel"
|
||||||
IsVisible="{Binding IsChallengeVisible}">
|
IsVisible="{Binding IsChallengeVisible}">
|
||||||
<Grid BackgroundColor="#88000000" Padding="24">
|
<Grid BackgroundColor="#88000000" Padding="24">
|
||||||
<Border VerticalOptions="Center" HorizontalOptions="Center" Stroke="LightGray" StrokeShape="RoundRectangle 12" BackgroundColor="White" Padding="16">
|
<Border VerticalOptions="Center" HorizontalOptions="Center" Stroke="LightGray" StrokeShape="RoundRectangle 12" BackgroundColor="White" Padding="20" WidthRequest="340">
|
||||||
<VerticalStackLayout Spacing="10">
|
<VerticalStackLayout Spacing="12">
|
||||||
<Label Text="Du wurdest beschuldigt" FontAttributes="Bold" />
|
<Label Text="⚠️ Du wurdest beschuldigt!" FontSize="18" FontAttributes="Bold" TextColor="#D32F2F" HorizontalTextAlignment="Center" />
|
||||||
<Label Text="{Binding PendingChallenge.ChallengeId, StringFormat='ChallengeId: {0}'}" />
|
<Label Text="{Binding PendingChallenge.ChallengingPlayerName, StringFormat='{0} behauptet, du hast eine geheime Phrase platziert!'}"
|
||||||
<HorizontalStackLayout>
|
HorizontalTextAlignment="Center" FontSize="14" />
|
||||||
<Button Text="Bestätigen" Command="{Binding ApproveChallengeCommand}" />
|
|
||||||
<Button Text="Ablehnen" Command="{Binding RejectChallengeCommand}" />
|
<Label Text="Karte zum Ablegen wählen (falls du annimmst):" FontAttributes="Bold" FontSize="13" Margin="0,6,0,0" />
|
||||||
</HorizontalStackLayout>
|
<Picker ItemsSource="{Binding MyCards}"
|
||||||
|
ItemDisplayBinding="{Binding Text}"
|
||||||
|
SelectedItem="{Binding SelectedCardToDiscard}"
|
||||||
|
Title="Karte auswählen" />
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="8" Margin="0,10,0,0">
|
||||||
|
<Button Text="Beschuldigung annehmen"
|
||||||
|
Command="{Binding ApproveChallengeCommand}"
|
||||||
|
BackgroundColor="#388E3C"
|
||||||
|
TextColor="White" />
|
||||||
|
<Label Text="Du legst 1 Karte ab und ziehst 2 neue." FontSize="11" TextColor="Gray" HorizontalTextAlignment="Center" />
|
||||||
|
|
||||||
|
<Button Text="Beschuldigung anfechten"
|
||||||
|
Command="{Binding RejectChallengeCommand}"
|
||||||
|
BackgroundColor="#E53935"
|
||||||
|
TextColor="White"
|
||||||
|
Margin="0,8,0,0" />
|
||||||
|
<Label Text="Beschuldigung wird zurückgenommen." FontSize="11" TextColor="Gray" HorizontalTextAlignment="Center" />
|
||||||
|
</VerticalStackLayout>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,33 +2,47 @@
|
|||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:views="clr-namespace:SlipItIn.Views"
|
xmlns:views="clr-namespace:SlipItIn.Views"
|
||||||
|
xmlns:vm="clr-namespace:SlipItIn.ViewModels"
|
||||||
|
xmlns:dto="clr-namespace:SlipItIn.Shared.DTOs;assembly=SlipItIn.Shared"
|
||||||
x:Class="SlipItIn.Views.GameBoardPage"
|
x:Class="SlipItIn.Views.GameBoardPage"
|
||||||
|
x:DataType="vm:GameBoardViewModel"
|
||||||
Title="Game Board">
|
Title="Game Board">
|
||||||
<Grid>
|
<Grid>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<VerticalStackLayout Padding="24" Spacing="12">
|
<VerticalStackLayout Padding="24" Spacing="12">
|
||||||
<Label Text="Spielbrett" FontSize="24" FontAttributes="Bold" />
|
<Label Text="Spielbrett" FontSize="24" FontAttributes="Bold" />
|
||||||
|
|
||||||
|
<!-- Gewinner / Spielende Banner -->
|
||||||
|
<Border BackgroundColor="#E8F5E9" Stroke="#4CAF50" StrokeThickness="2" StrokeShape="RoundRectangle 12" Padding="16" IsVisible="{Binding IsGameOver}">
|
||||||
|
<VerticalStackLayout Spacing="6">
|
||||||
|
<Label Text="🏆 SPIEL VORBEI 🏆" FontSize="20" FontAttributes="Bold" TextColor="#2E7D32" HorizontalTextAlignment="Center" />
|
||||||
|
<Label Text="{Binding GameOverMessage}" TextColor="#2E7D32" FontAttributes="Bold" FontSize="16" HorizontalTextAlignment="Center" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Label Text="{Binding CurrentRound, StringFormat='Runde: {0}'}" />
|
<Label Text="{Binding CurrentRound, StringFormat='Runde: {0}'}" />
|
||||||
<Label Text="{Binding RoundTimeRemaining, StringFormat='Verbleibende Zeit: {0}s'}" />
|
|
||||||
<Label Text="{Binding IsConnected, StringFormat='Verbindung: {0}'}" TextColor="Gray" />
|
<Label Text="{Binding IsConnected, StringFormat='Verbindung: {0}'}" TextColor="Gray" />
|
||||||
<Label Text="{Binding SyncStatusText}" TextColor="DodgerBlue" IsVisible="{Binding IsSyncing}" />
|
<Label Text="{Binding SyncStatusText}" TextColor="DodgerBlue" IsVisible="{Binding IsSyncing}" />
|
||||||
<Label Text="{Binding PendingSyncActions, StringFormat='Offline-Aktionen: {0}'}" TextColor="Gray" />
|
<Label Text="{Binding PendingSyncActions, StringFormat='Offline-Aktionen: {0}'}" TextColor="Gray" />
|
||||||
|
|
||||||
<Button Text="Status aktualisieren" Command="{Binding RefreshStateCommand}" />
|
<Button Text="Status aktualisieren" Command="{Binding RefreshStateCommand}" />
|
||||||
|
|
||||||
<Label Text="Meine Phrasen" FontAttributes="Bold" />
|
<!-- Eigene Handkarten -->
|
||||||
|
<Label Text="Meine Phrasen" FontAttributes="Bold" FontSize="18" />
|
||||||
<CollectionView ItemsSource="{Binding MyCards}">
|
<CollectionView ItemsSource="{Binding MyCards}">
|
||||||
<CollectionView.EmptyView>
|
<CollectionView.EmptyView>
|
||||||
<Label Text="Keine ungespielten Phrasen mehr." />
|
<Label Text="Keine ungespielten Phrasen mehr." FontAttributes="Italic" />
|
||||||
</CollectionView.EmptyView>
|
</CollectionView.EmptyView>
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="dto:PlayerCardDto">
|
||||||
<Border Margin="0,4" Padding="10" Stroke="LightGray" StrokeThickness="1" StrokeShape="RoundRectangle 10" BackgroundColor="White">
|
<Border Margin="0,4" Padding="12" Stroke="LightGray" StrokeThickness="1" StrokeShape="RoundRectangle 10" BackgroundColor="White">
|
||||||
<VerticalStackLayout>
|
<VerticalStackLayout Spacing="8">
|
||||||
<Label Text="{Binding Text}" />
|
<Label Text="{Binding Text}" FontSize="15" />
|
||||||
<HorizontalStackLayout>
|
<Label Text="Wird für 30s vorgehalten." TextColor="#F57C00" FontAttributes="Italic" IsVisible="{Binding IsPendingSlip}" />
|
||||||
<Button Text="Slip" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.SubmitSlipCommand}" CommandParameter="{Binding .}" />
|
<HorizontalStackLayout Spacing="8">
|
||||||
<Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" />
|
<Button Text="{Binding SlipButtonText}" IsEnabled="{Binding CanSlip}"
|
||||||
|
Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.SubmitSlipCommand}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
</HorizontalStackLayout>
|
</HorizontalStackLayout>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -36,11 +50,30 @@
|
|||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
|
|
||||||
<Label Text="Andere Spieler" FontAttributes="Bold" />
|
<!-- Andere Spieler & Beschuldigen-Sektion -->
|
||||||
|
<Label Text="Andere Spieler & Beschuldigen" FontAttributes="Bold" FontSize="18" Margin="0,12,0,0" />
|
||||||
|
|
||||||
|
<!-- Liste aller anderen Spieler mit Schnell-Beschuldigen -->
|
||||||
<CollectionView ItemsSource="{Binding OtherPlayers}">
|
<CollectionView ItemsSource="{Binding OtherPlayers}">
|
||||||
|
<CollectionView.EmptyView>
|
||||||
|
<Label Text="Keine anderen Spieler in der Lobby." FontAttributes="Italic" />
|
||||||
|
</CollectionView.EmptyView>
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="dto:PlayerInfoDto">
|
||||||
<Label Text="{Binding Username}" />
|
<Border Margin="0,4" Padding="10" Stroke="LightGray" StrokeThickness="1" StrokeShape="RoundRectangle 8" BackgroundColor="White">
|
||||||
|
<Grid ColumnDefinitions="*, Auto" VerticalOptions="Center">
|
||||||
|
<VerticalStackLayout Grid.Column="0">
|
||||||
|
<Label Text="{Binding Username}" FontAttributes="Bold" FontSize="15" />
|
||||||
|
<Label Text="{Binding CardCount, StringFormat='Verbleibende Karten: {0}'}" FontSize="12" TextColor="Gray" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="Beschuldigen"
|
||||||
|
Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengePlayerCommand}"
|
||||||
|
CommandParameter="{Binding .}"
|
||||||
|
BackgroundColor="#D32F2F"
|
||||||
|
TextColor="White" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
|
|||||||
Reference in New Issue
Block a user