From abe25169e6927445968cf459d547e9fe43a1f291 Mon Sep 17 00:00:00 2001 From: alt-potato Date: Wed, 15 Oct 2025 16:10:25 -0400 Subject: [PATCH 1/3] Add blackjack service interfaces --- .../Project.Api/Services/BlackjackService.cs | 83 +++++++++++++++++ .../Services/Interface/IBlackjackService.cs | 12 ++- .../Services/Interface/IGameService.cs | 92 ++++++------------- 3 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 Project.App/Project.Api/Services/BlackjackService.cs diff --git a/Project.App/Project.Api/Services/BlackjackService.cs b/Project.App/Project.Api/Services/BlackjackService.cs new file mode 100644 index 0000000..9501250 --- /dev/null +++ b/Project.App/Project.Api/Services/BlackjackService.cs @@ -0,0 +1,83 @@ +using Project.Api.Services.Interface; + +namespace Project.Api.Services; + +/* + +set up deck API connection +set up game configs + +loop + shuffle deck(?) + + loop + if everyone has bet/left or time is up + break + end if + wait + end loop + deduct bets + + deal 2 cards to each player + deal 2 cards to dealer (one hidden) + + loop (foreach player) + loop + if hit + deal card + else if stand + break + end if + end loop + end loop + + deal to dealer (hit until 17) + calculate scores + determine outcomes + distribute winnings +end loop + +teardown +close room + +*/ + +#region Blackjack Game Stages + +// initial setup +// initialize deck, set game configs +public record BlackjackInitStage : BlackjackStage; + +// doing pre-round setup +public record BlackjackSetupStage : BlackjackStage; + +// waiting for players to bet +public record BlackjackBettingStage : BlackjackStage; + +// dealing +public record BlackjackDealingStage : BlackjackStage; + +// player turn +// TODO: figure out how turn order will work +public record BlackjackPlayerTurnStage(int Index) : BlackjackStage; + +// dealer turn and distribute winnings +public record BlackjackDealerTurnStage : BlackjackStage; + +// teardown, close room +public record BlackjackTeardownStage : BlackjackStage; + +#endregion + +public class BlackjackService : IBlackjackService +{ + public Task GetGamestateAsync(string gameId) + { + throw new NotImplementedException(); + } + + public Task PerformActionAsync(string gameId, string playerId, string action) + { + throw new NotImplementedException(); + } +} diff --git a/Project.App/Project.Api/Services/Interface/IBlackjackService.cs b/Project.App/Project.Api/Services/Interface/IBlackjackService.cs index 86e9e6c..c4b4211 100644 --- a/Project.App/Project.Api/Services/Interface/IBlackjackService.cs +++ b/Project.App/Project.Api/Services/Interface/IBlackjackService.cs @@ -1,3 +1,13 @@ namespace Project.Api.Services.Interface; -public interface IBlackjackService : IGameService { } +public abstract record BlackjackStage : GameStage; + +public record BlackjackState : GameState +{ + public required List DealerHand { get; set; } = []; +} + +/// +/// Represents a service for handling blackjack logic. +/// +public interface IBlackjackService : IGameService { } diff --git a/Project.App/Project.Api/Services/Interface/IGameService.cs b/Project.App/Project.Api/Services/Interface/IGameService.cs index 1d47c95..4f916a1 100644 --- a/Project.App/Project.Api/Services/Interface/IGameService.cs +++ b/Project.App/Project.Api/Services/Interface/IGameService.cs @@ -1,72 +1,34 @@ namespace Project.Api.Services.Interface; -// -// start -> pre-game -> ||: pre-round -> | player turn (foreach player) | -> post-round :|| -> post-game -// - -public enum GameStage -{ - PreGame, - PreRound, - PlayerTurn, - PostRound, - PostGame, -} - -public record GameState +/// +/// Used to represent the current state of a game. +/// Each game should have their own "parent" abstract class that extends this class to ensure type safety. +/// +public abstract record GameStage; + +/// +/// Represents the current state of a game, including all relevant information. +/// Can be extended to include more information for a specific game type. +/// +public record GameState + where TStage : GameStage { - public GameStage Stage { get; set; } + public required TStage CurrentStage { get; set; } } -public interface IGameService +/// +/// A service for handling game logic. +/// Actions are async and stateless, so for each request, the service needs to get the current game state and +/// act accordingly, either by updating the game state or returning an error. +/// +public interface IGameService { - Task DoPreGameAsync(); - Task DoPreRoundAsync(); - Task DoPlayerTurnAsync(); - Task DoPostRoundAsync(); - Task DoPostGameAsync(); - - Task GetGamestateAsync(string gameId); - - // async Task GetGamestateAsync(string gameId) - // { - // return _gameRepository.GetGamestateAsync(gameId); - // } - - async Task DoGameActionAsync(string gameId, string playerId, string action) - { - GameState gamestate = await GetGamestateAsync(gameId); - return gamestate.Stage switch - { - GameStage.PreGame => await DoPreGameAsync(), - GameStage.PreRound => await DoPreRoundAsync(), - GameStage.PlayerTurn => await DoPlayerTurnAsync(), - GameStage.PostRound => await DoPostRoundAsync(), - GameStage.PostGame => await DoPostGameAsync(), - _ => throw new Exception("Unknown game stage"), - }; - } - - // if this was not distributed, this is how it would work - // BUT gamestate is tracked in the database, so it needs to be able to load the gamestate at any point - // async Task MainLoopAsync() - // { - // await DoPreGameAsync(); - - // // main loop - // bool isRunning = true; - // while (isRunning) - // { - // await DoPreRoundAsync(); - - // // TODO: foreach player - // // { - // // await DoPlayerTurnAsync(); - // // } - - // await DoPostRoundAsync(); - // } - - // await DoPostGameAsync(); - // } + Task GetGamestateAsync(string gameId); + + /// + /// Performs a user action on the game, if valid, then updates the game state. + /// Each game implementation provides their own main loop logic implementation, instead of being restricted to a + /// specific structure. + /// + Task PerformActionAsync(string gameId, string playerId, string action); } From cf96213ba739250e72bbb86834b4a80993d51784 Mon Sep 17 00:00:00 2001 From: alt-potato Date: Wed, 15 Oct 2025 17:07:14 -0400 Subject: [PATCH 2/3] Implement game action DTOs --- .../Project.Api/DTOs/BlackjackActionDTO.cs | 45 +++++++++++++++++++ Project.App/Project.Api/DTOs/GameActionDTO.cs | 6 +++ .../Project.Api/Services/BlackjackService.cs | 33 ++++++++++++-- .../Services/Interface/IGameService.cs | 6 ++- 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Project.App/Project.Api/DTOs/BlackjackActionDTO.cs create mode 100644 Project.App/Project.Api/DTOs/GameActionDTO.cs diff --git a/Project.App/Project.Api/DTOs/BlackjackActionDTO.cs b/Project.App/Project.Api/DTOs/BlackjackActionDTO.cs new file mode 100644 index 0000000..2e29b8d --- /dev/null +++ b/Project.App/Project.Api/DTOs/BlackjackActionDTO.cs @@ -0,0 +1,45 @@ +using System.Text.Json; + +namespace Project.Api.DTOs; + +/// +/// The base class for all blackjack-specific action DTOs. +/// +public abstract record BlackjackActionDTO : GameActionDTO; + +public record BetAction(long Amount) : BlackjackActionDTO; + +public record HitAction : BlackjackActionDTO; + +public record StandAction : BlackjackActionDTO; + +public record DoubleAction : BlackjackActionDTO; + +public record SplitAction : BlackjackActionDTO; + +public record SurrenderAction : BlackjackActionDTO; + +public static class JsonElementExtensions +{ + /// + /// Extension method to deserialize a from a . + /// Any errors thrown during deserialization will be passed to the caller. + /// + public static BlackjackActionDTO ToBlackjackAction(this JsonElement element, string action) + { + return ( + action switch + { + "bet" => element.Deserialize(), + "hit" => element.Deserialize(), + "stand" => element.Deserialize(), + "double" => element.Deserialize(), + "split" => element.Deserialize(), + "surrender" => (BlackjackActionDTO?)element.Deserialize(), + _ => throw new NotSupportedException( + $"Action '{action}' is not a valid action for Blackjack." + ), + } + ) ?? throw new InvalidOperationException($"Could not deserialize action {action}."); + } +} diff --git a/Project.App/Project.Api/DTOs/GameActionDTO.cs b/Project.App/Project.Api/DTOs/GameActionDTO.cs new file mode 100644 index 0000000..0f1f13f --- /dev/null +++ b/Project.App/Project.Api/DTOs/GameActionDTO.cs @@ -0,0 +1,6 @@ +namespace Project.Api.DTOs; + +/// +/// The base class for all game action DTOs. Does nothing. +/// +public abstract record GameActionDTO; diff --git a/Project.App/Project.Api/Services/BlackjackService.cs b/Project.App/Project.Api/Services/BlackjackService.cs index 9501250..1c1861e 100644 --- a/Project.App/Project.Api/Services/BlackjackService.cs +++ b/Project.App/Project.Api/Services/BlackjackService.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Project.Api.DTOs; using Project.Api.Services.Interface; namespace Project.Api.Services; @@ -59,10 +61,10 @@ public record BlackjackDealingStage : BlackjackStage; // player turn // TODO: figure out how turn order will work -public record BlackjackPlayerTurnStage(int Index) : BlackjackStage; +public record BlackjackPlayerActionStage(int Index) : BlackjackStage; // dealer turn and distribute winnings -public record BlackjackDealerTurnStage : BlackjackStage; +public record BlackjackFinishRoundStage : BlackjackStage; // teardown, close room public record BlackjackTeardownStage : BlackjackStage; @@ -76,8 +78,33 @@ public Task GetGamestateAsync(string gameId) throw new NotImplementedException(); } - public Task PerformActionAsync(string gameId, string playerId, string action) + public Task PerformActionAsync( + string gameId, + string playerId, + string action, + JsonElement data + ) { + BlackjackActionDTO actionDTO = data.ToBlackjackAction(action); + + switch (actionDTO) + { + case BetAction betAction: + throw new NotImplementedException(); + case HitAction hitAction: + throw new NotImplementedException(); + case StandAction standAction: + throw new NotImplementedException(); + case DoubleAction doubleAction: + throw new NotImplementedException(); + case SplitAction splitAction: + throw new NotImplementedException(); + case SurrenderAction surrenderAction: + throw new NotImplementedException(); + default: + throw new NotImplementedException(); + } + throw new NotImplementedException(); } } diff --git a/Project.App/Project.Api/Services/Interface/IGameService.cs b/Project.App/Project.Api/Services/Interface/IGameService.cs index 4f916a1..1eeafac 100644 --- a/Project.App/Project.Api/Services/Interface/IGameService.cs +++ b/Project.App/Project.Api/Services/Interface/IGameService.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace Project.Api.Services.Interface; /// @@ -30,5 +32,7 @@ public interface IGameService /// Each game implementation provides their own main loop logic implementation, instead of being restricted to a /// specific structure. /// - Task PerformActionAsync(string gameId, string playerId, string action); + /// The action to perform + /// A JSON object containing the action data + Task PerformActionAsync(string gameId, string playerId, string action, JsonElement data); } From 97f1f9df9dfa984bcef3595d9bd85801f75d9655 Mon Sep 17 00:00:00 2001 From: alt-potato Date: Wed, 15 Oct 2025 17:10:36 -0400 Subject: [PATCH 3/3] Clean up formatting --- Project.App/Project.Api/Data/AppDbContext.cs | 1 - Project.App/Project.Api/Data/MappingProfile.cs | 2 +- Project.App/Project.Api/Models/Room.cs | 12 +++++------- Project.App/Project.Api/Models/RoomPlayer.cs | 3 ++- Project.App/Project.Api/Models/User.cs | 12 +++++++----- .../Project.Api/Repositories/RepositoryExample.cs | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Project.App/Project.Api/Data/AppDbContext.cs b/Project.App/Project.Api/Data/AppDbContext.cs index 0497cf8..c565f6a 100644 --- a/Project.App/Project.Api/Data/AppDbContext.cs +++ b/Project.App/Project.Api/Data/AppDbContext.cs @@ -14,7 +14,6 @@ public AppDbContext(DbContextOptions options) public DbSet Rooms { get; set; } public DbSet RoomPlayers { get; set; } public DbSet Hands { get; set; } - /// /// Provides the configuration for TradeHubContext models. diff --git a/Project.App/Project.Api/Data/MappingProfile.cs b/Project.App/Project.Api/Data/MappingProfile.cs index d5176a5..48c5e06 100644 --- a/Project.App/Project.Api/Data/MappingProfile.cs +++ b/Project.App/Project.Api/Data/MappingProfile.cs @@ -1,7 +1,7 @@ using System.Net.Http.Headers; using AutoMapper; -using Project.DTOs; using Project.Api.Models; +using Project.DTOs; namespace Project.Data { diff --git a/Project.App/Project.Api/Models/Room.cs b/Project.App/Project.Api/Models/Room.cs index 544e509..3885eca 100644 --- a/Project.App/Project.Api/Models/Room.cs +++ b/Project.App/Project.Api/Models/Room.cs @@ -1,14 +1,12 @@ namespace Project.Api.Models; + using System.ComponentModel.DataAnnotations; public class Room { - [Key] public Guid Id {get; set;} + [Key] + public Guid Id { get; set; } - public long HostId {get;set;} + public long HostId { get; set; } public User Host { get; set; } = null!; - - - - -} \ No newline at end of file +} diff --git a/Project.App/Project.Api/Models/RoomPlayer.cs b/Project.App/Project.Api/Models/RoomPlayer.cs index e8086d9..96921e9 100644 --- a/Project.App/Project.Api/Models/RoomPlayer.cs +++ b/Project.App/Project.Api/Models/RoomPlayer.cs @@ -7,12 +7,13 @@ public class RoomPlayer { [Key] public long Id { get; set; } + [Required] public Guid RoomId { get; set; } + [Required] public long UserId { get; set; } // [Required] // public PlayerRole Role { get; set; } } - } diff --git a/Project.App/Project.Api/Models/User.cs b/Project.App/Project.Api/Models/User.cs index 8017e3d..e026434 100644 --- a/Project.App/Project.Api/Models/User.cs +++ b/Project.App/Project.Api/Models/User.cs @@ -7,16 +7,18 @@ namespace Project.Api.Models public class User { [Key] - public int Id {get; set;} + public int Id { get; set; } + [Required, MaxLength(256)] - public string Name { get; set;} = null!; + public string Name { get; set; } = null!; + [Required, MaxLength(256)] - public string Email {get; set;} = null!; + public string Email { get; set; } = null!; + [Required, MaxLength(256)] - public string PasswordHash {get; set;} = null!; + public string PasswordHash { get; set; } = null!; public double Balance { get; set; } = 1000; public ICollection RoomPlayers { get; set; } = new List(); - } } diff --git a/Project.App/Project.Api/Repositories/RepositoryExample.cs b/Project.App/Project.Api/Repositories/RepositoryExample.cs index 836bf29..0dd0f2c 100644 --- a/Project.App/Project.Api/Repositories/RepositoryExample.cs +++ b/Project.App/Project.Api/Repositories/RepositoryExample.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -using Project.Data; using Project.Api.Models; +using Project.Data; namespace Project.Api.Repositories {