using Microsoft.EntityFrameworkCore; using SlipItIn.Server.Data; using SlipItIn.Shared.DTOs; using SlipItIn.Shared.Models; using System.Security.Cryptography; namespace SlipItIn.Server.Services; public class GameService : IGameService { private readonly IDbContextFactory _contextFactory; private readonly ILogger _logger; public GameService(IDbContextFactory contextFactory, ILogger logger) { _contextFactory = contextFactory; _logger = logger; } public async Task CreateGameAsync(int hostUserId, string? connectionId = null) { using var context = _contextFactory.CreateDbContext(); var host = await context.Users.FirstOrDefaultAsync(u => u.Id == hostUserId) ?? throw new InvalidOperationException("Host not found"); var lobbyCode = await GenerateUniqueLobbyCodeAsync(context); var game = new Game { LobbyCode = lobbyCode, HostId = host.Id, Status = GameStatus.Lobby }; var hostPlayer = new Player { UserId = host.Id, ConnectionId = connectionId }; game.Players.Add(hostPlayer); context.Games.Add(game); await context.SaveChangesAsync(); return game; } public async Task JoinGameAsync(string lobbyCode, int userId, string? connectionId = null) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games .Include(g => g.Players) .FirstOrDefaultAsync(g => g.LobbyCode == lobbyCode && g.Status == GameStatus.Lobby) ?? throw new InvalidOperationException("Game not found or already started"); if (game.Players.Count >= game.MaxPlayers) throw new InvalidOperationException("Game is full"); var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId) ?? throw new InvalidOperationException("User not found"); if (game.Players.Any(p => p.UserId == userId)) throw new InvalidOperationException("User already in game"); var player = new Player { UserId = user.Id, GameId = game.Id, ConnectionId = connectionId }; context.Players.Add(player); await context.SaveChangesAsync(); return game; } public async Task StartGameAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games .Include(g => g.Players) .FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.Lobby) throw new InvalidOperationException("Game has already started or ended"); if (game.Players.Count < 2) throw new InvalidOperationException("At least 2 players are required to start the game"); if (game.Players.Any(player => !player.IsReady)) throw new InvalidOperationException("All players must be ready to start the game"); game.Status = GameStatus.InProgress; game.StartedAt = DateTime.UtcNow; var round = new GameRound { GameId = gameId, RoundNumber = 1 }; context.GameRounds.Add(round); await context.SaveChangesAsync(); return game; } public async Task SetPlayerReadyAsync(int gameId, int playerId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.Lobby) throw new InvalidOperationException("Game is not in lobby status"); var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId && p.GameId == gameId) ?? throw new InvalidOperationException("Player not found in this game"); player.IsReady = true; await context.SaveChangesAsync(); return game; } public async Task> DealCardsAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); var players = await context.Players.Where(p => p.GameId == gameId).ToListAsync(); var currentRound = await context.GameRounds .Where(gr => gr.GameId == gameId && gr.Status == RoundStatus.Waiting) .FirstOrDefaultAsync() ?? throw new InvalidOperationException("No active round found"); var allPhrases = await context.Phrases.Where(p => p.IsActive).ToListAsync(); if (allPhrases.Count < 5) throw new InvalidOperationException("Not enough phrases in database"); var dealtCards = new List(); foreach (var player in players) { var selectedPhrases = allPhrases.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)).Take(5); foreach (var phrase in selectedPhrases) { var card = new PlayerCard { PlayerId = player.Id, PhraseId = phrase.Id, GameRoundId = currentRound.Id, IsUsed = false }; context.PlayerCards.Add(card); dealtCards.Add(card); } } currentRound.Status = RoundStatus.Active; currentRound.StartedAt = DateTime.UtcNow; await context.SaveChangesAsync(); return dealtCards; } public async Task SubmitSlipAsync(int gameId, int playerId, int cardId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.InProgress) throw new InvalidOperationException("Game is not in progress"); var card = await context.PlayerCards .Include(pc => pc.GameRound) .FirstOrDefaultAsync(pc => pc.Id == cardId) ?? throw new InvalidOperationException("Card not found"); if (card.PlayerId != playerId) throw new UnauthorizedAccessException("Card does not belong to player"); if (card.GameRound.GameId != gameId) throw new InvalidOperationException("Card does not belong to this game"); if (card.GameRound.Status != RoundStatus.Active) throw new InvalidOperationException("Round is not active"); if (card.IsUsed) throw new InvalidOperationException("Card has already been used"); // Karte wird für 30 Sekunden vorgehalten card.SlippedAt = DateTime.UtcNow; await context.SaveChangesAsync(); return true; } public async Task 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 CreateChallengeAsync(int gameId, int challengingPlayerId, int targetPlayerId) { using var context = _contextFactory.CreateDbContext(); var currentRound = await context.GameRounds .Where(gr => gr.GameId == gameId && gr.Status == RoundStatus.Active) .FirstOrDefaultAsync() ?? throw new InvalidOperationException("No active round found"); var challengerBelongsToGame = await context.Players .AnyAsync(p => p.Id == challengingPlayerId && p.GameId == gameId); if (!challengerBelongsToGame) throw new UnauthorizedAccessException("Challenger does not belong to this game"); var targetPlayer = await context.Players .FirstOrDefaultAsync(p => p.Id == targetPlayerId && p.GameId == gameId) ?? throw new InvalidOperationException("Target player not found in this game"); if (targetPlayerId == challengingPlayerId) throw new InvalidOperationException("Cannot challenge yourself"); // Eventuell aktuell vorgehaltene Karte finden var slippingCard = await context.PlayerCards .Where(pc => pc.PlayerId == targetPlayerId && pc.GameRoundId == currentRound.Id && pc.SlippedAt.HasValue && !pc.IsUsed) .OrderByDescending(pc => pc.SlippedAt) .FirstOrDefaultAsync(); var pendingExists = await context.SlipChallenges .AnyAsync(c => c.GameRoundId == currentRound.Id && c.TargetPlayerId == targetPlayerId && c.Status == ChallengeStatus.Pending); if (pendingExists) throw new InvalidOperationException("A challenge for this player is already pending"); var challenge = new SlipChallenge { GameRoundId = currentRound.Id, ChallengingPlayerId = challengingPlayerId, TargetPlayerId = targetPlayerId, TargetCardId = slippingCard?.Id, Status = ChallengeStatus.Pending }; context.SlipChallenges.Add(challenge); await context.SaveChangesAsync(); return challenge; } public async Task ResolveChallengeAsync(int challengeId, bool approved, int? discardCardId = null) { using var context = _contextFactory.CreateDbContext(); var challenge = await context.SlipChallenges .Include(sc => sc.GameRound) .ThenInclude(gr => gr.Game) .Include(sc => sc.ChallengingPlayer) .Include(sc => sc.TargetPlayer) .ThenInclude(p => p.Cards) .FirstOrDefaultAsync(sc => sc.Id == challengeId) ?? throw new InvalidOperationException("Challenge not found"); if (challenge.Status != ChallengeStatus.Pending) throw new InvalidOperationException("Challenge has already been resolved"); if (approved) { // Beschuldigung angenommen: // 1. Beschuldiger erhält Punkt challenge.ChallengingPlayer.Score += 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 { // Anfechten: "dann wird die Beschuldigung zurückgenommen." // Status wird auf Rejected gesetzt, keine Strafen. } challenge.Status = approved ? ChallengeStatus.Approved : ChallengeStatus.Rejected; challenge.ResolvedAt = DateTime.UtcNow; await context.SaveChangesAsync(); return challenge; } public async Task GetGameStateAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); await CompleteExpiredSlipsInternalAsync(context, gameId); var game = await context.Games .Include(g => g.Players) .ThenInclude(p => p.User) .Include(g => g.Players) .ThenInclude(p => p.Cards) .Include(g => g.Rounds) .Include(g => g.WinnerPlayer) .ThenInclude(wp => wp!.User) .FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); var playerInfos = game.Players.Select(p => new PlayerInfoDto { PlayerId = p.Id, Username = p.User.Username, Score = p.Score, IsReady = p.IsReady, CardCount = p.Cards.Count(c => !c.IsUsed) }).ToList(); var currentRound = game.Rounds .OrderByDescending(r => r.RoundNumber) .FirstOrDefault(); var roundDurationSeconds = Math.Clamp(game.RoundDurationSeconds, 30, 60); var roundTimeRemaining = roundDurationSeconds; if (currentRound?.Status == RoundStatus.Active) { var elapsedSeconds = (int)(DateTime.UtcNow - currentRound.StartedAt).TotalSeconds; 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 { GameId = game.Id, LobbyCode = game.LobbyCode, Status = game.Status.ToString(), Players = playerInfos, HostUserId = game.HostId, CurrentRound = currentRound?.RoundNumber ?? 0, RoundTimeRemaining = roundTimeRemaining, WinnerPlayerId = game.WinnerPlayerId, WinnerUsername = winnerUsername }; } public async Task GetPlayerHandAsync(int playerId) { using var context = _contextFactory.CreateDbContext(); var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId); if (player != null) { await CompleteExpiredSlipsInternalAsync(context, player.GameId); } var activeRoundIds = await context.GameRounds .Where(gr => gr.Status == RoundStatus.Active && gr.Game.Players.Any(p => p.Id == playerId)) .Select(gr => gr.Id) .ToListAsync(); var cards = await context.PlayerCards .Include(pc => pc.Phrase) .Where(pc => pc.PlayerId == playerId && activeRoundIds.Contains(pc.GameRoundId)) .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, PhraseId = pc.PhraseId, Text = pc.Phrase.Text, IsUsed = pc.IsUsed, SlippedAt = pc.SlippedAt, SlippedHoldSecondsRemaining = remainingHold }; }).ToList(); return new PlayerHandDto { PlayerId = playerId, Cards = cardDtos }; } public async Task GetGameAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); return await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? 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() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var buffer = new char[6]; for (var i = 0; i < buffer.Length; i++) buffer[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)]; return new string(buffer); } private static async Task GenerateUniqueLobbyCodeAsync(SlipItInDbContext context, int maxAttempts = 10) { for (var attempt = 0; attempt < maxAttempts; attempt++) { var code = GenerateLobbyCode(); var exists = await context.Games.AnyAsync(g => g.LobbyCode == code); if (!exists) return code; } throw new InvalidOperationException("Could not generate a unique lobby code"); } }