Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions Project.App/Project.Api/Controllers/RoomController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Project.Api.DTOs;
using Project.Api.Models.Games;
using Project.Api.Services.Interface;
using Project.Api.Utilities;
using Project.Api.Utilities.Enums;

namespace Project.Api.Controllers;

Expand Down Expand Up @@ -46,19 +48,19 @@ public async Task<ActionResult<IEnumerable<RoomDTO>>> GetPublicRooms()
[HttpGet("{id}")]
public async Task<ActionResult<RoomDTO>> GetRoomById(Guid id)
{
var room = await _roomService.GetRoomByIdAsync(id);
if (room == null)
throw new NotFoundException($"Room with ID {id} not found");
var room =
await _roomService.GetRoomByIdAsync(id)
?? throw new NotFoundException($"Room with ID {id} not found");
return Ok(room);
}

// GET: api/room/host/{hostId}
[HttpGet("host/{hostId}")]
public async Task<ActionResult<RoomDTO>> GetRoomByHostId(Guid hostId)
{
var room = await _roomService.GetRoomByHostIdAsync(hostId);
if (room == null)
throw new NotFoundException($"Room with host ID {hostId} not found");
var room =
await _roomService.GetRoomByHostIdAsync(hostId)
?? throw new NotFoundException($"Room with host ID {hostId} not found");
return Ok(room);
}

Expand Down Expand Up @@ -93,7 +95,7 @@ public async Task<ActionResult<RoomDTO>> CreateRoom([FromBody] CreateRoomDTO dto
if (!ModelState.IsValid)
return BadRequest(ModelState);

var room = await _roomService.CreateRoomAsync(dto);
RoomDTO room = await _roomService.CreateRoomAsync(dto);
return CreatedAtAction(nameof(GetRoomById), new { id = room.Id }, room);
}

Expand All @@ -107,9 +109,9 @@ public async Task<ActionResult<RoomDTO>> UpdateRoom(Guid id, [FromBody] UpdateRo
if (!ModelState.IsValid)
return BadRequest(ModelState);

var room = await _roomService.UpdateRoomAsync(dto);
if (room == null)
throw new NotFoundException($"Room with ID {id} not found");
var room =
await _roomService.UpdateRoomAsync(dto)
?? throw new NotFoundException($"Room with ID {id} not found");

return Ok(room);
}
Expand Down Expand Up @@ -242,9 +244,13 @@ public async Task<IActionResult> BroadcastMessage(Guid roomId, [FromBody] Messag
throw new BadRequestException("Message content cannot be empty.");
}

string name = User.Identity?.Name ?? "Anonymous";
MessageEventData data = new()
{
Sender = User.Identity?.Name ?? "Anonymous",
Content = message.Content,
};

await _roomSSEService.BroadcastEventAsync(roomId, "message", $"{name}: {message.Content}");
await _roomSSEService.BroadcastEventAsync(roomId, RoomEventType.Message, data);
return Ok();
}

Expand Down
1 change: 1 addition & 0 deletions Project.App/Project.Api/DTOs/CardDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public class CardDTO
public string Image { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Suit { get; set; } = string.Empty;
public bool IsFaceDown { get; set; } = false;
}
3 changes: 2 additions & 1 deletion Project.App/Project.Api/DTOs/HandDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public class HandDTO
public required Guid RoomPlayerId { get; set; }

public required int Order { get; set; }
public int HandNumber { get; set; } = 0;

public required int Bet { get; set; }
public required long Bet { get; set; }
}
}
10 changes: 4 additions & 6 deletions Project.App/Project.Api/DTOs/RoomDTOs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,19 @@ public class RoomDTO
public int MaxPlayers { get; set; }
public int MinPlayers { get; set; }
public string DeckId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public bool IsActive { get; set; }
}

public class CreateRoomDTO
{
public Guid HostId { get; set; }
public bool IsPublic { get; set; }
public bool IsPublic { get; set; } = true;
public string GameMode { get; set; } = string.Empty;
public string GameState { get; set; } = string.Empty;
public string GameConfig { get; set; } = string.Empty;
public string? Description { get; set; }
public int MaxPlayers { get; set; }
public int MinPlayers { get; set; }
public string DeckId { get; set; } = string.Empty;
public int MaxPlayers { get; set; } = 10;
public int MinPlayers { get; set; } = 1;
}

