mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
committed by
GitHub
parent
77dad18e92
commit
df869c6e82
534
KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs
Normal file
534
KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs
Normal file
@@ -0,0 +1,534 @@
|
||||
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;
|
||||
using RandN.Compat;
|
||||
using RandN;
|
||||
|
||||
namespace KfChatDotNetBot.Commands.Kasino;
|
||||
|
||||
[KasinoCommand]
|
||||
[WagerCommand]
|
||||
public class BlackjackCommand : ICommand
|
||||
{
|
||||
private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
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)$", RegexOptions.IgnoreCase),
|
||||
new Regex(@"^bj (?<action>hit|stand|double)$", RegexOptions.IgnoreCase)
|
||||
];
|
||||
|
||||
public string? HelpText => "!blackjack <amount> or !bj <amount> to start, then !bj hit/stand/double";
|
||||
public UserRight RequiredRight => UserRight.Loser;
|
||||
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
|
||||
public RateLimitOptionsModel? RateLimitOptions => new()
|
||||
{
|
||||
MaxInvocations = 5,
|
||||
Window = TimeSpan.FromSeconds(20),
|
||||
Flags = RateLimitFlags.NoAutoDeleteCooldownResponse
|
||||
};
|
||||
|
||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
||||
CancellationToken ctx)
|
||||
{
|
||||
var cleanupDelay = TimeSpan.FromMilliseconds(
|
||||
(await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoDiceCleanupDelay)).ToType<int>());
|
||||
|
||||
try
|
||||
{
|
||||
// Check if this is a new game or continuing existing game
|
||||
if (arguments.TryGetValue("amount", out var amountGroup))
|
||||
{
|
||||
await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx);
|
||||
}
|
||||
else if (arguments.TryGetValue("action", out var actionGroup))
|
||||
{
|
||||
await ContinueGame(botInstance, user, actionGroup.Value.ToLower(), cleanupDelay, ctx);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"[DEBUG] Blackjack error for {user.FormatUsername()}: {ex.Message}", true);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task StartNewGame(ChatBot botInstance, UserDbModel user, string amountStr,
|
||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
|
||||
// Check if user has enough balance
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for existing incomplete blackjack game
|
||||
await using var db = new ApplicationDbContext();
|
||||
var existingGame = await db.Wagers
|
||||
.Where(w => w.Gambler.Id == gambler.Id &&
|
||||
w.Game == WagerGame.Blackjack &&
|
||||
!w.IsComplete)
|
||||
.OrderByDescending(w => w.Id)
|
||||
.FirstOrDefaultAsync(ctx);
|
||||
|
||||
if (existingGame != null)
|
||||
{
|
||||
if (existingGame.GameMeta == null)
|
||||
{
|
||||
// Mark as complete with loss and continue
|
||||
db.Attach(existingGame);
|
||||
existingGame.IsComplete = true;
|
||||
existingGame.WagerEffect = -existingGame.WagerAmount;
|
||||
existingGame.Multiplier = 0m;
|
||||
await db.SaveChangesAsync(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingGameState = existingGame.GameMeta.JsonDeserialize<BlackjackGameMetaModel>();
|
||||
|
||||
if (existingGameState == null)
|
||||
{
|
||||
db.Attach(existingGame);
|
||||
existingGame.IsComplete = true;
|
||||
existingGame.WagerEffect = -existingGame.WagerAmount;
|
||||
existingGame.Multiplier = 0m;
|
||||
await db.SaveChangesAsync(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if game has timed out
|
||||
var timeSinceStart = DateTimeOffset.UtcNow - existingGameState.GameStarted;
|
||||
|
||||
if (timeSinceStart > GameTimeout)
|
||||
{
|
||||
await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, you already have an active blackjack game. Use !bj hit or !bj stand to continue.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create deck and deal initial hands
|
||||
var rng = StandardRng.Create();
|
||||
var random = RandomShim.Create(rng);
|
||||
var deck = BlackjackHelper.CreateDeck(random);
|
||||
|
||||
var playerHand = new List<Card> { deck[0], deck[2] };
|
||||
var dealerHand = new List<Card> { deck[1], deck[3] };
|
||||
deck.RemoveRange(0, 4);
|
||||
|
||||
// Create game state
|
||||
var newGameState = new BlackjackGameMetaModel
|
||||
{
|
||||
WagerId = 0, // Will be set after wager is created
|
||||
PlayerHand = playerHand,
|
||||
DealerHand = dealerHand,
|
||||
Deck = deck,
|
||||
GameStarted = DateTimeOffset.UtcNow,
|
||||
HasDoubledDown = false
|
||||
};
|
||||
|
||||
// Create incomplete wager
|
||||
var newBalance = await Money.NewWagerAsync(
|
||||
gambler.Id,
|
||||
wager,
|
||||
-wager, // This will be the effect for incomplete wagers
|
||||
WagerGame.Blackjack,
|
||||
autoModifyBalance: true,
|
||||
gameMeta: newGameState,
|
||||
isComplete: false,
|
||||
ct: ctx
|
||||
);
|
||||
|
||||
// Update wager ID in game state
|
||||
var createdWager = await db.Wagers
|
||||
.Where(w => w.Gambler.Id == gambler.Id && w.Game == WagerGame.Blackjack && !w.IsComplete)
|
||||
.OrderByDescending(w => w.Id)
|
||||
.FirstAsync(ctx);
|
||||
|
||||
newGameState.WagerId = createdWager.Id;
|
||||
db.Attach(createdWager);
|
||||
createdWager.GameMeta = newGameState.JsonSerialize();
|
||||
await db.SaveChangesAsync(ctx);
|
||||
|
||||
// Check for immediate blackjacks
|
||||
var playerValue = BlackjackHelper.CalculateHandValue(playerHand);
|
||||
var dealerValue = BlackjackHelper.CalculateHandValue(dealerHand);
|
||||
var playerBlackjack = BlackjackHelper.IsBlackjack(playerHand);
|
||||
var dealerBlackjack = BlackjackHelper.IsBlackjack(dealerHand);
|
||||
|
||||
if (playerBlackjack || dealerBlackjack)
|
||||
{
|
||||
await ResolveGame(botInstance, user, gambler, createdWager, newGameState, true, cleanupDelay, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display initial game state
|
||||
var colors = await SettingsProvider.GetMultipleValuesAsync([
|
||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
||||
]);
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"🃏 {user.FormatUsername()} started blackjack with {await wager.FormatKasinoCurrencyAsync()}[br]" +
|
||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(playerHand)} = {playerValue}[br]" +
|
||||
$"[B]Dealer:[/B] {BlackjackHelper.FormatHand(dealerHand, hideFirstCard: true)}[br]" +
|
||||
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
}
|
||||
|
||||
private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action,
|
||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||
|
||||
if (gambler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
||||
}
|
||||
|
||||
// Find active game
|
||||
await using var db = new ApplicationDbContext();
|
||||
var activeWager = await db.Wagers
|
||||
.Where(w => w.Gambler.Id == gambler.Id &&
|
||||
w.Game == WagerGame.Blackjack &&
|
||||
!w.IsComplete)
|
||||
.OrderByDescending(w => w.Id)
|
||||
.FirstOrDefaultAsync(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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeWager.GameMeta == null)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your game data is corrupted. Please start a new game.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentGameState = activeWager.GameMeta.JsonDeserialize<BlackjackGameMetaModel>();
|
||||
if (currentGameState == null)
|
||||
{
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your game data is corrupted. Please start a new game.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check timeout
|
||||
var timeSinceStart = DateTimeOffset.UtcNow - currentGameState.GameStarted;
|
||||
if (timeSinceStart > GameTimeout)
|
||||
{
|
||||
await ForfeitGame(botInstance, user, gambler, activeWager, cleanupDelay, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var rng = StandardRng.Create();
|
||||
var random = RandomShim.Create(rng);
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "hit":
|
||||
await HandleHit(botInstance, user, gambler, activeWager, currentGameState, random, cleanupDelay, ctx);
|
||||
break;
|
||||
case "stand":
|
||||
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
|
||||
break;
|
||||
case "double":
|
||||
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, random, cleanupDelay, ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||
WagerDbModel wager, BlackjackGameMetaModel gameState, Random random, 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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw card
|
||||
var card = gameState.Deck[0];
|
||||
gameState.Deck.RemoveAt(0);
|
||||
gameState.PlayerHand.Add(card);
|
||||
|
||||
var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand);
|
||||
|
||||
if (playerValue > 21)
|
||||
{
|
||||
// Bust - player loses
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()} hit and drew {card}[br]" +
|
||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" +
|
||||
$"[B][COLOR=red]BUST![/COLOR][/B]",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
|
||||
await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx);
|
||||
}
|
||||
else if (gameState.HasDoubledDown)
|
||||
{
|
||||
// Auto-stand after double down hit
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()} hit and drew {card}[br]" +
|
||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
|
||||
await HandleStand(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Continue game
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(wager);
|
||||
wager.GameMeta = gameState.JsonSerialize();
|
||||
await db.SaveChangesAsync(ctx);
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()} hit and drew {card}[br]" +
|
||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" +
|
||||
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
|
||||
// Dealer plays
|
||||
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, false, cleanupDelay, ctx);
|
||||
}
|
||||
|
||||
private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||
WagerDbModel wager, BlackjackGameMetaModel gameState, Random random, TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
// Check if player can double (only on first action with 2 cards)
|
||||
if (gameState.PlayerHand.Count != 2)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, you can only double down on your first action.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player has enough balance for double
|
||||
if (gambler.Balance < wager.WagerAmount)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, you don't have enough balance to double down.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
// Double the wager
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(wager);
|
||||
db.Attach(gambler);
|
||||
|
||||
var additionalWager = wager.WagerAmount;
|
||||
gambler.Balance -= additionalWager;
|
||||
wager.WagerAmount *= 2;
|
||||
wager.WagerEffect -= additionalWager; // Subtract the additional wager
|
||||
|
||||
gameState.HasDoubledDown = true;
|
||||
|
||||
await db.SaveChangesAsync(ctx);
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()} doubled down! Wager is now {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
|
||||
// Draw one card and auto-stand
|
||||
await HandleHit(botInstance, user, gambler, wager, gameState, random, cleanupDelay, ctx);
|
||||
}
|
||||
|
||||
private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||
WagerDbModel wager, BlackjackGameMetaModel gameState, bool immediateResolution,
|
||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand);
|
||||
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
||||
var playerBlackjack = BlackjackHelper.IsBlackjack(gameState.PlayerHand);
|
||||
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
|
||||
|
||||
decimal finalEffect;
|
||||
decimal multiplier;
|
||||
string result;
|
||||
|
||||
var colors = await SettingsProvider.GetMultipleValuesAsync([
|
||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
||||
]);
|
||||
|
||||
// Determine outcome
|
||||
if (playerBlackjack && dealerBlackjack)
|
||||
{
|
||||
finalEffect = 0;
|
||||
multiplier = 1m;
|
||||
result = $"[B][COLOR=orange]PUSH![/COLOR][/B] Both have blackjack";
|
||||
}
|
||||
else if (playerBlackjack)
|
||||
{
|
||||
finalEffect = wager.WagerAmount * 1.5m;
|
||||
multiplier = 2.5m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK![/COLOR][/B]";
|
||||
}
|
||||
else if (dealerBlackjack)
|
||||
{
|
||||
finalEffect = -wager.WagerAmount;
|
||||
multiplier = 0m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK![/COLOR][/B]";
|
||||
}
|
||||
else if (playerValue > 21)
|
||||
{
|
||||
finalEffect = -wager.WagerAmount;
|
||||
multiplier = 0m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]";
|
||||
}
|
||||
else if (dealerValue > 21)
|
||||
{
|
||||
finalEffect = wager.WagerAmount;
|
||||
multiplier = 2m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST - YOU WIN![/COLOR][/B]";
|
||||
}
|
||||
else if (playerValue > dealerValue)
|
||||
{
|
||||
finalEffect = wager.WagerAmount;
|
||||
multiplier = 2m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]YOU WIN![/COLOR][/B]";
|
||||
}
|
||||
else if (playerValue < dealerValue)
|
||||
{
|
||||
finalEffect = -wager.WagerAmount;
|
||||
multiplier = 0m;
|
||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER WINS![/COLOR][/B]";
|
||||
}
|
||||
else
|
||||
{
|
||||
finalEffect = 0;
|
||||
multiplier = 1m;
|
||||
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
|
||||
}
|
||||
|
||||
// Update wager to complete
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(wager);
|
||||
db.Attach(gambler);
|
||||
|
||||
wager.IsComplete = true;
|
||||
wager.WagerEffect = finalEffect;
|
||||
wager.Multiplier = multiplier;
|
||||
|
||||
var balanceAdjustment = finalEffect + wager.WagerAmount;
|
||||
gambler.Balance += balanceAdjustment;
|
||||
|
||||
// Create transaction for the outcome
|
||||
await db.Transactions.AddAsync(new TransactionDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
EventSource = TransactionSourceEventType.Gambling,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Effect = balanceAdjustment,
|
||||
Comment = $"Blackjack outcome from wager {wager.Id}",
|
||||
NewBalance = gambler.Balance,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ctx);
|
||||
|
||||
await db.SaveChangesAsync(ctx);
|
||||
|
||||
// Display result
|
||||
var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]" +
|
||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" +
|
||||
$"[B]Dealer:[/B] {BlackjackHelper.FormatHand(gameState.DealerHand)} = {dealerValue}[br]" +
|
||||
$"{result}[br]";
|
||||
|
||||
if (finalEffect > 0)
|
||||
{
|
||||
message += $"You won {await finalEffect.FormatKasinoCurrencyAsync()}! ";
|
||||
}
|
||||
else if (finalEffect < 0)
|
||||
{
|
||||
message += $"You lost {await Math.Abs(finalEffect).FormatKasinoCurrencyAsync()}! ";
|
||||
}
|
||||
|
||||
message += $"Balance: {await gambler.Balance.FormatKasinoCurrencyAsync()}";
|
||||
|
||||
await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay);
|
||||
}
|
||||
|
||||
private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||
WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(wager);
|
||||
|
||||
wager.IsComplete = true;
|
||||
wager.WagerEffect = -wager.WagerAmount;
|
||||
wager.Multiplier = 0m;
|
||||
|
||||
await db.SaveChangesAsync(ctx);
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your blackjack game timed out and you forfeited {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KfChatDotNetBot.Extensions;
|
||||
|
||||
@@ -137,4 +138,30 @@ public static class Extensions
|
||||
{
|
||||
return $"@{user.KfUsername}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JSON string to the specified type using Newtonsoft.Json
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to deserialize to</typeparam>
|
||||
/// <param name="jsonString">JSON string to deserialize</param>
|
||||
/// <returns>Deserialized object or null if string is null/empty</returns>
|
||||
public static T? JsonDeserialize<T>(this string? jsonString) where T : class
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(jsonString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize an object to JSON string using Newtonsoft.Json
|
||||
/// </summary>
|
||||
/// <param name="obj">Object to serialize</param>
|
||||
/// <returns>JSON string representation</returns>
|
||||
public static string JsonSerialize(this object obj)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, Formatting.Indented);
|
||||
}
|
||||
}
|
||||
149
KfChatDotNetBot/Models/BlackjackGameMetaModel.cs
Normal file
149
KfChatDotNetBot/Models/BlackjackGameMetaModel.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
namespace KfChatDotNetBot.Models;
|
||||
|
||||
public class BlackjackGameMetaModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The wager ID associated with this game
|
||||
/// </summary>
|
||||
public required int WagerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Player's hand
|
||||
/// </summary>
|
||||
public required List<Card> PlayerHand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dealer's hand
|
||||
/// </summary>
|
||||
public required List<Card> DealerHand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Remaining cards in the deck
|
||||
/// </summary>
|
||||
public required List<Card> Deck { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the game was started
|
||||
/// </summary>
|
||||
public required DateTimeOffset GameStarted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether player has doubled down (can only hit once more)
|
||||
/// </summary>
|
||||
public bool HasDoubledDown { get; set; } = false;
|
||||
}
|
||||
|
||||
public class Card
|
||||
{
|
||||
/// <summary>
|
||||
/// Card rank (2-10, J, Q, K, A)
|
||||
/// </summary>
|
||||
public required string Rank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Card suit (♠, ♥, ♦, ♣)
|
||||
/// </summary>
|
||||
public required string Suit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the blackjack value of this card
|
||||
/// </summary>
|
||||
public int GetValue()
|
||||
{
|
||||
return Rank switch
|
||||
{
|
||||
"A" => 11, // Aces are handled specially in hand calculation
|
||||
"K" or "Q" or "J" => 10,
|
||||
_ => int.Parse(Rank)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display card as string
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Rank}{Suit}";
|
||||
}
|
||||
}
|
||||
|
||||
public static class BlackjackHelper
|
||||
{
|
||||
private static readonly string[] Ranks = { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" };
|
||||
private static readonly string[] Suits = { "♠", "♥", "♦", "♣" };
|
||||
|
||||
/// <summary>
|
||||
/// Create a new shuffled deck
|
||||
/// </summary>
|
||||
public static List<Card> CreateDeck(Random random)
|
||||
{
|
||||
var deck = new List<Card>();
|
||||
foreach (var suit in Suits)
|
||||
{
|
||||
foreach (var rank in Ranks)
|
||||
{
|
||||
deck.Add(new Card { Rank = rank, Suit = suit });
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle using Fisher-Yates
|
||||
for (int i = deck.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = random.Next(0, i + 1);
|
||||
(deck[i], deck[j]) = (deck[j], deck[i]);
|
||||
}
|
||||
|
||||
return deck;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate hand value with proper Ace handling
|
||||
/// </summary>
|
||||
public static int CalculateHandValue(List<Card> hand)
|
||||
{
|
||||
int value = 0;
|
||||
int aces = 0;
|
||||
|
||||
foreach (var card in hand)
|
||||
{
|
||||
if (card.Rank == "A")
|
||||
{
|
||||
aces++;
|
||||
value += 11;
|
||||
}
|
||||
else
|
||||
{
|
||||
value += card.GetValue();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Aces from 11 to 1 if needed to avoid bust
|
||||
while (value > 21 && aces > 0)
|
||||
{
|
||||
value -= 10;
|
||||
aces--;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if hand is blackjack (21 with 2 cards)
|
||||
/// </summary>
|
||||
public static bool IsBlackjack(List<Card> hand)
|
||||
{
|
||||
return hand.Count == 2 && CalculateHandValue(hand) == 21;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format hand for display
|
||||
/// </summary>
|
||||
public static string FormatHand(List<Card> hand, bool hideFirstCard = false)
|
||||
{
|
||||
if (hideFirstCard && hand.Count > 0)
|
||||
{
|
||||
return $"[HIDDEN] {string.Join(" ", hand.Skip(1).Select(c => c.ToString()))}";
|
||||
}
|
||||
return string.Join(" ", hand.Select(c => c.ToString()));
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,8 @@ public enum WagerGame
|
||||
[Description("Guess what number I'm thinking of")]
|
||||
GuessWhatNumber,
|
||||
Wheel,
|
||||
Slots
|
||||
Slots,
|
||||
Blackjack
|
||||
}
|
||||
|
||||
public enum GamblerState
|
||||
|
||||
Reference in New Issue
Block a user