Backend für "Slip It In" mit ASP.NET Core erstellt
- Neues Backend-Projekt mit Entity Framework Core (PostgreSQL) und geteilten Modellen/DTOs angelegt - Datenbankstruktur und Migrations implementiert (User, Game, Player, Phrase, PlayerCard, GameRound, SlipChallenge) - SignalR-Hub für Spielkommunikation (Lobby, Spielstart, Kartenvergabe, Slip/Challenge) hinzugefügt - Spiellogik und DB-Zugriffe im GameService gekapselt - JWT-Authentifizierung und CORS für Entwicklung konfiguriert - Projektdateien, NuGet-Abhängigkeiten und .gitignore aktualisiert - Startkonfiguration und Umgebungsdateien angepasst
This commit is contained in:
265
SlipItIn.Server/Services/GameService.cs
Normal file
265
SlipItIn.Server/Services/GameService.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SlipItIn.Server.Data;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
using SlipItIn.Shared.Models;
|
||||
|
||||
namespace SlipItIn.Server.Services;
|
||||
|
||||
public class GameService : IGameService
|
||||
{
|
||||
private readonly IDbContextFactory<SlipItInDbContext> _contextFactory;
|
||||
private readonly ILogger<GameService> _logger;
|
||||
|
||||
public GameService(IDbContextFactory<SlipItInDbContext> contextFactory, ILogger<GameService> logger)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Game> 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 = GenerateLobbyCode();
|
||||
var game = new Game
|
||||
{
|
||||
LobbyCode = lobbyCode,
|
||||
HostId = host.Id,
|
||||
Status = GameStatus.Lobby
|
||||
};
|
||||
|
||||
context.Games.Add(game);
|
||||
|
||||
// Host wird automatisch als erster Spieler hinzugefügt
|
||||
var hostPlayer = new Player { UserId = host.Id, GameId = game.Id, ConnectionId = connectionId };
|
||||
context.Players.Add(hostPlayer);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
public async Task<Game> 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");
|
||||
|
||||
// Prüfen ob User bereits im Spiel ist
|
||||
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<Game> StartGameAsync(int gameId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found");
|
||||
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<Game> SetPlayerReadyAsync(int gameId, int playerId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId) ?? throw new InvalidOperationException("Player not found");
|
||||
player.IsReady = true;
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return await context.Games.FirstOrDefaultAsync(g => g.Id == gameId)
|
||||
?? throw new InvalidOperationException("Game not found");
|
||||
}
|
||||
|
||||
public async Task<List<PlayerCard>> 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<PlayerCard>();
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
var selectedPhrases = allPhrases.OrderBy(_ => Random.Shared.Next()).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;
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return dealtCards;
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitSlipAsync(int gameId, int playerId, int cardId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var card = await context.PlayerCards
|
||||
.Include(pc => pc.Player)
|
||||
.FirstOrDefaultAsync(pc => pc.Id == cardId) ?? throw new InvalidOperationException("Card not found");
|
||||
if (card.PlayerId != playerId) throw new UnauthorizedAccessException("Card does not belong to player");
|
||||
|
||||
card.IsUsed = true;
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetCardId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var targetCard = await context.PlayerCards.FirstOrDefaultAsync(pc => pc.Id == targetCardId) ?? throw new InvalidOperationException("Target card not found");
|
||||
|
||||
var currentRound = await context.GameRounds
|
||||
.Where(gr => gr.GameId == gameId && gr.Status == RoundStatus.Active)
|
||||
.FirstOrDefaultAsync() ?? throw new InvalidOperationException("No active round found");
|
||||
|
||||
if (currentRound == null) throw new InvalidOperationException("No active round found");
|
||||
|
||||
var challenge = new SlipChallenge
|
||||
{
|
||||
GameRoundId = currentRound.Id,
|
||||
ChallengingPlayerId = challengingPlayerId,
|
||||
TargetPlayerId = targetCard.PlayerId,
|
||||
TargetCardId = targetCardId,
|
||||
Status = ChallengeStatus.Pending
|
||||
};
|
||||
|
||||
context.SlipChallenges.Add(challenge);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public async Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var challenge = await context.SlipChallenges
|
||||
.Include(sc => sc.GameRound)
|
||||
.Include(sc => sc.TargetCard)
|
||||
.FirstOrDefaultAsync(sc => sc.Id == challengeId) ?? throw new InvalidOperationException("Challenge not found");
|
||||
|
||||
// approved = Beschuldigung stimmt (Phrase war falsch)
|
||||
// rejected = Beschuldigung stimmt nicht (Phrase war richtig)
|
||||
|
||||
if (!approved)
|
||||
{
|
||||
// FALSCHE BESCHULDIGUNG: Beschuldiger erhält die Phrase des Beschuldigten
|
||||
var targetCard = challenge.TargetCard;
|
||||
targetCard.PlayerId = challenge.ChallengingPlayerId;
|
||||
context.PlayerCards.Update(targetCard);
|
||||
}
|
||||
// Wenn approved: Phrase bleibt bei Beschuldigtem (= Phrase war tatsächlich ein SlipIn)
|
||||
|
||||
challenge.Status = approved ? ChallengeStatus.Approved : ChallengeStatus.Rejected;
|
||||
challenge.ResolvedAt = DateTime.UtcNow;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public async Task<GameStateDto> GetGameStateAsync(int gameId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var game = await context.Games
|
||||
.Include(g => g.Players)
|
||||
.ThenInclude(p => p.User)
|
||||
.Include(g => g.Players)
|
||||
.ThenInclude(p => p.Cards)
|
||||
.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 // Nur Anzahl, nicht die Karten selbst!
|
||||
}).ToList();
|
||||
|
||||
return new GameStateDto
|
||||
{
|
||||
GameId = game.Id,
|
||||
LobbyCode = game.LobbyCode,
|
||||
Status = game.Status.ToString(),
|
||||
Players = playerInfos,
|
||||
CurrentRound = game.Rounds?.Count ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PlayerHandDto> GetPlayerHandAsync(int playerId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
|
||||
var cards = await context.PlayerCards
|
||||
.Include(pc => pc.Phrase)
|
||||
.Where(pc => pc.PlayerId == playerId)
|
||||
.Select(pc => new PlayerCardDto
|
||||
{
|
||||
CardId = pc.Id,
|
||||
PhraseId = pc.PhraseId,
|
||||
Text = pc.Phrase.Text,
|
||||
IsUsed = pc.IsUsed
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new PlayerHandDto
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Cards = cards
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Game> GetGameAsync(int gameId)
|
||||
{
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
return await context.Games.FirstOrDefaultAsync(g => g.Id == gameId)
|
||||
?? throw new InvalidOperationException("Game not found");
|
||||
}
|
||||
|
||||
private string GenerateLobbyCode()
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
var code = new string([.. Enumerable.Range(0, 6).Select(_ => chars[Random.Shared.Next(chars.Length)])]);
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
19
SlipItIn.Server/Services/IGameService.cs
Normal file
19
SlipItIn.Server/Services/IGameService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using SlipItIn.Shared.DTOs;
|
||||
using SlipItIn.Shared.Models;
|
||||
|
||||
namespace SlipItIn.Server.Services;
|
||||
|
||||
public interface IGameService
|
||||
{
|
||||
Task<Game> CreateGameAsync(int hostUserId, string? connectionId = null);
|
||||
Task<Game> JoinGameAsync(string lobbyCode, int userId, string? connectionId = null);
|
||||
Task<Game> StartGameAsync(int gameId);
|
||||
Task<Game> SetPlayerReadyAsync(int gameId, int playerId);
|
||||
Task<List<PlayerCard>> DealCardsAsync(int gameId);
|
||||
Task<bool> SubmitSlipAsync(int gameId, int playerId, int cardId);
|
||||
Task<SlipChallenge> CreateChallengeAsync(int gameId, int challengingPlayerId, int targetCardId);
|
||||
Task<SlipChallenge> ResolveChallengeAsync(int challengeId, bool approved);
|
||||
Task<GameStateDto> GetGameStateAsync(int gameId);
|
||||
Task<PlayerHandDto> GetPlayerHandAsync(int playerId);
|
||||
Task<Game> GetGameAsync(int gameId);
|
||||
}
|
||||
Reference in New Issue
Block a user