public class UpdateRoomDTO : CreateRoomDTO
Expand Down
1 change: 1 addition & 0 deletions Project.App/Project.Api/Models/Games/BlackjackConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public record BlackjackConfig : GameConfig
public long MinBet { get; set; } = 0;
public TimeSpan BettingTimeLimit { get; set; } = TimeSpan.FromSeconds(60);
public override TimeSpan TurnTimeLimit { get; set; } = TimeSpan.FromSeconds(30);
public bool AllowBalanceReset { get; set; } = true;
}
32 changes: 29 additions & 3 deletions Project.App/Project.Api/Models/Games/BlackjackState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ namespace Project.Api.Models.Games;

public record BlackjackState : GameState<BlackjackStage>
{
public List<object> DealerHand { get; set; } = [];
public string DealerHand { get; set; } = "";
public Dictionary<Guid, long> Bets { get; set; } = [];
}

[JsonDerivedType(typeof(BlackjackInitStage), typeDiscriminator: "init")]
Expand All @@ -31,11 +32,36 @@ public record BlackjackBettingStage(DateTimeOffset Deadline, Dictionary<Guid, lo
public record BlackjackDealingStage : BlackjackStage;

// player turn
// TODO: figure out how turn order will work
public record BlackjackPlayerActionStage(DateTimeOffset Deadline, int Index) : BlackjackStage;
public record BlackjackPlayerActionStage(DateTimeOffset Deadline, int PlayerIndex, int HandIndex)
: BlackjackStage;

// dealer turn and distribute winnings
public record BlackjackFinishRoundStage : BlackjackStage;

// teardown, close room
public record BlackjackTeardownStage : BlackjackStage;

public static class BlackjackStateExtensions
{
/// <summary>
/// Resets the deadline of a betting stage to the current time plus the given duration.
/// </summary>
public static BlackjackBettingStage ResetDeadline(
this BlackjackBettingStage stage,
TimeSpan duration
)
{
return stage with { Deadline = DateTimeOffset.UtcNow + duration };
}

/// <summary>
/// Resets the deadline of a player action stage to the current time plus the given duration.
/// </summary>
public static BlackjackPlayerActionStage ResetDeadline(
this BlackjackPlayerActionStage stage,
TimeSpan duration
)
{
return stage with { Deadline = DateTimeOffset.UtcNow + duration };
}
}
3 changes: 3 additions & 0 deletions Project.App/Project.Api/Models/Games/GameConfig.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System.Text.Json.Serialization;

namespace Project.Api.Models.Games;

/// <summary>
/// Represents the configuration for a game.
/// Should be extended with settings for a specific game type.
/// </summary>
[JsonDerivedType(typeof(BlackjackConfig), typeDiscriminator: "blackjack")]
public abstract record GameConfig
{
public virtual int? MaxPlayers { get; set; }
Expand Down
90 changes: 90 additions & 0 deletions Project.App/Project.Api/Models/Games/RoomEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Project.Api.DTOs;

namespace Project.Api.Models.Games;

/// <summary>
/// Base interface for broadcast game events
/// </summary>
public interface IRoomEventData { }

/// <summary>
/// Specific DTO for a chat message event
/// </summary>
public class MessageEventData : IRoomEventData
{
public string Sender { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
}

/// <summary>
/// Specific DTO for a game state update event
/// </summary>
public record GameStateUpdateEventData : IRoomEventData
{
public required BlackjackStage CurrentStage { get; set; }
}

/// <summary>
/// Specific DTO for a player action event.
/// Provides some context about the action taken by a player that any action might use.
/// </summary>
public record PlayerActionEventData : IRoomEventData
{
public Guid PlayerId { get; set; }
public int HandIndex { get; set; } = 0;
public string Action { get; set; } = string.Empty;

// TODO: make event data type-safe
public long? Amount { get; set; } = 0;
public List<CardDTO>? Cards { get; set; }
public Guid? TargetPlayerId { get; set; }
public bool? Success { get; set; } = true;
}

/// <summary>
/// Specific DTO for a player join event
/// </summary>
public record PlayerJoinEventData : IRoomEventData
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
}

/// <summary>
/// Specific DTO for a player leave event
/// </summary>
public record PlayerLeaveEventData : IRoomEventData
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
}

/// <summary>
/// Specific DTO for a host change event (if the current host leaves)
/// </summary>
public record HostChangeEventData : IRoomEventData
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
}

