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:
Tim Krampitz
2026-07-23 21:39:15 +02:00
parent c1c6c9cb94
commit d29bff17a1
28 changed files with 2379 additions and 9 deletions

5
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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>

View 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);
}
}

View 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);
}
}

View 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
}
}
}

View 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");
}
}
}

View 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
View 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();

View 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"
}
}
}
}

View 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;
}
}

View 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);
}

View 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>

View File

@@ -0,0 +1,6 @@
@SlipItIn.Server_HostAddress = http://localhost:5234
GET {{SlipItIn.Server_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View 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
}
}

View 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; }
}

View 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; } = [];
}

View 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; } = [];
}

View 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; } = [];
}

View 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; } = [];
}

View 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!;
}

View 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
}

View 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; } = [];
}

View 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>

View File

@@ -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>

View File

@@ -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>