mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
* Minimize amount of lines blackjack needs * selfdestruct sloppa images * massivly reduce amount of time slot graphic stays in chat
576 lines
25 KiB
C#
576 lines
25 KiB
C#
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using KfChatDotNetBot.Extensions;
|
|
using KfChatDotNetBot.Models;
|
|
using KfChatDotNetBot.Models.DbModels;
|
|
using KfChatDotNetBot.Services;
|
|
using KfChatDotNetBot.Settings;
|
|
using KfChatDotNetWsClient.Models.Events;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NLog;
|
|
|
|
namespace KfChatDotNetBot.Commands.Kasino;
|
|
|
|
[KasinoCommand]
|
|
[WagerCommand]
|
|
public class BlackjackCommand : ICommand
|
|
{
|
|
private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5);
|
|
|
|
// Colors fetched once per user action and threaded through to all helpers,
|
|
// so we never fetch them redundantly mid-game-flow.
|
|
private record GameColors(string Green, string Red);
|
|
|
|
public List<Regex> Patterns => [
|
|
new Regex(@"^blackjack (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
|
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
|
new Regex(@"^bj (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
|
new Regex(@"^bj (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
|
new Regex(@"^blackjack (?<action>hit|stand|double|split)$", RegexOptions.IgnoreCase),
|
|
new Regex(@"^bj (?<action>hit|stand|double|split)$", RegexOptions.IgnoreCase)
|
|
];
|
|
|
|
public string? HelpText => "!blackjack <amount> or !bj <amount> to start, then !bj hit/stand/double/split";
|
|
public UserRight RequiredRight => UserRight.Loser;
|
|
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
|
|
public RateLimitOptionsModel? RateLimitOptions => new()
|
|
{
|
|
MaxInvocations = 5,
|
|
Window = TimeSpan.FromSeconds(20),
|
|
Flags = RateLimitFlags.NoAutoDeleteCooldownResponse
|
|
};
|
|
|
|
private ApplicationDbContext _dbContext = new();
|
|
|
|
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
|
CancellationToken ctx)
|
|
{
|
|
var settings = await SettingsProvider.GetMultipleValuesAsync([
|
|
BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoBlackjackCleanupDelay,
|
|
BuiltIn.Keys.KasinoBlackjackEnabled
|
|
]);
|
|
|
|
var blackjackEnabled = settings[BuiltIn.Keys.KasinoBlackjackEnabled].ToBoolean();
|
|
if (!blackjackEnabled)
|
|
{
|
|
var gameDisabledCleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>());
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, blackjack is currently disabled.",
|
|
true, autoDeleteAfter: gameDisabledCleanupDelay);
|
|
return;
|
|
}
|
|
|
|
var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType<int>());
|
|
|
|
if (arguments.TryGetValue("amount", out var amountGroup))
|
|
{
|
|
await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
if (arguments.TryGetValue("action", out var actionGroup))
|
|
{
|
|
await ContinueGame(botInstance, user, actionGroup.Value.ToLower(), cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException($"User {user.KfUsername} somehow ran blackjack without an amount or action: {message.MessageRaw}");
|
|
}
|
|
|
|
|
|
private async Task StartNewGame(ChatBot botInstance, UserDbModel user, string amountStr,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var logger = LogManager.GetCurrentClassLogger();
|
|
|
|
// Fetch colors upfront — needed for both the immediate-blackjack ResolveGame path
|
|
// and the normal GameStart display path.
|
|
var colorSettings = await SettingsProvider.GetMultipleValuesAsync([
|
|
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
|
]);
|
|
var colors = new GameColors(
|
|
colorSettings[BuiltIn.Keys.KiwiFarmsGreenColor].Value,
|
|
colorSettings[BuiltIn.Keys.KiwiFarmsRedColor].Value);
|
|
|
|
var wager = Convert.ToDecimal(amountStr);
|
|
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
|
|
|
if (gambler == null)
|
|
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
|
|
|
if (wager == 0)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you have to wager more than {await wager.FormatKasinoCurrencyAsync()}",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
if (gambler.Balance < wager)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
// Check for an existing incomplete game
|
|
var existingGame = await _dbContext.Wagers
|
|
.OrderBy(x => x.Id)
|
|
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
|
w.Game == WagerGame.Blackjack &&
|
|
!w.IsComplete && w.GameMeta != null,
|
|
cancellationToken: ctx);
|
|
|
|
if (existingGame != null)
|
|
{
|
|
try
|
|
{
|
|
_ = JsonSerializer.Deserialize<BlackjackGameMetaModel>(existingGame.GameMeta!) ??
|
|
throw new InvalidOperationException();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error($"Caught error when deserializing meta for wager ID {existingGame.Id}");
|
|
logger.Error(e);
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, somehow your previous blackjack game state got messed up. Please try again",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
existingGame.IsComplete = true;
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
throw;
|
|
}
|
|
|
|
var timeSinceStart = DateTimeOffset.UtcNow - existingGame.Time;
|
|
if (timeSinceStart > GameTimeout)
|
|
{
|
|
await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you already have an active blackjack game. Use !bj hit or !bj stand to continue.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
return;
|
|
}
|
|
|
|
// Deal initial hands
|
|
var deck = BlackjackHelper.CreateDeck(gambler);
|
|
var playerHand = new List<Card> { deck[0], deck[2] };
|
|
var dealerHand = new List<Card> { deck[1], deck[3] };
|
|
deck.RemoveRange(0, 4);
|
|
|
|
var newGameState = new BlackjackGameMetaModel
|
|
{
|
|
PlayerHands = new List<List<Card>> { playerHand },
|
|
DealerHand = dealerHand,
|
|
Deck = deck,
|
|
HasDoubledDown = false,
|
|
CurrentHandIndex = 0,
|
|
OriginalWagerAmount = wager
|
|
};
|
|
|
|
await Money.NewWagerAsync(
|
|
gambler.Id, wager, -wager,
|
|
WagerGame.Blackjack,
|
|
autoModifyBalance: true,
|
|
gameMeta: newGameState,
|
|
isComplete: false,
|
|
ct: ctx);
|
|
|
|
var createdWager = await _dbContext.Wagers
|
|
.OrderBy(x => x.Id)
|
|
.LastOrDefaultAsync(
|
|
w => w.Gambler.Id == gambler.Id && w.Game == WagerGame.Blackjack && !w.IsComplete && w.GameMeta != null,
|
|
cancellationToken: ctx) ?? throw new InvalidOperationException();
|
|
createdWager.GameMeta = JsonSerializer.Serialize(newGameState);
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
// Immediate blackjack check — goes straight to resolution
|
|
if (BlackjackHelper.IsBlackjack(playerHand) || BlackjackHelper.IsBlackjack(dealerHand))
|
|
{
|
|
await ResolveGame(botInstance, user, gambler, createdWager, newGameState, colors, cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
var playerValue = BlackjackHelper.CalculateHandValue(playerHand);
|
|
var canSplit = BlackjackHelper.CanSplit(playerHand);
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
await BlackjackDisplay.GameStart(user, wager, playerHand, playerValue, dealerHand, canSplit, colors.Red),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
}
|
|
|
|
|
|
private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
// Fetch colors once here; pass them to every downstream method
|
|
var colorSettings = await SettingsProvider.GetMultipleValuesAsync([
|
|
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
|
]);
|
|
var colors = new GameColors(
|
|
colorSettings[BuiltIn.Keys.KiwiFarmsGreenColor].Value,
|
|
colorSettings[BuiltIn.Keys.KiwiFarmsRedColor].Value);
|
|
|
|
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
|
|
|
if (gambler == null)
|
|
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
|
|
|
var activeWager = await _dbContext.Wagers
|
|
.OrderBy(x => x.Id)
|
|
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
|
w.Game == WagerGame.Blackjack &&
|
|
!w.IsComplete && w.GameMeta != null,
|
|
cancellationToken: ctx);
|
|
|
|
if (activeWager == null)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you don't have an active blackjack game. Start one with !bj <amount>",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
var currentGameState = JsonSerializer.Deserialize<BlackjackGameMetaModel>(activeWager.GameMeta!);
|
|
if (currentGameState == null)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, your game data is corrupted. Please start a new game.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
activeWager.IsComplete = true;
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
return;
|
|
}
|
|
|
|
var timeSinceStart = DateTimeOffset.UtcNow - activeWager.Time;
|
|
if (timeSinceStart > GameTimeout)
|
|
{
|
|
await ForfeitGame(botInstance, user, gambler, activeWager, cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case "hit":
|
|
await HandleHit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
|
break;
|
|
case "stand":
|
|
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
|
break;
|
|
case "double":
|
|
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
|
break;
|
|
case "split":
|
|
await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
if (gameState.Deck.Count == 0)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, game error: no cards left in deck. Game forfeited.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
await ForfeitGame(botInstance, user, gambler, wager, cleanupDelay, ctx);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
|
var handLabel = gameState.PlayerHands.Count > 1 ? $" (H{gameState.CurrentHandIndex + 1})" : "";
|
|
|
|
var card = gameState.Deck[0];
|
|
gameState.Deck.RemoveAt(0);
|
|
currentHand.Add(card);
|
|
|
|
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
|
|
bool handEnded = playerValue > 21 || playerValue == 21 || gameState.HasDoubledDown;
|
|
|
|
if (!handEnded)
|
|
{
|
|
// Hand is still live — show updated state and prompt for next action
|
|
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
await botInstance.SendChatMessageAsync(
|
|
BlackjackDisplay.HitInProgress(user, card, currentHand, playerValue, gameState.DealerHand, handLabel, colors.Red),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
return;
|
|
}
|
|
|
|
// Hand ended (bust / 21 / post-double auto-stand).
|
|
// MoveToNextHandOrResolve sends the combined transition message when moving
|
|
// to the next split hand, or falls through silently to ResolveGame.
|
|
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState,
|
|
currentHand, busted: playerValue > 21, colors, cleanupDelay, ctx);
|
|
}
|
|
|
|
|
|
private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
|
|
|
// No stand message needed here — MoveToNextHandOrResolve handles all output:
|
|
// a combined split-transition message when moving to the next hand, and
|
|
// silence when falling through to the final resolution.
|
|
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState,
|
|
currentHand, busted: false, colors, cleanupDelay, ctx);
|
|
}
|
|
|
|
|
|
private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
|
|
|
if (currentHand.Count != 2)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you can only double down on your first action.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
return;
|
|
}
|
|
|
|
if (gameState.PlayerHands.Count > 1)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you cannot double down after splitting.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
return;
|
|
}
|
|
|
|
if (gambler.Balance < gameState.OriginalWagerAmount)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you don't have enough balance to double down.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
return;
|
|
}
|
|
|
|
var additionalWager = gameState.OriginalWagerAmount;
|
|
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
|
$"Double down for {wager.Id}", ct: ctx);
|
|
wager.WagerAmount += additionalWager;
|
|
wager.WagerEffect -= additionalWager;
|
|
gameState.HasDoubledDown = true;
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
// Confirm the double, then let HandleHit draw the one card and auto-stand.
|
|
// HasDoubledDown is now true, so HandleHit treats the hand as ended and falls
|
|
// through silently to ResolveGame — just two total messages: this + the result.
|
|
await botInstance.SendChatMessageAsync(
|
|
await BlackjackDisplay.DoubledDown(user, wager.WagerAmount),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
|
|
await HandleHit(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
|
}
|
|
|
|
|
|
private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
|
|
|
if (!BlackjackHelper.CanSplit(currentHand))
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you can only split with two cards of the same rank.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
if (gameState.PlayerHands.Count > 1)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you can only split once per game.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
if (gambler.Balance < gameState.OriginalWagerAmount)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, you don't have enough balance to split.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
if (gameState.Deck.Count < 2)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, not enough cards in deck to split.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
return;
|
|
}
|
|
|
|
var card1 = currentHand[0];
|
|
var card2 = currentHand[1];
|
|
var hand1 = new List<Card> { card1, gameState.Deck[0] };
|
|
var hand2 = new List<Card> { card2, gameState.Deck[1] };
|
|
gameState.Deck.RemoveRange(0, 2);
|
|
|
|
gameState.PlayerHands = new List<List<Card>> { hand1, hand2 };
|
|
gameState.HasDoubledDown = false;
|
|
gameState.CurrentHandIndex = 0;
|
|
|
|
var additionalWager = gameState.OriginalWagerAmount;
|
|
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
|
$"Split for {wager.Id}", ct: ctx);
|
|
wager.WagerAmount += additionalWager;
|
|
wager.WagerEffect -= additionalWager;
|
|
|
|
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
var value1 = BlackjackHelper.CalculateHandValue(hand1);
|
|
var value2 = BlackjackHelper.CalculateHandValue(hand2);
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
await BlackjackDisplay.SplitDeal(user, wager.WagerAmount, hand1, value1, hand2, value2, colors.Red),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Advances to the next split hand, or kicks off dealer play and resolution.
|
|
/// </summary>
|
|
/// <param name="finishedHand">The hand that just ended (bust, stand, or doubled auto-stand).</param>
|
|
/// <param name="busted">True if the finished hand went over 21.</param>
|
|
private async Task MoveToNextHandOrResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState,
|
|
List<Card> finishedHand, bool busted,
|
|
GameColors colors, TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var finishedIndex = gameState.CurrentHandIndex;
|
|
gameState.CurrentHandIndex++;
|
|
|
|
if (gameState.CurrentHandIndex < gameState.PlayerHands.Count)
|
|
{
|
|
// More split hands to play — one combined message covers both
|
|
// "what happened to the hand that just ended" and "here's your next hand".
|
|
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
var finishedValue = BlackjackHelper.CalculateHandValue(finishedHand);
|
|
var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
|
var nextValue = BlackjackHelper.CalculateHandValue(nextHand);
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
BlackjackDisplay.SplitTransition(
|
|
finishedIndex, finishedHand, finishedValue, busted,
|
|
gameState.CurrentHandIndex, nextHand, nextValue,
|
|
colors.Red),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
}
|
|
else
|
|
{
|
|
await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
|
}
|
|
}
|
|
|
|
|
|
private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
// Dealer only plays when at least one player hand hasn't busted
|
|
bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21);
|
|
|
|
if (!allHandsBusted)
|
|
{
|
|
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
|
while (dealerValue < 17)
|
|
{
|
|
if (gameState.Deck.Count == 0)
|
|
{
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, game error: dealer ran out of cards. Game forfeited.",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
await ForfeitGame(botInstance, user, gambler, wager, cleanupDelay, ctx);
|
|
return;
|
|
}
|
|
|
|
var card = gameState.Deck[0];
|
|
gameState.Deck.RemoveAt(0);
|
|
gameState.DealerHand.Add(card);
|
|
dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
|
}
|
|
}
|
|
|
|
await ResolveGame(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
|
}
|
|
|
|
|
|
private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
|
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
|
|
bool isSplitGame = gameState.PlayerHands.Count > 1;
|
|
|
|
decimal totalEffect = 0;
|
|
var results = new List<HandResultData>();
|
|
|
|
for (int i = 0; i < gameState.PlayerHands.Count; i++)
|
|
{
|
|
var hand = gameState.PlayerHands[i];
|
|
var playerValue = BlackjackHelper.CalculateHandValue(hand);
|
|
var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
|
|
// Split hands each pay the original per-hand wager; a single hand pays the full
|
|
// (possibly doubled) wager amount already tracked in wager.WagerAmount.
|
|
var handWager = isSplitGame ? gameState.OriginalWagerAmount : wager.WagerAmount;
|
|
|
|
var (outcome, effect) = BlackjackDisplay.ClassifyHand(
|
|
playerValue, playerBlackjack, dealerValue, dealerBlackjack, handWager);
|
|
|
|
results.Add(new HandResultData(i, hand, playerValue, outcome, effect));
|
|
totalEffect += effect;
|
|
}
|
|
|
|
wager.IsComplete = true;
|
|
wager.WagerEffect = totalEffect;
|
|
wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount;
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
var balanceAdjustment = totalEffect + wager.WagerAmount;
|
|
var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment,
|
|
TransactionSourceEventType.Gambling, $"Blackjack outcome from wager {wager.Id}", null, ctx);
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
await BlackjackDisplay.FinalResult(
|
|
user, results, gameState.DealerHand, dealerValue,
|
|
totalEffect, newBalance, isSplitGame, colors.Green, colors.Red),
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
}
|
|
|
|
|
|
private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
|
WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx)
|
|
{
|
|
wager.IsComplete = true;
|
|
await _dbContext.SaveChangesAsync(ctx);
|
|
|
|
await botInstance.SendChatMessageAsync(
|
|
$"{user.FormatUsername()}, your blackjack game timed out and you forfeited {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
|
|
true, autoDeleteAfter: cleanupDelay);
|
|
}
|
|
} |