/// <summary>
/// Specific DTO for revealing the dealer's cards
/// </summary>
public record DealerRevealEventData : IRoomEventData
{
public List<CardDTO> DealerHand { get; set; } = [];
public int DealerScore { get; set; }
}

/// <summary>
/// Specific DTO for revealing scores at the end of a round
/// </summary>
public record PlayerRevealEventData : IRoomEventData
{
public Guid PlayerId { get; set; }
public int HandIndex { get; set; } = 0;
public List<CardDTO> PlayerHand { get; set; } = [];
public int PlayerScore { get; set; }
}
6 changes: 4 additions & 2 deletions Project.App/Project.Api/Models/Hand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ public class Hand

public Guid RoomPlayerId { get; set; }

public int Order { get; set; }
public required int Order { get; set; } // order of the player in the game

public int Bet { get; set; }
public int HandNumber { get; set; } = 0; // number of the hand for the player

public long Bet { get; set; }

[ForeignKey("RoomPlayerId")]
public virtual RoomPlayer? RoomPlayer { get; set; }
Expand Down
6 changes: 3 additions & 3 deletions Project.App/Project.Api/Models/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ public class Room

public bool IsActive { get; set; }

public DateTime CreatedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }

public DateTime? StartedAt { get; set; }
public DateTimeOffset? StartedAt { get; set; }

public DateTime? EndedAt { get; set; }
public DateTimeOffset? EndedAt { get; set; }

[MaxLength(50)]
public required string GameMode { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Project.App/Project.Api/Models/RoomPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class RoomPlayer

public Role Role { get; set; }

public Status Status { get; set; }
public Status Status { get; set; } = Status.Active;

[Required]
public long Balance { get; set; }
Expand Down
41 changes: 22 additions & 19 deletions Project.App/Project.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.EntityFrameworkCore;
using Project.Api.Data;
using Project.Api.Models.Games;
using Project.Api.Repositories;
using Project.Api.Repositories.Interface;
using Project.Api.Services;
Expand Down Expand Up @@ -37,7 +38,8 @@ public static void Main(string[] args)
builder.Host.UseSerilog();

// use extension methods to configure services
builder.Services.AddDatabase(builder.Configuration);
if (!builder.Environment.IsEnvironment("Testing"))
builder.Services.AddDatabase(builder.Configuration); // do not add database in testing
builder.Services.AddApplicationServices();
builder.Services.AddCorsPolicy();
builder.Services.AddAuth(builder.Configuration, builder.Environment);
Expand All @@ -49,23 +51,8 @@ public static void Main(string[] args)

var app = builder.Build();

// optional/safe auto-migrate in other words our startup wont crash if the our migration model doenst match the DB
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
db.Database.Migrate();
Log.Information("[Startup] Database migration check complete.");
}
catch (Exception ex)
{
Log.Warning(
ex,
"[Startup] Database migration skipped or failed. Continuing startup."
);
}
}
if (!builder.Environment.IsEnvironment("Testing"))
app.ApplyMigrations(); // there's no database in testing

app.UseMiddleware<GlobalExceptionHandler>();

Expand Down Expand Up @@ -143,7 +130,7 @@ IConfiguration configuration
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// scoped services
services.AddScoped<IBlackjackService, BlackjackService>();
services.AddScoped<IGameService<IGameState, GameConfig>, BlackjackService>();
services.AddScoped<IHandService, HandService>();
services.AddScoped<IRoomService, RoomService>();
services.AddScoped<IUserService, UserService>();
Expand Down Expand Up @@ -320,4 +307,20 @@ await response.Content.ReadAsStringAsync()

return services;
}

public static void ApplyMigrations(this WebApplication app)
{
// optional/safe auto-migrate in other words our startup wont crash if the our migration model doenst match the DB
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
db.Database.Migrate();
Log.Information("[Startup] Database migration check complete.");
}
catch (Exception ex)
{
Log.Warning(ex, "[Startup] Database migration skipped or failed. Continuing startup.");
}
}
}
Loading