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
45 changes: 45 additions & 0 deletions Project.App/Project.Api/DTOs/BlackjackActionDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Text.Json;

namespace Project.Api.DTOs;

/// <summary>
/// The base class for all blackjack-specific action DTOs.
/// </summary>
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
{
/// <summary>
/// Extension method to deserialize a <see cref="BlackjackActionDTO"/> from a <see cref="JsonElement"/>.
/// Any errors thrown during deserialization will be passed to the caller.
/// </summary>
public static BlackjackActionDTO ToBlackjackAction(this JsonElement element, string action)
{
return (
action switch
{
"bet" => element.Deserialize<BetAction>(),
"hit" => element.Deserialize<HitAction>(),
"stand" => element.Deserialize<StandAction>(),
"double" => element.Deserialize<DoubleAction>(),
"split" => element.Deserialize<SplitAction>(),
"surrender" => (BlackjackActionDTO?)element.Deserialize<SurrenderAction>(),
_ => throw new NotSupportedException(
$"Action '{action}' is not a valid action for Blackjack."
),
}
) ?? throw new InvalidOperationException($"Could not deserialize action {action}.");
}
}
6 changes: 6 additions & 0 deletions Project.App/Project.Api/DTOs/GameActionDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Project.Api.DTOs;

/// <summary>
/// The base class for all game action DTOs. Does nothing.
/// </summary>
public abstract record GameActionDTO;
110 changes: 110 additions & 0 deletions Project.App/Project.Api/Services/BlackjackService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Text.Json;
using Project.Api.DTOs;
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 BlackjackPlayerActionStage(int Index) : BlackjackStage;

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

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

#endregion

public class BlackjackService : IBlackjackService
{
public Task<BlackjackState> GetGamestateAsync(string gameId)
{
throw new NotImplementedException();
}

public Task<bool> 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();
}
}
12 changes: 11 additions & 1 deletion Project.App/Project.Api/Services/Interface/IBlackjackService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
namespace Project.Api.Services.Interface;

public interface IBlackjackService : IGameService { }
public abstract record BlackjackStage : GameStage;

public record BlackjackState : GameState<BlackjackStage>
{
public required List<object> DealerHand { get; set; } = [];
}

/// <summary>
/// Represents a service for handling blackjack logic.
/// </summary>
public interface IBlackjackService : IGameService<BlackjackState> { }
96 changes: 31 additions & 65 deletions Project.App/Project.Api/Services/Interface/IGameService.cs
Original file line number Diff line number Diff line change
@@ -1,72 +1,38 @@
namespace Project.Api.Services.Interface;

//
// start -> pre-game -> ||: pre-round -> | player turn (foreach player) | -> post-round :|| -> post-game
//
using System.Text.Json;

public enum GameStage
{
PreGame,
PreRound,
PlayerTurn,
PostRound,
PostGame,
}
namespace Project.Api.Services.Interface;

public record GameState
/// <summary>
/// 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.
/// </summary>
public abstract record GameStage;

/// <summary>
/// Represents the current state of a game, including all relevant information.
/// Can be extended to include more information for a specific game type.
/// </summary>
public record GameState<TStage>
where TStage : GameStage
{
public GameStage Stage { get; set; }
public required TStage CurrentStage { get; set; }
}

public interface IGameService
/// <summary>
/// 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.
/// </summary>
public interface IGameService<TState>
{
Task<bool> DoPreGameAsync();
Task<bool> DoPreRoundAsync();
Task<bool> DoPlayerTurnAsync();
Task<bool> DoPostRoundAsync();
Task<bool> DoPostGameAsync();

Task<GameState> GetGamestateAsync(string gameId);

// async Task<GameState> GetGamestateAsync(string gameId)
// {
// return _gameRepository.GetGamestateAsync(gameId);
// }

async Task<bool> 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<TState> GetGamestateAsync(string gameId);

/// <summary>
/// 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.
/// </summary>
/// <param name="action">The action to perform</param>
/// <param name="data">A JSON object containing the action data</param>
Task<bool> PerformActionAsync(string gameId, string playerId, string action, JsonElement data);
}
Loading