mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Yesterdays bullshit served tomorrow (#100)
* Minimize amount of lines blackjack needs * selfdestruct sloppa images * massivly reduce amount of time slot graphic stays in chat
This commit is contained in:
committed by
GitHub
parent
a6810591de
commit
606e7867d0
@@ -204,7 +204,7 @@ public class GetRandomImage : ICommand
|
|||||||
: new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(),
|
: new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(),
|
||||||
settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>()));
|
settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>()));
|
||||||
}
|
}
|
||||||
else if (key == "chink" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean())
|
else if (key is "chink" or "sloppa" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean())
|
||||||
{
|
{
|
||||||
RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded);
|
RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded);
|
||||||
timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>());
|
timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>());
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ public class BlackjackCommand : ICommand
|
|||||||
{
|
{
|
||||||
private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5);
|
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 => [
|
public List<Regex> Patterns => [
|
||||||
new Regex(@"^blackjack (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^blackjack (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
||||||
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
||||||
@@ -46,11 +50,10 @@ public class BlackjackCommand : ICommand
|
|||||||
BuiltIn.Keys.KasinoBlackjackEnabled
|
BuiltIn.Keys.KasinoBlackjackEnabled
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if blackjack is enabled
|
var blackjackEnabled = settings[BuiltIn.Keys.KasinoBlackjackEnabled].ToBoolean();
|
||||||
var blackjackEnabled = (settings[BuiltIn.Keys.KasinoBlackjackEnabled]).ToBoolean();
|
|
||||||
if (!blackjackEnabled)
|
if (!blackjackEnabled)
|
||||||
{
|
{
|
||||||
var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>());
|
var gameDisabledCleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>());
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()}, blackjack is currently disabled.",
|
$"{user.FormatUsername()}, blackjack is currently disabled.",
|
||||||
true, autoDeleteAfter: gameDisabledCleanupDelay);
|
true, autoDeleteAfter: gameDisabledCleanupDelay);
|
||||||
@@ -59,7 +62,6 @@ public class BlackjackCommand : ICommand
|
|||||||
|
|
||||||
var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType<int>());
|
var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType<int>());
|
||||||
|
|
||||||
// Check if this is a new game or continuing existing game
|
|
||||||
if (arguments.TryGetValue("amount", out var amountGroup))
|
if (arguments.TryGetValue("amount", out var amountGroup))
|
||||||
{
|
{
|
||||||
await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx);
|
await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx);
|
||||||
@@ -80,15 +82,31 @@ public class BlackjackCommand : ICommand
|
|||||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetCurrentClassLogger();
|
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 wager = Convert.ToDecimal(amountStr);
|
||||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||||
|
|
||||||
if (gambler == null)
|
if (gambler == null)
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has enough balance
|
|
||||||
if (gambler.Balance < wager)
|
if (gambler.Balance < wager)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -98,16 +116,7 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wager == 0)
|
// Check for an existing incomplete game
|
||||||
{
|
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{user.FormatUsername()}, you have to wager more than {await wager.FormatKasinoCurrencyAsync()}", true,
|
|
||||||
autoDeleteAfter: cleanupDelay);
|
|
||||||
RateLimitService.RemoveMostRecentEntry(user, this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing incomplete blackjack game
|
|
||||||
var existingGame = await _dbContext.Wagers
|
var existingGame = await _dbContext.Wagers
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
||||||
@@ -133,9 +142,8 @@ public class BlackjackCommand : ICommand
|
|||||||
await _dbContext.SaveChangesAsync(ctx);
|
await _dbContext.SaveChangesAsync(ctx);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
// Check if game has timed out
|
|
||||||
var timeSinceStart = DateTimeOffset.UtcNow - existingGame.Time;
|
|
||||||
|
|
||||||
|
var timeSinceStart = DateTimeOffset.UtcNow - existingGame.Time;
|
||||||
if (timeSinceStart > GameTimeout)
|
if (timeSinceStart > GameTimeout)
|
||||||
{
|
{
|
||||||
await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx);
|
await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx);
|
||||||
@@ -148,16 +156,12 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deal initial hands
|
||||||
// Create deck and deal initial hands
|
|
||||||
var deck = BlackjackHelper.CreateDeck(gambler);
|
var deck = BlackjackHelper.CreateDeck(gambler);
|
||||||
|
|
||||||
var playerHand = new List<Card> { deck[0], deck[2] };
|
var playerHand = new List<Card> { deck[0], deck[2] };
|
||||||
var dealerHand = new List<Card> { deck[1], deck[3] };
|
var dealerHand = new List<Card> { deck[1], deck[3] };
|
||||||
deck.RemoveRange(0, 4);
|
deck.RemoveRange(0, 4);
|
||||||
|
|
||||||
// Create game state
|
|
||||||
// HasDoubledDown is a single bool — doubles are not permitted after a split
|
|
||||||
var newGameState = new BlackjackGameMetaModel
|
var newGameState = new BlackjackGameMetaModel
|
||||||
{
|
{
|
||||||
PlayerHands = new List<List<Card>> { playerHand },
|
PlayerHands = new List<List<Card>> { playerHand },
|
||||||
@@ -168,19 +172,14 @@ public class BlackjackCommand : ICommand
|
|||||||
OriginalWagerAmount = wager
|
OriginalWagerAmount = wager
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create incomplete wager
|
|
||||||
await Money.NewWagerAsync(
|
await Money.NewWagerAsync(
|
||||||
gambler.Id,
|
gambler.Id, wager, -wager,
|
||||||
wager,
|
|
||||||
-wager, // This will be the effect for incomplete wagers
|
|
||||||
WagerGame.Blackjack,
|
WagerGame.Blackjack,
|
||||||
autoModifyBalance: true,
|
autoModifyBalance: true,
|
||||||
gameMeta: newGameState,
|
gameMeta: newGameState,
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
ct: ctx
|
ct: ctx);
|
||||||
);
|
|
||||||
|
|
||||||
// Update wager ID in game state
|
|
||||||
var createdWager = await _dbContext.Wagers
|
var createdWager = await _dbContext.Wagers
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.LastOrDefaultAsync(
|
.LastOrDefaultAsync(
|
||||||
@@ -189,50 +188,44 @@ public class BlackjackCommand : ICommand
|
|||||||
createdWager.GameMeta = JsonSerializer.Serialize(newGameState);
|
createdWager.GameMeta = JsonSerializer.Serialize(newGameState);
|
||||||
await _dbContext.SaveChangesAsync(ctx);
|
await _dbContext.SaveChangesAsync(ctx);
|
||||||
|
|
||||||
// Check for immediate blackjacks
|
// Immediate blackjack check — goes straight to resolution
|
||||||
var playerValue = BlackjackHelper.CalculateHandValue(playerHand);
|
if (BlackjackHelper.IsBlackjack(playerHand) || BlackjackHelper.IsBlackjack(dealerHand))
|
||||||
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);
|
await ResolveGame(botInstance, user, gambler, createdWager, newGameState, colors, cleanupDelay, ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display initial game state
|
var playerValue = BlackjackHelper.CalculateHandValue(playerHand);
|
||||||
var colors = await SettingsProvider.GetMultipleValuesAsync([
|
|
||||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
|
||||||
]);
|
|
||||||
|
|
||||||
var canSplit = BlackjackHelper.CanSplit(playerHand);
|
var canSplit = BlackjackHelper.CanSplit(playerHand);
|
||||||
var splitText = canSplit ? " or [B]!bj split[/B]" : "";
|
|
||||||
|
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"🃏 {user.FormatUsername()} started blackjack with {await wager.FormatKasinoCurrencyAsync()}[br]" +
|
await BlackjackDisplay.GameStart(user, wager, playerHand, playerValue, dealerHand, canSplit, colors.Red),
|
||||||
$"[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]{splitText} to continue",
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action,
|
private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action,
|
||||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
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);
|
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||||
|
|
||||||
if (gambler == null)
|
if (gambler == null)
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
|
||||||
}
|
|
||||||
|
|
||||||
// Find active game
|
|
||||||
var activeWager = await _dbContext.Wagers
|
var activeWager = await _dbContext.Wagers
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
|
||||||
w.Game == WagerGame.Blackjack &&
|
w.Game == WagerGame.Blackjack &&
|
||||||
!w.IsComplete && w.GameMeta != null, cancellationToken: ctx);
|
!w.IsComplete && w.GameMeta != null,
|
||||||
|
cancellationToken: ctx);
|
||||||
|
|
||||||
if (activeWager == null)
|
if (activeWager == null)
|
||||||
{
|
{
|
||||||
@@ -255,7 +248,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check timeout
|
|
||||||
var timeSinceStart = DateTimeOffset.UtcNow - activeWager.Time;
|
var timeSinceStart = DateTimeOffset.UtcNow - activeWager.Time;
|
||||||
if (timeSinceStart > GameTimeout)
|
if (timeSinceStart > GameTimeout)
|
||||||
{
|
{
|
||||||
@@ -266,22 +258,24 @@ public class BlackjackCommand : ICommand
|
|||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case "hit":
|
case "hit":
|
||||||
await HandleHit(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
|
await HandleHit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
||||||
break;
|
break;
|
||||||
case "stand":
|
case "stand":
|
||||||
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
|
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
||||||
break;
|
break;
|
||||||
case "double":
|
case "double":
|
||||||
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
|
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
||||||
break;
|
break;
|
||||||
case "split":
|
case "split":
|
||||||
await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
|
await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
if (gameState.Deck.Count == 0)
|
if (gameState.Deck.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -294,86 +288,54 @@ public class BlackjackCommand : ICommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
||||||
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
|
var handLabel = gameState.PlayerHands.Count > 1 ? $" (H{gameState.CurrentHandIndex + 1})" : "";
|
||||||
|
|
||||||
// Draw card
|
|
||||||
var card = gameState.Deck[0];
|
var card = gameState.Deck[0];
|
||||||
gameState.Deck.RemoveAt(0);
|
gameState.Deck.RemoveAt(0);
|
||||||
currentHand.Add(card);
|
currentHand.Add(card);
|
||||||
|
|
||||||
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
|
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
|
||||||
|
|
||||||
// Whether this hit ends the current hand (bust, 21, or post-double auto-stand)
|
|
||||||
bool handEnded = playerValue > 21 || playerValue == 21 || gameState.HasDoubledDown;
|
bool handEnded = playerValue > 21 || playerValue == 21 || gameState.HasDoubledDown;
|
||||||
|
|
||||||
// Whether this is the last hand — if so, skip the intermediate message and let ResolveGame
|
if (!handEnded)
|
||||||
// produce the single consolidated output, avoiding a duplicate bust/result message.
|
|
||||||
bool isLastHand = gameState.CurrentHandIndex >= gameState.PlayerHands.Count - 1;
|
|
||||||
|
|
||||||
if (handEnded && !isLastHand)
|
|
||||||
{
|
{
|
||||||
// Transitioning between split hands: show what happened on this hand before moving on
|
// Hand is still live — show updated state and prompt for next action
|
||||||
string transitionalResult;
|
|
||||||
if (playerValue > 21)
|
|
||||||
{
|
|
||||||
var colors = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.KiwiFarmsRedColor]);
|
|
||||||
transitionalResult = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
transitionalResult = $"[B]Standing on {playerValue}[/B]";
|
|
||||||
}
|
|
||||||
|
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
|
|
||||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
|
|
||||||
transitionalResult,
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
|
||||||
}
|
|
||||||
else if (!handEnded)
|
|
||||||
{
|
|
||||||
// Hand is still in progress — show current state and prompt for next action
|
|
||||||
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
||||||
await _dbContext.SaveChangesAsync(ctx);
|
await _dbContext.SaveChangesAsync(ctx);
|
||||||
|
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
|
BlackjackDisplay.HitInProgress(user, card, currentHand, playerValue, gameState.DealerHand, handLabel, colors.Red),
|
||||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
|
|
||||||
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If handEnded && isLastHand: fall through silently — ResolveGame will show everything
|
|
||||||
|
|
||||||
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
|
// 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,
|
private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
|
|
||||||
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
||||||
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
|
|
||||||
|
|
||||||
// Only send an intermediate stand message when transitioning between split hands.
|
// No stand message needed here — MoveToNextHandOrResolve handles all output:
|
||||||
// For the final/only hand, ResolveGame produces the sole consolidated message.
|
// a combined split-transition message when moving to the next hand, and
|
||||||
bool isLastHand = gameState.CurrentHandIndex >= gameState.PlayerHands.Count - 1;
|
// silence when falling through to the final resolution.
|
||||||
if (!isLastHand)
|
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState,
|
||||||
{
|
currentHand, busted: false, colors, cleanupDelay, ctx);
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{user.FormatUsername()}{handLabel} stands with {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
||||||
|
|
||||||
// Check if player can double (only on first action with 2 cards)
|
|
||||||
if (currentHand.Count != 2)
|
if (currentHand.Count != 2)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -382,7 +344,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doubling after a split is not permitted
|
|
||||||
if (gameState.PlayerHands.Count > 1)
|
if (gameState.PlayerHands.Count > 1)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -391,7 +352,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if player has enough balance for double
|
|
||||||
if (gambler.Balance < gameState.OriginalWagerAmount)
|
if (gambler.Balance < gameState.OriginalWagerAmount)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -400,30 +360,31 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double the wager: charge the additional amount and record it
|
|
||||||
var additionalWager = gameState.OriginalWagerAmount;
|
var additionalWager = gameState.OriginalWagerAmount;
|
||||||
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
||||||
$"Double down for {wager.Id}", ct: ctx);
|
$"Double down for {wager.Id}", ct: ctx);
|
||||||
wager.WagerAmount += additionalWager; // Total wager is now OriginalWagerAmount * 2
|
wager.WagerAmount += additionalWager;
|
||||||
wager.WagerEffect -= additionalWager; // Outstanding loss reflects the extra stake
|
wager.WagerEffect -= additionalWager;
|
||||||
gameState.HasDoubledDown = true;
|
gameState.HasDoubledDown = true;
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(ctx);
|
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 botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()} doubled down! Wager is now {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
|
await BlackjackDisplay.DoubledDown(user, wager.WagerAmount),
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
|
|
||||||
// Draw exactly one card then auto-stand (handled inside HandleHit via HasDoubledDown flag)
|
await HandleHit(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
||||||
await HandleHit(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
||||||
|
|
||||||
// Check if player can split
|
|
||||||
if (!BlackjackHelper.CanSplit(currentHand))
|
if (!BlackjackHelper.CanSplit(currentHand))
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -433,7 +394,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already split
|
|
||||||
if (gameState.PlayerHands.Count > 1)
|
if (gameState.PlayerHands.Count > 1)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -443,7 +403,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if player has enough balance
|
|
||||||
if (gambler.Balance < gameState.OriginalWagerAmount)
|
if (gambler.Balance < gameState.OriginalWagerAmount)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -453,7 +412,6 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if deck has enough cards
|
|
||||||
if (gameState.Deck.Count < 2)
|
if (gameState.Deck.Count < 2)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
@@ -463,22 +421,19 @@ public class BlackjackCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the split
|
|
||||||
var card1 = currentHand[0];
|
var card1 = currentHand[0];
|
||||||
var card2 = currentHand[1];
|
var card2 = currentHand[1];
|
||||||
|
|
||||||
var hand1 = new List<Card> { card1, gameState.Deck[0] };
|
var hand1 = new List<Card> { card1, gameState.Deck[0] };
|
||||||
var hand2 = new List<Card> { card2, gameState.Deck[1] };
|
var hand2 = new List<Card> { card2, gameState.Deck[1] };
|
||||||
gameState.Deck.RemoveRange(0, 2);
|
gameState.Deck.RemoveRange(0, 2);
|
||||||
|
|
||||||
gameState.PlayerHands = new List<List<Card>> { hand1, hand2 };
|
gameState.PlayerHands = new List<List<Card>> { hand1, hand2 };
|
||||||
gameState.HasDoubledDown = false; // Single bool — doubles are blocked after splitting
|
gameState.HasDoubledDown = false;
|
||||||
gameState.CurrentHandIndex = 0;
|
gameState.CurrentHandIndex = 0;
|
||||||
|
|
||||||
// Charge for the split
|
|
||||||
var additionalWager = gameState.OriginalWagerAmount;
|
var additionalWager = gameState.OriginalWagerAmount;
|
||||||
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
|
||||||
$"Split down for {wager.Id}", ct: ctx);
|
$"Split for {wager.Id}", ct: ctx);
|
||||||
wager.WagerAmount += additionalWager;
|
wager.WagerAmount += additionalWager;
|
||||||
wager.WagerEffect -= additionalWager;
|
wager.WagerEffect -= additionalWager;
|
||||||
|
|
||||||
@@ -489,51 +444,59 @@ public class BlackjackCommand : ICommand
|
|||||||
var value2 = BlackjackHelper.CalculateHandValue(hand2);
|
var value2 = BlackjackHelper.CalculateHandValue(hand2);
|
||||||
|
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()} split their hand! Total wager: {await wager.WagerAmount.FormatKasinoCurrencyAsync()}[br]" +
|
await BlackjackDisplay.SplitDeal(user, wager.WagerAmount, hand1, value1, hand2, value2, colors.Red),
|
||||||
$"[B]Hand 1:[/B] {BlackjackHelper.FormatHand(hand1)} = {value1}[br]" +
|
|
||||||
$"[B]Hand 2:[/B] {BlackjackHelper.FormatHand(hand2)} = {value2}[br]" +
|
|
||||||
$"Playing Hand 1 - Use [B]!bj hit[/B] or [B]!bj stand[/B]",
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
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,
|
private async Task MoveToNextHandOrResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState,
|
||||||
|
List<Card> finishedHand, bool busted,
|
||||||
|
GameColors colors, TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
|
var finishedIndex = gameState.CurrentHandIndex;
|
||||||
gameState.CurrentHandIndex++;
|
gameState.CurrentHandIndex++;
|
||||||
|
|
||||||
if (gameState.CurrentHandIndex < gameState.PlayerHands.Count)
|
if (gameState.CurrentHandIndex < gameState.PlayerHands.Count)
|
||||||
{
|
{
|
||||||
// Move to next split hand
|
// 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);
|
wager.GameMeta = JsonSerializer.Serialize(gameState);
|
||||||
await _dbContext.SaveChangesAsync(ctx);
|
await _dbContext.SaveChangesAsync(ctx);
|
||||||
|
|
||||||
|
var finishedValue = BlackjackHelper.CalculateHandValue(finishedHand);
|
||||||
var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex];
|
||||||
var nextValue = BlackjackHelper.CalculateHandValue(nextHand);
|
var nextValue = BlackjackHelper.CalculateHandValue(nextHand);
|
||||||
|
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"Playing Hand {gameState.CurrentHandIndex + 1}[br]" +
|
BlackjackDisplay.SplitTransition(
|
||||||
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(nextHand)} = {nextValue}[br]" +
|
finishedIndex, finishedHand, finishedValue, busted,
|
||||||
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
|
gameState.CurrentHandIndex, nextHand, nextValue,
|
||||||
|
colors.Red),
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// All hands played, dealer plays and resolve
|
await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
||||||
await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
// Check if all hands busted
|
// Dealer only plays when at least one player hand hasn't busted
|
||||||
bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21);
|
bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21);
|
||||||
|
|
||||||
if (!allHandsBusted)
|
if (!allHandsBusted)
|
||||||
{
|
{
|
||||||
// Dealer plays
|
|
||||||
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
||||||
|
|
||||||
while (dealerValue < 17)
|
while (dealerValue < 17)
|
||||||
{
|
{
|
||||||
if (gameState.Deck.Count == 0)
|
if (gameState.Deck.Count == 0)
|
||||||
@@ -552,109 +515,54 @@ public class BlackjackCommand : ICommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx);
|
await ResolveGame(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, BlackjackGameMetaModel gameState, bool immediateResolution,
|
WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
|
||||||
TimeSpan cleanupDelay, CancellationToken ctx)
|
TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
|
||||||
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
|
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
|
||||||
|
|
||||||
decimal totalEffect = 0;
|
|
||||||
var colors = await SettingsProvider.GetMultipleValuesAsync([
|
|
||||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
|
||||||
]);
|
|
||||||
|
|
||||||
var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]";
|
|
||||||
|
|
||||||
// For a split game each hand pays out at the original wager per hand.
|
|
||||||
// For a single hand (with optional double down) the full wager.WagerAmount is at stake,
|
|
||||||
// which already reflects the doubled stake when the player doubled down.
|
|
||||||
bool isSplitGame = gameState.PlayerHands.Count > 1;
|
bool isSplitGame = gameState.PlayerHands.Count > 1;
|
||||||
|
|
||||||
// Process each hand
|
decimal totalEffect = 0;
|
||||||
|
var results = new List<HandResultData>();
|
||||||
|
|
||||||
for (int i = 0; i < gameState.PlayerHands.Count; i++)
|
for (int i = 0; i < gameState.PlayerHands.Count; i++)
|
||||||
{
|
{
|
||||||
var hand = gameState.PlayerHands[i];
|
var hand = gameState.PlayerHands[i];
|
||||||
var playerValue = BlackjackHelper.CalculateHandValue(hand);
|
var playerValue = BlackjackHelper.CalculateHandValue(hand);
|
||||||
var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
|
var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
|
||||||
|
// Split hands each pay the original per-hand wager; a single hand pays the full
|
||||||
// Split hands each pay at the original wager amount.
|
// (possibly doubled) wager amount already tracked in wager.WagerAmount.
|
||||||
// A single hand pays at the full (possibly doubled) wager amount.
|
|
||||||
var handWager = isSplitGame ? gameState.OriginalWagerAmount : wager.WagerAmount;
|
var handWager = isSplitGame ? gameState.OriginalWagerAmount : wager.WagerAmount;
|
||||||
|
|
||||||
var handLabel = isSplitGame ? $" {i + 1}" : "";
|
var (outcome, effect) = BlackjackDisplay.ClassifyHand(
|
||||||
message += $"[B]Your hand{handLabel}:[/B] {BlackjackHelper.FormatHand(hand)} = {playerValue}[br]";
|
playerValue, playerBlackjack, dealerValue, dealerBlackjack, handWager);
|
||||||
|
|
||||||
decimal handEffect;
|
results.Add(new HandResultData(i, hand, playerValue, outcome, effect));
|
||||||
string result;
|
totalEffect += effect;
|
||||||
|
|
||||||
// Determine outcome for this hand
|
|
||||||
if (playerBlackjack && dealerBlackjack)
|
|
||||||
{
|
|
||||||
handEffect = 0;
|
|
||||||
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (playerBlackjack)
|
|
||||||
{
|
|
||||||
handEffect = handWager * 1.5m;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (dealerBlackjack)
|
|
||||||
{
|
|
||||||
handEffect = -handWager;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (playerValue > 21)
|
|
||||||
{
|
|
||||||
handEffect = -handWager;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (dealerValue > 21)
|
|
||||||
{
|
|
||||||
handEffect = handWager;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (playerValue > dealerValue)
|
|
||||||
{
|
|
||||||
handEffect = handWager;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]WIN! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else if (playerValue < dealerValue)
|
|
||||||
{
|
|
||||||
handEffect = -handWager;
|
|
||||||
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]LOSE! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
handEffect = 0;
|
|
||||||
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message += $"{result}[br]";
|
|
||||||
totalEffect += handEffect;
|
|
||||||
}
|
|
||||||
|
|
||||||
message += $"[B]Dealer:[/B] {BlackjackHelper.FormatHand(gameState.DealerHand)} = {dealerValue}[br]";
|
|
||||||
|
|
||||||
// Update wager to complete
|
|
||||||
wager.IsComplete = true;
|
wager.IsComplete = true;
|
||||||
wager.WagerEffect = totalEffect;
|
wager.WagerEffect = totalEffect;
|
||||||
wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount;
|
wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount;
|
||||||
|
|
||||||
// Update balance and create transaction in same context
|
|
||||||
await _dbContext.SaveChangesAsync(ctx);
|
await _dbContext.SaveChangesAsync(ctx);
|
||||||
|
|
||||||
var balanceAdjustment = totalEffect + wager.WagerAmount;
|
var balanceAdjustment = totalEffect + wager.WagerAmount;
|
||||||
var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment, TransactionSourceEventType.Gambling,
|
var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment,
|
||||||
$"Blackjack outcome from wager {wager.Id}", null, ctx);
|
TransactionSourceEventType.Gambling, $"Blackjack outcome from wager {wager.Id}", null, ctx);
|
||||||
|
|
||||||
message += $"[u][B]Net:[/B] {(totalEffect >= 0 ? "+" : "")}{await totalEffect.FormatKasinoCurrencyAsync()} | Balance: {await newBalance.FormatKasinoCurrencyAsync()}";
|
await botInstance.SendChatMessageAsync(
|
||||||
|
await BlackjackDisplay.FinalResult(
|
||||||
await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay);
|
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,
|
private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
|
||||||
WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx)
|
WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
|
|||||||
231
KfChatDotNetBot/Commands/Kasino/BlackjackDisplay.cs
Normal file
231
KfChatDotNetBot/Commands/Kasino/BlackjackDisplay.cs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
using KfChatDotNetBot.Extensions;
|
||||||
|
using KfChatDotNetBot.Models;
|
||||||
|
using KfChatDotNetBot.Models.DbModels;
|
||||||
|
|
||||||
|
namespace KfChatDotNetBot.Commands.Kasino;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds every chat message string for the blackjack game.
|
||||||
|
/// No game logic lives here — only presentation.
|
||||||
|
/// <para>
|
||||||
|
/// Keeping all string construction in one place means tweaking the UI never
|
||||||
|
/// requires touching the game-flow code in <see cref="BlackjackCommand"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
internal static class BlackjackDisplay
|
||||||
|
{
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Primitive helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Wraps ♥ and ♦ in the game's losing-text red so suit glyphs render in red.
|
||||||
|
/// Applied to every hand string so the color is consistent with loss messages.
|
||||||
|
internal static string ColorizeSuits(string text, string redHex) =>
|
||||||
|
text.Replace("♥", $"[COLOR={redHex}]♥[/COLOR]")
|
||||||
|
.Replace("♦", $"[COLOR={redHex}]♦[/COLOR]");
|
||||||
|
|
||||||
|
private static string FmtHand(List<Card> hand, string redHex, bool hideFirst = false) =>
|
||||||
|
ColorizeSuits(BlackjackHelper.FormatHand(hand, hideFirstCard: hideFirst), redHex);
|
||||||
|
|
||||||
|
private static string FmtCard(Card card, string redHex) =>
|
||||||
|
ColorizeSuits(card.ToString()!, redHex);
|
||||||
|
|
||||||
|
/// Compact action-hint line. Only advertises actions the player can actually take right now.
|
||||||
|
/// ✦ marks double-down; ✂ marks split — both are hidden once unavailable.
|
||||||
|
private static string ActionHints(bool canDouble = false, bool canSplit = false)
|
||||||
|
{
|
||||||
|
var parts = new List<string> { "[B]hit[/B]", "[B]stand[/B]" };
|
||||||
|
if (canDouble) parts.Add("[B]double[/B] ✦");
|
||||||
|
if (canSplit) parts.Add("[B]split[/B] ✂");
|
||||||
|
return "!bj: " + string.Join(" · ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Game-start (fresh deal)
|
||||||
|
// Two lines: hand state + action hints.
|
||||||
|
// Double is always shown — balance check happens inside HandleDouble if attempted.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static async Task<string> GameStart(
|
||||||
|
UserDbModel user, decimal wager,
|
||||||
|
List<Card> playerHand, int playerValue,
|
||||||
|
List<Card> dealerHand,
|
||||||
|
bool canSplit, string redHex)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"🃏 [B]{user.FormatUsername()}[/B] · {await wager.FormatKasinoCurrencyAsync()} — " +
|
||||||
|
$"[B]You:[/B] {FmtHand(playerHand, redHex)} ({playerValue}) " +
|
||||||
|
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex, hideFirst: true)}[br]" +
|
||||||
|
ActionHints(canDouble: true, canSplit: canSplit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Hit still in progress (hand not yet resolved)
|
||||||
|
// Two lines: drew-card + updated state + action hints.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static string HitInProgress(
|
||||||
|
UserDbModel user, Card drawnCard,
|
||||||
|
List<Card> currentHand, int handValue,
|
||||||
|
List<Card> dealerHand,
|
||||||
|
string handLabel, string redHex)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"{user.FormatUsername()}{handLabel} drew {FmtCard(drawnCard, redHex)} — " +
|
||||||
|
$"[B]You:[/B] {FmtHand(currentHand, redHex)} ({handValue}) " +
|
||||||
|
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex, hideFirst: true)}[br]" +
|
||||||
|
ActionHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Double-down confirmation
|
||||||
|
// One line, shown once before the auto-hit silently proceeds to resolution.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static async Task<string> DoubledDown(UserDbModel user, decimal newTotalWager) =>
|
||||||
|
$"{user.FormatUsername()} doubled down · Wager: [B]{await newTotalWager.FormatKasinoCurrencyAsync()}[/B]";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Split: initial deal display
|
||||||
|
// Three lines: wager header, both hands side-by-side, action hints for Hand 1.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static async Task<string> SplitDeal(
|
||||||
|
UserDbModel user, decimal totalWager,
|
||||||
|
List<Card> hand1, int value1,
|
||||||
|
List<Card> hand2, int value2,
|
||||||
|
string redHex)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"{user.FormatUsername()} split · Wager: [B]{await totalWager.FormatKasinoCurrencyAsync()}[/B][br]" +
|
||||||
|
$"[B]H1:[/B] {FmtHand(hand1, redHex)} ({value1}) · [B]H2:[/B] {FmtHand(hand2, redHex)} ({value2})[br]" +
|
||||||
|
$"Playing [B]H1[/B] — {ActionHints()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Split: hand transition
|
||||||
|
// Two lines combining "what happened to the finished hand" and "what you
|
||||||
|
// have on the next hand" into one message, saving a separate chat post.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static string SplitTransition(
|
||||||
|
int finishedIndex, List<Card> finishedHand, int finishedValue, bool busted,
|
||||||
|
int nextIndex, List<Card> nextHand, int nextValue,
|
||||||
|
string redHex)
|
||||||
|
{
|
||||||
|
var outcome = busted
|
||||||
|
? $"[B][COLOR={redHex}]BUST[/COLOR][/B]"
|
||||||
|
: $"stood [B]{finishedValue}[/B]";
|
||||||
|
|
||||||
|
return
|
||||||
|
$"[B]H{finishedIndex + 1}:[/B] {FmtHand(finishedHand, redHex)} ({finishedValue}) — {outcome} " +
|
||||||
|
$"→ [B]H{nextIndex + 1}:[/B] {FmtHand(nextHand, redHex)} ({nextValue})[br]" +
|
||||||
|
ActionHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Final result
|
||||||
|
// Single hand → 2 lines: You vs Dealer — RESULT / Net · Balance
|
||||||
|
// Split game → 3 lines: header / H1 — R · H2 — R / Dealer · Net · Balance
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static async Task<string> FinalResult(
|
||||||
|
UserDbModel user,
|
||||||
|
IReadOnlyList<HandResultData> results,
|
||||||
|
List<Card> dealerHand, int dealerValue,
|
||||||
|
decimal totalEffect, decimal newBalance,
|
||||||
|
bool isSplitGame, string greenHex, string redHex)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
var sign = totalEffect >= 0 ? "+" : "";
|
||||||
|
var netLine =
|
||||||
|
$"[U]Net {sign}{await totalEffect.FormatKasinoCurrencyAsync()} · " +
|
||||||
|
$"Balance {await newBalance.FormatKasinoCurrencyAsync()}[/U]";
|
||||||
|
|
||||||
|
if (!isSplitGame)
|
||||||
|
{
|
||||||
|
// ── Single hand: hand + dealer + result all on one line ──────────
|
||||||
|
var r = results[0];
|
||||||
|
sb.Append(
|
||||||
|
$"🃏 [B]{user.FormatUsername()}[/B] · " +
|
||||||
|
$"[B]You:[/B] {FmtHand(r.Hand, redHex)} ({r.PlayerValue}) " +
|
||||||
|
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex)} ({dealerValue}) — " +
|
||||||
|
$"{await FormatOutcomeTag(r, greenHex, redHex)}[br]" +
|
||||||
|
netLine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ── Split game: header, then both hands on one line, dealer + net ─
|
||||||
|
sb.Append($"🃏 [B]{user.FormatUsername()}[/B][br]");
|
||||||
|
|
||||||
|
var handParts = new List<string>();
|
||||||
|
foreach (var r in results)
|
||||||
|
{
|
||||||
|
handParts.Add(
|
||||||
|
$"[B]H{r.HandIndex + 1}:[/B] {FmtHand(r.Hand, redHex)} ({r.PlayerValue}) — " +
|
||||||
|
$"{await FormatOutcomeTag(r, greenHex, redHex)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(string.Join(" · ", handParts) + "[br]");
|
||||||
|
sb.Append($"[B]Dealer:[/B] {FmtHand(dealerHand, redHex)} ({dealerValue}) · {netLine}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Outcome classification
|
||||||
|
// Called by BlackjackCommand.ResolveGame to populate HandResultData before
|
||||||
|
// passing it here for display. Keeping it in this file co-locates it with
|
||||||
|
// the outcome tags it feeds into.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
internal static (HandOutcome Outcome, decimal Effect) ClassifyHand(
|
||||||
|
int playerValue, bool playerBlackjack,
|
||||||
|
int dealerValue, bool dealerBlackjack,
|
||||||
|
decimal handWager)
|
||||||
|
{
|
||||||
|
if (playerBlackjack && dealerBlackjack) return (HandOutcome.Push, 0);
|
||||||
|
if (playerBlackjack) return (HandOutcome.Blackjack, handWager * 1.5m);
|
||||||
|
if (dealerBlackjack) return (HandOutcome.DealerBlackjack, -handWager);
|
||||||
|
if (playerValue > 21) return (HandOutcome.Bust, -handWager);
|
||||||
|
if (dealerValue > 21) return (HandOutcome.DealerBust, handWager);
|
||||||
|
if (playerValue > dealerValue) return (HandOutcome.Win, handWager);
|
||||||
|
if (playerValue < dealerValue) return (HandOutcome.Lose, -handWager);
|
||||||
|
return (HandOutcome.Push, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> FormatOutcomeTag(HandResultData r, string greenHex, string redHex)
|
||||||
|
{
|
||||||
|
var amt = await Math.Abs(r.Effect).FormatKasinoCurrencyAsync();
|
||||||
|
return r.Outcome switch
|
||||||
|
{
|
||||||
|
HandOutcome.Blackjack => $"[B][COLOR={greenHex}]BLACKJACK! +{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.Win => $"[B][COLOR={greenHex}]WIN! +{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.DealerBust => $"[B][COLOR={greenHex}]DEALER BUST! +{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.Lose => $"[B][COLOR={redHex}]LOSE! -{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.Bust => $"[B][COLOR={redHex}]BUST! -{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.DealerBlackjack => $"[B][COLOR={redHex}]DEALER BLACKJACK! -{amt}[/COLOR][/B]",
|
||||||
|
HandOutcome.Push => "[B][COLOR=orange]PUSH[/COLOR][/B]",
|
||||||
|
_ => "?"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Supporting types used across BlackjackDisplay and BlackjackCommand
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
internal enum HandOutcome
|
||||||
|
{
|
||||||
|
Blackjack, DealerBlackjack, Win, Lose, Bust, DealerBust, Push
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-computed per-hand result data passed from BlackjackCommand.ResolveGame
|
||||||
|
/// to BlackjackDisplay.FinalResult for rendering.
|
||||||
|
internal record HandResultData(
|
||||||
|
int HandIndex,
|
||||||
|
List<Card> Hand,
|
||||||
|
int PlayerValue,
|
||||||
|
HandOutcome Outcome,
|
||||||
|
decimal Effect);
|
||||||
@@ -131,7 +131,7 @@ public class SlotsCommand : ICommand
|
|||||||
}
|
}
|
||||||
var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
|
var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
|
||||||
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true,
|
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true,
|
||||||
autoDeleteAfter: TimeSpan.FromSeconds(150));
|
autoDeleteAfter: TimeSpan.FromSeconds(60)); // delay till slots graphic deletion.
|
||||||
}
|
}
|
||||||
|
|
||||||
winnings = (decimal)board.RunningTotalDisplay;
|
winnings = (decimal)board.RunningTotalDisplay;
|
||||||
|
|||||||
Reference in New Issue
Block a user