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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -360,4 +360,7 @@ MigrationBackup/
|
|||||||
.ionide/
|
.ionide/
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/SlipItIn.Server/slipitIn.db
|
||||||
|
/SlipItIn.Server/slipitIn.db-shm
|
||||||
|
/SlipItIn.Server/slipitIn.db-wal
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
Dieses Dokument beschreibt die Phasen und Aufgaben zur Implementierung des Spiels "Slip It In" mit .NET MAUI (.NET 10) und einem ASP.NET Core Backend.
|
Dieses Dokument beschreibt die Phasen und Aufgaben zur Implementierung des Spiels "Slip It In" mit .NET MAUI (.NET 10) und einem ASP.NET Core Backend.
|
||||||
|
|
||||||
## Phase 1: Vorbereitung & Architektur-Setup
|
## Phase 1: Vorbereitung & Architektur-Setup
|
||||||
- [ ] Erstellung der Verzeichnisstruktur im Workspace
|
- [x] Erstellung der Verzeichnisstruktur im Workspace
|
||||||
- [ ] Aufsetzen der gemeinsamen Datenmodelle (Shared Class Library)
|
- [x] Aufsetzen der gemeinsamen Datenmodelle (Shared Class Library)
|
||||||
- `User`, `Game`, `Phrase`, `PlayerCard`
|
- `User`, `Game`, `Phrase`, `PlayerCard`
|
||||||
- [ ] Initialisierung des ASP.NET Core Web API & SignalR Backends
|
- [x] Initialisierung des ASP.NET Core Web API & SignalR Backends
|
||||||
|
|
||||||
## Phase 2: Backend-Entwicklung (Web API & SignalR)
|
## Phase 2: Backend-Entwicklung (Web API & SignalR)
|
||||||
- [ ] **Datenbank & Persistenz**: Entity Framework Core Setup mit SQLite
|
- [ ] **Datenbank & Persistenz**: Entity Framework Core Setup mit SQLite
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
var builder = DistributedApplication.CreateBuilder(args);
|
var builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.AddProject<Projects.SlipItIn>("slipitin");
|
var db = builder.AddPostgres("pgsql")
|
||||||
|
.AddDatabase("postgresdb");
|
||||||
|
|
||||||
|
var server = builder.AddProject<Projects.SlipItIn_Server>("server")
|
||||||
|
.WithReference(db)
|
||||||
|
.WaitFor(db);
|
||||||
|
|
||||||
|
builder.AddProject<Projects.SlipItIn>("slipitin")
|
||||||
|
.WithReference(server);
|
||||||
|
|
||||||
builder.Build().Run();
|
builder.Build().Run();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Aspire.AppHost.Sdk/13.2.4">
|
<Project Sdk="Aspire.AppHost.Sdk/13.4.6">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -9,13 +9,16 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.6" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.6" />
|
||||||
<PackageReference Include="MessagePack" Version="3.1.8" />
|
<PackageReference Include="MessagePack" Version="3.1.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SlipItIN\SlipItIn.csproj">
|
<ProjectReference Include="..\SlipItIN\SlipItIn.csproj">
|
||||||
<SetTargetFramework>TargetFramework=net10.0</SetTargetFramework>
|
<SetTargetFramework>TargetFramework=net10.0-windows10.0.19041.0</SetTargetFramework>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\SlipItIn.Server\SlipItIn.Server.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
97
SlipItIn.Server/Data/SlipItInDbContext.cs
Normal file
97
SlipItIn.Server/Data/SlipItInDbContext.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Data;
|
||||||
|
|
||||||
|
public class SlipItInDbContext : DbContext
|
||||||
|
{
|
||||||
|
public SlipItInDbContext(DbContextOptions<SlipItInDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Phrase> Phrases { get; set; }
|
||||||
|
public DbSet<Game> Games { get; set; }
|
||||||
|
public DbSet<Player> Players { get; set; }
|
||||||
|
public DbSet<PlayerCard> PlayerCards { get; set; }
|
||||||
|
public DbSet<GameRound> GameRounds { get; set; }
|
||||||
|
public DbSet<SlipChallenge> SlipChallenges { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// User
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasIndex(u => u.Username).IsUnique();
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasIndex(u => u.Email).IsUnique();
|
||||||
|
|
||||||
|
// Game
|
||||||
|
modelBuilder.Entity<Game>()
|
||||||
|
.HasIndex(g => g.LobbyCode).IsUnique();
|
||||||
|
modelBuilder.Entity<Game>()
|
||||||
|
.HasOne(g => g.Host)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(g => g.HostId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// Player
|
||||||
|
modelBuilder.Entity<Player>()
|
||||||
|
.HasOne(p => p.User)
|
||||||
|
.WithMany(u => u.Players)
|
||||||
|
.HasForeignKey(p => p.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Player>()
|
||||||
|
.HasOne(p => p.Game)
|
||||||
|
.WithMany(g => g.Players)
|
||||||
|
.HasForeignKey(p => p.GameId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Player>()
|
||||||
|
.HasIndex(p => new { p.GameId, p.UserId }).IsUnique();
|
||||||
|
|
||||||
|
// PlayerCard
|
||||||
|
modelBuilder.Entity<PlayerCard>()
|
||||||
|
.HasOne(pc => pc.Player)
|
||||||
|
.WithMany(p => p.Cards)
|
||||||
|
.HasForeignKey(pc => pc.PlayerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<PlayerCard>()
|
||||||
|
.HasOne(pc => pc.Phrase)
|
||||||
|
.WithMany(ph => ph.PlayerCards)
|
||||||
|
.HasForeignKey(pc => pc.PhraseId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<PlayerCard>()
|
||||||
|
.HasOne(pc => pc.GameRound)
|
||||||
|
.WithMany(gr => gr.ActiveCards)
|
||||||
|
.HasForeignKey(pc => pc.GameRoundId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// GameRound
|
||||||
|
modelBuilder.Entity<GameRound>()
|
||||||
|
.HasOne(gr => gr.Game)
|
||||||
|
.WithMany(g => g.Rounds)
|
||||||
|
.HasForeignKey(gr => gr.GameId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// SlipChallenge
|
||||||
|
modelBuilder.Entity<SlipChallenge>()
|
||||||
|
.HasOne(sc => sc.GameRound)
|
||||||
|
.WithMany(gr => gr.Challenges)
|
||||||
|
.HasForeignKey(sc => sc.GameRoundId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<SlipChallenge>()
|
||||||
|
.HasOne(sc => sc.ChallengingPlayer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(sc => sc.ChallengingPlayerId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<SlipChallenge>()
|
||||||
|
.HasOne(sc => sc.TargetPlayer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(sc => sc.TargetPlayerId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<SlipChallenge>()
|
||||||
|
.HasOne(sc => sc.TargetCard)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(sc => sc.TargetCardId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
370
SlipItIn.Server/Hubs/GameHub.cs
Normal file
370
SlipItIn.Server/Hubs/GameHub.cs
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Server.Services;
|
||||||
|
using SlipItIn.Shared.DTOs;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Hubs;
|
||||||
|
|
||||||
|
public class GameHub : Hub
|
||||||
|
{
|
||||||
|
private readonly IGameService _gameService;
|
||||||
|
private readonly IDbContextFactory<SlipItInDbContext> _contextFactory;
|
||||||
|
private readonly ILogger<GameHub> _logger;
|
||||||
|
|
||||||
|
public GameHub(IGameService gameService, IDbContextFactory<SlipItInDbContext> contextFactory, ILogger<GameHub> logger)
|
||||||
|
{
|
||||||
|
_gameService = gameService;
|
||||||
|
_contextFactory = contextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validiert die JWT-Claims des aktuellen Nutzers
|
||||||
|
/// Wirft Exception wenn User nicht authentifiziert oder Claim fehlt
|
||||||
|
/// </summary>
|
||||||
|
private int GetAuthenticatedUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = Context.User?.FindFirst(ClaimTypes.NameIdentifier);
|
||||||
|
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
|
||||||
|
throw new UnauthorizedAccessException("Invalid or missing user claim");
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sichert ab, dass ein Spieler nur auf seine eigenen Daten zugreift
|
||||||
|
/// </summary>
|
||||||
|
private async Task ValidatePlayerAccessAsync(int playerId, int authUserId)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||||
|
if (player?.UserId != authUserId)
|
||||||
|
throw new UnauthorizedAccessException("Player does not belong to authenticated user");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Speichert die ConnectionId für einen Spieler in der Datenbank
|
||||||
|
/// </summary>
|
||||||
|
private async Task UpdatePlayerConnectionIdAsync(int userId, int gameId, string connectionId)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var player = await context.Players
|
||||||
|
.FirstOrDefaultAsync(p => p.UserId == userId && p.GameId == gameId);
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
player.ConnectionId = connectionId;
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validiert dass der authentifizierte User der Host des Spiels ist
|
||||||
|
/// </summary>
|
||||||
|
private async Task ValidateHostAccessAsync(int gameId, int authUserId)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId);
|
||||||
|
if (game?.HostId != authUserId)
|
||||||
|
throw new UnauthorizedAccessException("Only the host can perform this action");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validiert dass der authentifizierte User der Ziel-Spieler einer Challenge ist
|
||||||
|
/// </summary>
|
||||||
|
private async Task<int> ValidateChallengeTargetAccessAsync(int challengeId, int authUserId)
|
||||||
|
{
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var challenge = await context.SlipChallenges
|
||||||
|
.Include(c => c.TargetPlayer)
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == challengeId);
|
||||||
|
|
||||||
|
if (challenge == null)
|
||||||
|
throw new InvalidOperationException("Challenge not found");
|
||||||
|
|
||||||
|
if (challenge.TargetPlayer.UserId != authUserId)
|
||||||
|
throw new UnauthorizedAccessException("Only the accused player can resolve this challenge");
|
||||||
|
|
||||||
|
return challenge.TargetPlayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lobby-Verwaltung
|
||||||
|
public async Task CreateLobby()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
var connectionId = Context.ConnectionId;
|
||||||
|
var game = await _gameService.CreateGameAsync(userId, connectionId);
|
||||||
|
await Groups.AddToGroupAsync(connectionId, game.LobbyCode);
|
||||||
|
|
||||||
|
await Clients.Caller.SendAsync("LobbyCreated", new { GameId = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
_logger.LogInformation("Lobby {LobbyCode} created by user {UserId}", game.LobbyCode, userId);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "Unauthorized" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating lobby");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while creating the lobby." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task JoinLobby(string lobbyCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
var connectionId = Context.ConnectionId;
|
||||||
|
var game = await _gameService.JoinGameAsync(lobbyCode, userId, connectionId);
|
||||||
|
await Groups.AddToGroupAsync(connectionId, lobbyCode);
|
||||||
|
|
||||||
|
var gameState = await _gameService.GetGameStateAsync(game.Id);
|
||||||
|
await Clients.Group(lobbyCode).SendAsync("PlayerJoined", gameState);
|
||||||
|
_logger.LogInformation("User {UserId} joined lobby {LobbyCode}", userId, lobbyCode);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "Unauthorized" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error joining lobby");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while joining the lobby." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlayerReady(int gameId, int playerId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidatePlayerAccessAsync(playerId, userId);
|
||||||
|
|
||||||
|
var game = await _gameService.SetPlayerReadyAsync(gameId, playerId);
|
||||||
|
var gameState = await _gameService.GetGameStateAsync(gameId);
|
||||||
|
|
||||||
|
var lobbyCode = game.LobbyCode;
|
||||||
|
await Clients.Group(lobbyCode).SendAsync("GameStateUpdated", gameState);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error setting player ready");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while setting player status." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spiel-Start & Kartenvergabe
|
||||||
|
public async Task StartGame(int gameId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Nur der Host darf das Spiel starten
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidateHostAccessAsync(gameId, userId);
|
||||||
|
|
||||||
|
var game = await _gameService.StartGameAsync(gameId);
|
||||||
|
var cards = await _gameService.DealCardsAsync(gameId);
|
||||||
|
|
||||||
|
var lobbyCode = game.LobbyCode;
|
||||||
|
await Clients.Group(lobbyCode).SendAsync("GameStarted", new { GameId = gameId });
|
||||||
|
|
||||||
|
// Alle Spieler und ihre ConnectionIds in einem Batch laden (Optimierung)
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var playerIds = cards.Select(c => c.PlayerId).Distinct().ToList();
|
||||||
|
var players = await context.Players
|
||||||
|
.Where(p => playerIds.Contains(p.Id))
|
||||||
|
.ToDictionaryAsync(p => p.Id, p => p.ConnectionId);
|
||||||
|
|
||||||
|
// Jeder Spieler erhält nur SEINE eigenen Karten
|
||||||
|
foreach (var playerCards in cards.GroupBy(c => c.PlayerId))
|
||||||
|
{
|
||||||
|
var hand = await _gameService.GetPlayerHandAsync(playerCards.Key);
|
||||||
|
if (players.TryGetValue(playerCards.Key, out var connectionId) && connectionId != null)
|
||||||
|
{
|
||||||
|
await Clients.Client(connectionId).SendAsync("PlayerHandUpdated", hand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Game {GameId} started by user {UserId}", gameId, userId);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "Only the host can start the game." });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error starting game");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while starting the game." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slip-Mechaniken
|
||||||
|
public async Task SubmitSlip(int gameId, int playerId, int cardId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidatePlayerAccessAsync(playerId, userId);
|
||||||
|
|
||||||
|
var result = await _gameService.SubmitSlipAsync(gameId, playerId, cardId);
|
||||||
|
var game = await _gameService.GetGameAsync(gameId);
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("SlipSubmitted", new
|
||||||
|
{
|
||||||
|
PlayerId = playerId,
|
||||||
|
CardId = cardId,
|
||||||
|
Success = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error submitting slip");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while submitting slip." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChallengeSlip(int gameId, int challengingPlayerId, int targetCardId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidatePlayerAccessAsync(challengingPlayerId, userId);
|
||||||
|
|
||||||
|
var challenge = await _gameService.CreateChallengeAsync(gameId, challengingPlayerId, targetCardId);
|
||||||
|
var game = await _gameService.GetGameAsync(gameId);
|
||||||
|
|
||||||
|
var challengeDto = new SlipChallengeDto
|
||||||
|
{
|
||||||
|
ChallengeId = challenge.Id,
|
||||||
|
ChallengingPlayerId = challengingPlayerId,
|
||||||
|
TargetPlayerId = challenge.TargetPlayerId,
|
||||||
|
TargetCardId = targetCardId,
|
||||||
|
Status = challenge.Status.ToString(),
|
||||||
|
CreatedAt = challenge.CreatedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("SlipChallenged", challengeDto);
|
||||||
|
|
||||||
|
// Sende direkte Notification an den beschuldigten Spieler
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var targetPlayer = await context.Players.FindAsync(challenge.TargetPlayerId);
|
||||||
|
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 slip");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while challenging slip." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResolveChallenge(int challengeId, bool approved)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
await ValidateChallengeTargetAccessAsync(challengeId, userId);
|
||||||
|
|
||||||
|
var result = await _gameService.ResolveChallengeAsync(challengeId, approved);
|
||||||
|
var game = await _gameService.GetGameAsync(result.GameRound.GameId);
|
||||||
|
|
||||||
|
await Clients.Group(game.LobbyCode).SendAsync("ChallengeResolved", new
|
||||||
|
{
|
||||||
|
ChallengeId = challengeId,
|
||||||
|
Approved = approved
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktualisierte Handkarten an betroffene Spieler senden
|
||||||
|
var challengingHand = await _gameService.GetPlayerHandAsync(result.ChallengingPlayerId);
|
||||||
|
var targetHand = await _gameService.GetPlayerHandAsync(result.TargetPlayerId);
|
||||||
|
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var challengingPlayer = await context.Players.FindAsync(result.ChallengingPlayerId);
|
||||||
|
var targetPlayer = await context.Players.FindAsync(result.TargetPlayerId);
|
||||||
|
|
||||||
|
if (challengingPlayer?.ConnectionId != null)
|
||||||
|
await Clients.Client(challengingPlayer.ConnectionId).SendAsync("PlayerHandUpdated", challengingHand);
|
||||||
|
|
||||||
|
if (targetPlayer?.ConnectionId != null)
|
||||||
|
await Clients.Client(targetPlayer.ConnectionId).SendAsync("PlayerHandUpdated", targetHand);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error resolving challenge");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while resolving challenge." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RequestGameState(int gameId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var gameState = await _gameService.GetGameStateAsync(gameId);
|
||||||
|
await Clients.Caller.SendAsync("GameStateUpdated", gameState);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error requesting game state");
|
||||||
|
await Clients.Caller.SendAsync("Error", new { Message = "An error occurred while requesting game state." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Client {ConnectionId} connected", Context.ConnectionId);
|
||||||
|
|
||||||
|
// Speichere ConnectionId beim Player falls User bereits in einem Spiel ist (Lobby oder InProgress)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetAuthenticatedUserId();
|
||||||
|
using var context = _contextFactory.CreateDbContext();
|
||||||
|
var player = await context.Players
|
||||||
|
.FirstOrDefaultAsync(p => p.UserId == userId && (p.Game.Status == Shared.Models.GameStatus.InProgress || p.Game.Status == Shared.Models.GameStatus.Lobby));
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
player.ConnectionId = Context.ConnectionId;
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Ignorieren falls nicht authentifiziert */ }
|
||||||
|
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Client {ConnectionId} disconnected", Context.ConnectionId);
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
432
SlipItIn.Server/Migrations/20260723193505_InitialCreate.Designer.cs
generated
Normal file
432
SlipItIn.Server/Migrations/20260723193505_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
// <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("20260723193505_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.10")
|
||||||
|
.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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("HostId");
|
||||||
|
|
||||||
|
b.HasIndex("LobbyCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
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.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.Navigation("Host");
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
319
SlipItIn.Server/Migrations/20260723193505_InitialCreate.cs
Normal file
319
SlipItIn.Server/Migrations/20260723193505_InitialCreate.cs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Username = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Email = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Games",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
LobbyCode = table.Column<string>(type: "text", nullable: false),
|
||||||
|
HostId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
EndedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
MaxPlayers = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RoundDurationSeconds = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Games", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Games_Users_HostId",
|
||||||
|
column: x => x.HostId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Phrases",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatorId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Phrases", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Phrases_Users_CreatorId",
|
||||||
|
column: x => x.CreatorId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "GameRounds",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
GameId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RoundNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
EndedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_GameRounds", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_GameRounds_Games_GameId",
|
||||||
|
column: x => x.GameId,
|
||||||
|
principalTable: "Games",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Players",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
GameId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Score = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SuccessfulSlips = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
FailedSlips = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
JoinedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
IsReady = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
ConnectionId = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Players", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Players_Games_GameId",
|
||||||
|
column: x => x.GameId,
|
||||||
|
principalTable: "Games",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Players_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlayerCards",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
PlayerId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PhraseId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
GameRoundId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
IsUsed = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
AssignedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlayerCards", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayerCards_GameRounds_GameRoundId",
|
||||||
|
column: x => x.GameRoundId,
|
||||||
|
principalTable: "GameRounds",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayerCards_Phrases_PhraseId",
|
||||||
|
column: x => x.PhraseId,
|
||||||
|
principalTable: "Phrases",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayerCards_Players_PlayerId",
|
||||||
|
column: x => x.PlayerId,
|
||||||
|
principalTable: "Players",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SlipChallenges",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
GameRoundId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ChallengingPlayerId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TargetPlayerId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TargetCardId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
ResolvedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SlipChallenges", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SlipChallenges_GameRounds_GameRoundId",
|
||||||
|
column: x => x.GameRoundId,
|
||||||
|
principalTable: "GameRounds",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SlipChallenges_PlayerCards_TargetCardId",
|
||||||
|
column: x => x.TargetCardId,
|
||||||
|
principalTable: "PlayerCards",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SlipChallenges_Players_ChallengingPlayerId",
|
||||||
|
column: x => x.ChallengingPlayerId,
|
||||||
|
principalTable: "Players",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SlipChallenges_Players_TargetPlayerId",
|
||||||
|
column: x => x.TargetPlayerId,
|
||||||
|
principalTable: "Players",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_GameRounds_GameId",
|
||||||
|
table: "GameRounds",
|
||||||
|
column: "GameId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Games_HostId",
|
||||||
|
table: "Games",
|
||||||
|
column: "HostId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Games_LobbyCode",
|
||||||
|
table: "Games",
|
||||||
|
column: "LobbyCode",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Phrases_CreatorId",
|
||||||
|
table: "Phrases",
|
||||||
|
column: "CreatorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayerCards_GameRoundId",
|
||||||
|
table: "PlayerCards",
|
||||||
|
column: "GameRoundId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayerCards_PhraseId",
|
||||||
|
table: "PlayerCards",
|
||||||
|
column: "PhraseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayerCards_PlayerId",
|
||||||
|
table: "PlayerCards",
|
||||||
|
column: "PlayerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Players_GameId_UserId",
|
||||||
|
table: "Players",
|
||||||
|
columns: new[] { "GameId", "UserId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Players_UserId",
|
||||||
|
table: "Players",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SlipChallenges_ChallengingPlayerId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
column: "ChallengingPlayerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SlipChallenges_GameRoundId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
column: "GameRoundId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SlipChallenges_TargetCardId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
column: "TargetCardId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SlipChallenges_TargetPlayerId",
|
||||||
|
table: "SlipChallenges",
|
||||||
|
column: "TargetPlayerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_Email",
|
||||||
|
table: "Users",
|
||||||
|
column: "Email",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_Username",
|
||||||
|
table: "Users",
|
||||||
|
column: "Username",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SlipChallenges");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlayerCards");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "GameRounds");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Phrases");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Players");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Games");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
429
SlipItIn.Server/Migrations/SlipItInDbContextModelSnapshot.cs
Normal file
429
SlipItIn.Server/Migrations/SlipItInDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SlipItInDbContext))]
|
||||||
|
partial class SlipItInDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.10")
|
||||||
|
.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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("HostId");
|
||||||
|
|
||||||
|
b.HasIndex("LobbyCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
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.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.Navigation("Host");
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
SlipItIn.Server/Program.cs
Normal file
103
SlipItIn.Server/Program.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Server.Hubs;
|
||||||
|
using SlipItIn.Server.Services;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Aspire Service Defaults (OpenTelemetry, Health Checks, Service Discovery)
|
||||||
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
|
builder.AddNpgsqlDataSource(connectionName: "postgresdb");
|
||||||
|
|
||||||
|
builder.Services.AddDbContextFactory<SlipItInDbContext>(options => options.UseNpgsql());
|
||||||
|
|
||||||
|
// Services registrieren
|
||||||
|
builder.Services.AddTransient<IGameService, GameService>();
|
||||||
|
|
||||||
|
// JWT Authentication
|
||||||
|
var jwtKey = builder.Configuration["Jwt:Key"] ?? "YourSuperSecretKeyThatIsAtLeast32CharactersLong!";
|
||||||
|
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "SlipItInServer";
|
||||||
|
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "SlipItInClient";
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwtIssuer,
|
||||||
|
ValidAudience = jwtAudience,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||||
|
};
|
||||||
|
|
||||||
|
// SignalR-Support für JWT in Query String
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"];
|
||||||
|
var path = context.HttpContext.Request.Path;
|
||||||
|
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||||
|
{
|
||||||
|
context.Token = accessToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// SignalR
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
// CORS für Development
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Controller & OpenAPI
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Aspire Default Endpoints (Health Checks)
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
|
// Migrations anwenden
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SlipItInDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.MapOpenApi();
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.MapHub<GameHub>("/hubs/game");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
23
SlipItIn.Server/Properties/launchSettings.json
Normal file
23
SlipItIn.Server/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5234",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "https://localhost:7274;http://localhost:5234",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
27
SlipItIn.Server/SlipItIn.Server.csproj
Normal file
27
SlipItIn.Server/SlipItIn.Server.csproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Aspire.Npgsql" Version="13.4.6" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SlipItIn.ServiceDefaults\SlipItIn.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\SlipItIn.Shared\SlipItIn.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
SlipItIn.Server/SlipItIn.Server.http
Normal file
6
SlipItIn.Server/SlipItIn.Server.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@SlipItIn.Server_HostAddress = http://localhost:5234
|
||||||
|
|
||||||
|
GET {{SlipItIn.Server_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
8
SlipItIn.Server/appsettings.Development.json
Normal file
8
SlipItIn.Server/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
SlipItIn.Server/appsettings.json
Normal file
19
SlipItIn.Server/appsettings.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"postgresdb": "Server=(localdb)\\postgres;Database=postgresdb"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
|
||||||
|
"Issuer": "SlipItInServer",
|
||||||
|
"Audience": "SlipItInClient",
|
||||||
|
"ExpirationMinutes": 1440
|
||||||
|
}
|
||||||
|
}
|
||||||
47
SlipItIn.Shared/DTOs/GameStateDto.cs
Normal file
47
SlipItIn.Shared/DTOs/GameStateDto.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace SlipItIn.Shared.DTOs;
|
||||||
|
|
||||||
|
public class GameStateDto
|
||||||
|
{
|
||||||
|
public int GameId { get; set; }
|
||||||
|
public string LobbyCode { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public List<PlayerInfoDto> Players { get; set; } = [];
|
||||||
|
public int CurrentRound { get; set; }
|
||||||
|
public int RoundTimeRemaining { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerInfoDto
|
||||||
|
{
|
||||||
|
// Öffentliche Daten (für alle sichtbar)
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public int Score { get; set; }
|
||||||
|
public bool IsReady { get; set; }
|
||||||
|
public int CardCount { get; set; } // Anzahl der Karten (nicht die Karten selbst!)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerHandDto
|
||||||
|
{
|
||||||
|
// Private Daten (nur für den Spieler selbst)
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
public List<PlayerCardDto> Cards { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerCardDto
|
||||||
|
{
|
||||||
|
public int CardId { get; set; }
|
||||||
|
public int PhraseId { get; set; }
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public bool IsUsed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SlipChallengeDto
|
||||||
|
{
|
||||||
|
public int ChallengeId { get; set; }
|
||||||
|
public int ChallengingPlayerId { get; set; }
|
||||||
|
public string ChallengingPlayerName { get; set; } = string.Empty;
|
||||||
|
public int TargetPlayerId { get; set; }
|
||||||
|
public int TargetCardId { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
27
SlipItIn.Shared/Models/Game.cs
Normal file
27
SlipItIn.Shared/Models/Game.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public enum GameStatus
|
||||||
|
{
|
||||||
|
Lobby,
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Game
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string LobbyCode { get; set; } = string.Empty; // 4-6 stelliger Code
|
||||||
|
public int HostId { get; set; }
|
||||||
|
public GameStatus Status { get; set; } = GameStatus.Lobby;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? StartedAt { get; set; }
|
||||||
|
public DateTime? EndedAt { get; set; }
|
||||||
|
public int MaxPlayers { get; set; } = 8;
|
||||||
|
public int RoundDurationSeconds { get; set; } = 45;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public User Host { get; set; } = null!;
|
||||||
|
public ICollection<Player> Players { get; set; } = [];
|
||||||
|
public ICollection<GameRound> Rounds { get; set; } = [];
|
||||||
|
}
|
||||||
24
SlipItIn.Shared/Models/GameRound.cs
Normal file
24
SlipItIn.Shared/Models/GameRound.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public enum RoundStatus
|
||||||
|
{
|
||||||
|
Waiting,
|
||||||
|
Active,
|
||||||
|
Resolving,
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GameRound
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int GameId { get; set; }
|
||||||
|
public int RoundNumber { get; set; }
|
||||||
|
public RoundStatus Status { get; set; } = RoundStatus.Waiting;
|
||||||
|
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? EndedAt { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public Game Game { get; set; } = null!;
|
||||||
|
public ICollection<PlayerCard> ActiveCards { get; set; } = [];
|
||||||
|
public ICollection<SlipChallenge> Challenges { get; set; } = [];
|
||||||
|
}
|
||||||
14
SlipItIn.Shared/Models/Phrase.cs
Normal file
14
SlipItIn.Shared/Models/Phrase.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public class Phrase
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public int CreatorId { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public User Creator { get; set; } = null!;
|
||||||
|
public ICollection<PlayerCard> PlayerCards { get; set; } = [];
|
||||||
|
}
|
||||||
19
SlipItIn.Shared/Models/Player.cs
Normal file
19
SlipItIn.Shared/Models/Player.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public int GameId { get; set; }
|
||||||
|
public int Score { get; set; } = 0;
|
||||||
|
public int SuccessfulSlips { get; set; } = 0;
|
||||||
|
public int FailedSlips { get; set; } = 0;
|
||||||
|
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool IsReady { get; set; } = false;
|
||||||
|
public string? ConnectionId { get; set; } // SignalR-Verbindungs-ID
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
public Game Game { get; set; } = null!;
|
||||||
|
public ICollection<PlayerCard> Cards { get; set; } = [];
|
||||||
|
}
|
||||||
16
SlipItIn.Shared/Models/PlayerCard.cs
Normal file
16
SlipItIn.Shared/Models/PlayerCard.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public class PlayerCard
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
public int PhraseId { get; set; }
|
||||||
|
public int GameRoundId { get; set; }
|
||||||
|
public bool IsUsed { get; set; } = false;
|
||||||
|
public DateTime AssignedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public Player Player { get; set; } = null!;
|
||||||
|
public Phrase Phrase { get; set; } = null!;
|
||||||
|
public GameRound GameRound { get; set; } = null!;
|
||||||
|
}
|
||||||
26
SlipItIn.Shared/Models/SlipChallenge.cs
Normal file
26
SlipItIn.Shared/Models/SlipChallenge.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public enum ChallengeStatus
|
||||||
|
{
|
||||||
|
Pending, // Beschuldigung eingegangen, wartet auf Bestätigung/Ablehnung
|
||||||
|
Approved, // Beschuldigung war berechtigt → Phrase bleibt beim Beschuldiger
|
||||||
|
Rejected // Beschuldigung falsch → Beschuldiger erhält die Phrase
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SlipChallenge
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int GameRoundId { get; set; }
|
||||||
|
public int ChallengingPlayerId { get; set; } // Wer beschuldigt
|
||||||
|
public int TargetPlayerId { get; set; } // Wer beschuldigt wird
|
||||||
|
public int TargetCardId { get; set; } // Welche Phrase wird als falsch behauptet
|
||||||
|
public ChallengeStatus Status { get; set; } = ChallengeStatus.Pending;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? ResolvedAt { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public GameRound GameRound { get; set; } = null!;
|
||||||
|
public Player ChallengingPlayer { get; set; } = null!; // Der Beschuldiger
|
||||||
|
public Player TargetPlayer { get; set; } = null!; // Der Beschuldigte
|
||||||
|
public PlayerCard TargetCard { get; set; } = null!; // Die angefochtene Phrase
|
||||||
|
}
|
||||||
15
SlipItIn.Shared/Models/User.cs
Normal file
15
SlipItIn.Shared/Models/User.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string PasswordHash { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public ICollection<Player> Players { get; set; } = [];
|
||||||
|
}
|
||||||
13
SlipItIn.Shared/SlipItIn.Shared.csproj
Normal file
13
SlipItIn.Shared/SlipItIn.Shared.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<Folder Name="/Projektmappenelemente/">
|
||||||
|
<File Path="Agents/Architecture.md" />
|
||||||
|
<File Path="Agents/ProjectPlan.md" />
|
||||||
|
</Folder>
|
||||||
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
||||||
|
<Project Path="SlipItIn.Server/SlipItIn.Server.csproj" />
|
||||||
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
||||||
|
<Project Path="SlipItIn.Shared/SlipItIn.Shared.csproj" />
|
||||||
<Project Path="SlipItIN/SlipItIn.csproj">
|
<Project Path="SlipItIN/SlipItIn.csproj">
|
||||||
<Deploy Solution="Debug|*" />
|
<Deploy Solution="Debug|*" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -47,6 +47,38 @@
|
|||||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-android|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-ios|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-maccatalyst|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-windows10.0.19041.0|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-android|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-ios|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-maccatalyst|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-windows10.0.19041.0|AnyCPU'">
|
||||||
|
<ApplicationTitle>SlipItIn</ApplicationTitle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- App Icon -->
|
<!-- App Icon -->
|
||||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||||
@@ -66,8 +98,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.80" />
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.90" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="11.0.0-preview.6.26359.118" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user