Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a blackjack command to gamble #5821

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/mahoji/commands/gamble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { prisma } from '../../lib/settings/prisma';
import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation';
import itemIsTradeable from '../../lib/util/itemIsTradeable';
import { blackjackCommand } from '../lib/abstracted_commands/blackjackCommand';
import { capeGambleCommand, capeGambleStatsCommand } from '../lib/abstracted_commands/capegamble';
import { diceCommand } from '../lib/abstracted_commands/diceCommand';
import { duelCommand } from '../lib/abstracted_commands/duelCommand';
Expand Down Expand Up @@ -65,6 +66,24 @@
}
]
},
/**
*
* Blackjack
*
*/
{
type: ApplicationCommandOptionType.Subcommand,
name: 'blackjack',
description: 'Allows you play lucky pick and risk your GP.',
options: [
{
type: ApplicationCommandOptionType.String,
name: 'amount',
description: 'Amount you wish to gamble.',
required: true
}
]
},
/**
*
* Duel
Expand Down Expand Up @@ -177,6 +196,7 @@
}: CommandRunOptions<{
cape?: { type?: string; autoconfirm?: boolean };
dice?: { amount?: string };
blackjack?: { amount: string };
duel?: { user: MahojiUserOption; amount?: string };
lucky_pick?: { amount: string };
slots?: { amount?: string };
Expand Down Expand Up @@ -213,6 +233,10 @@
return 'You have gambling disabled and cannot gamble!';
}

if (options.blackjack) {
return blackjackCommand(user, options.blackjack.amount, interaction);
}

if (options.lucky_pick) {
return luckyPickCommand(user, options.lucky_pick.amount, interaction);
}
Expand Down Expand Up @@ -246,7 +270,7 @@
.filter(i => itemIsTradeable(i[0].id))
.filter(i => !user.user.favoriteItems.includes(i[0].id));
const entry = randArrItem(bank);
if (!entry) return 'You have no items you can give away!';

Check warning on line 273 in src/mahoji/commands/gamble.ts

View workflow job for this annotation

GitHub Actions / Node v18.12.0 - ubuntu-latest

Unexpected object value in conditional. The condition is always true

Check warning on line 273 in src/mahoji/commands/gamble.ts

View workflow job for this annotation

GitHub Actions / Node v20 - ubuntu-latest

Unexpected object value in conditional. The condition is always true
const [item, qty] = entry;
const loot = new Bank().add(item.id, qty);

Expand Down
211 changes: 211 additions & 0 deletions src/mahoji/lib/abstracted_commands/blackjackCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { channelIsSendable } from '@oldschoolgg/toolkit';
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction } from 'discord.js';
import { Time } from 'e';
import { CommandResponse } from 'mahoji/dist/lib/structures/ICommand';
import { Bank } from 'oldschooljs';
import { toKMB } from 'oldschooljs/dist/util';

import { awaitMessageComponentInteraction } from '../../../lib/util';
import { handleMahojiConfirmation } from '../../../lib/util/handleMahojiConfirmation';
import { deferInteraction } from '../../../lib/util/interactionReply';
import { mahojiParseNumber } from '../../mahojiSettings';

const returnButtons = [
new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder({
label: 'Hit',
style: ButtonStyle.Secondary,
customId: 'hit'
}),
new ButtonBuilder({
label: 'Stand',
style: ButtonStyle.Secondary,
customId: 'stand'
})
])
];

interface Card {
suit: string;
value: string;
}

// Define constants for suits and values
const suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades'];
const values = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It MIGHT make formatting the game easier if you use a single char for card values (and suits?).

eg. 10 = 'T', J, K , Q, A

It would be REALLY cool to dynamically generate the result like slots. You don't have to draw the entire card, just a simple box around painted unicode letters, 3♠, A♥ etc. Paint spades/clubs black, and hearts/diamonds red. T♣

Look at slots for simple graphics stuff, and the bank code has examples of drawing text (including unicode symbols) onto canvas)


// Function to create a deck of cards
function createDeck(): Card[] {
const deck: Card[] = [];
for (const suit of suits) {
for (const value of values) {
deck.push({ suit, value });
}
}
return deck;
}

// Function to shuffle the deck
function shuffleDeck(deck: Card[]): void {
Copy link
Contributor

@themrrobert themrrobert Apr 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shuffle looks pretty good, but I would still shuffle the deck at least 2-3 times. (either multiple calls to shuffle deck, or more likely just loop inside the function 2-3 times)

for (let i = deck.length - 1; i > 0; i--) {
Copy link
Contributor

@themrrobert themrrobert Apr 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be i >=0 otherwise the loop where i = 0 is not executed, and you're just hoping the shuffle chooses card zero.

Basically if you don't do >=0, then the 0th card ends up unshuffled (2 of Hearts) a disproportionate amount of the time.

const j = Math.floor(Math.random() * (i + 1));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use a nodeCrypto random, try cryptoRand() from util.ts, and you don't need to floor it.

Alternatively, randFloat() from util.ts also uses nodeCrypto, but why use two functions when you can use one? :D

[deck[i], deck[j]] = [deck[j], deck[i]];
}
}

// Function to deal a card from the deck
function dealCard(deck: Card[]): Card {
return deck.pop()!;
}

// Function to calculate the value of a hand
function calculateHandValue(hand: Card[]): number {
let sum = 0;
let aceCount = 0;
for (const card of hand) {
if (card.value === 'Ace') {
sum += 11;
aceCount++;
} else if (card.value === 'Jack' || card.value === 'Queen' || card.value === 'King') {
sum += 10;
} else {
sum += parseInt(card.value);
}
}
while (sum > 21 && aceCount > 0) {
sum -= 10;
aceCount--;
}
return sum;
}

export async function blackjackCommand(
user: MUser,
_amount: string,
interaction: ChatInputCommandInteraction
): CommandResponse {
await deferInteraction(interaction);
const amount = mahojiParseNumber({ input: _amount, min: 1 });

if (user.isIronman) {
return "Ironmen can't gamble! Go pickpocket some men for GP.";
}

if (!amount) {
return `**Blackjack:**
- Beat the dealer to win! You must gamble between 20m and 1b.`;
}

if (amount < 20_000_000 || amount > 1_000_000_000) {
return 'You can only gamble between 20m and 1b.';
}

const channel = globalClient.channels.cache.get(interaction.channelId);
if (!channelIsSendable(channel)) return 'Invalid channel.';

await handleMahojiConfirmation(
interaction,
`Are you sure you want to gamble ${toKMB(amount)}? You might lose it all, you might double it.`
);
await user.sync();
const currentBalance = user.GP;
if (currentBalance < amount) {
return "You don't have enough GP to make this bet.";
}
await user.removeItemsFromBank(new Bank().add('Coins', amount));

const winnings = amount * 2;

// Create and shuffle the deck
const deck = createDeck();
shuffleDeck(deck);

// Deal initial hands
const playerHand = [dealCard(deck), dealCard(deck)];
const dealerHand = [dealCard(deck), dealCard(deck)];

// Check for player blackjack
if (calculateHandValue(playerHand) === 21) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blackjack should pay 2.5x (1.5x their bet in profit).

Or a push if dealer also has blackjack.

You could get crazy with insurance, even-money on blackjack vs A, etc, but that's probably not necessary :D

await user.addItemsToBank({ items: new Bank().add('Coins', winnings), collectionLog: false });
const playerCards = `**Your Hand**: ${playerHand.map(card => `${card.value} of ${card.suit}`).join(', ')}`;
return `Blackjack! Player wins ${toKMB(amount)}!\n${playerCards}`;
}

// Send initial message with player's hand and hit/stand buttons
const dealerCard = dealerHand[0];
let content = `**Dealer Card**: ${dealerCard.value} of ${dealerCard.suit}\n**Your Hand**: ${playerHand
.map(card => `${card.value} of ${card.suit}`)
.join(', ')}`;

const sentMessage = await channel.send({ content, components: returnButtons });

try {
while (true) {
const selection = await awaitMessageComponentInteraction({
message: sentMessage,
filter: i => {
if (i.user.id !== user.id) {
i.reply({ ephemeral: true, content: 'This is not your confirmation message.' });

Check warning on line 148 in src/mahoji/lib/abstracted_commands/blackjackCommand.ts

View workflow job for this annotation

GitHub Actions / Node v18.12.0 - ubuntu-latest

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 148 in src/mahoji/lib/abstracted_commands/blackjackCommand.ts

View workflow job for this annotation

GitHub Actions / Node v20 - ubuntu-latest

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
return false;
}
return true;
},
time: Time.Second * 15
});
if (!selection.isButton()) continue;
switch (selection.customId) {
case 'hit': {
// Player hits
playerHand.push(dealCard(deck));
let playerHandValue = calculateHandValue(playerHand);
if (playerHandValue > 21) {
// Player busts, update message content to show full hand
content = `**Dealer Card**: ${dealerCard.value} of ${
dealerCard.suit
}\n**Your Final Hand**: ${playerHand.map(card => `${card.value} of ${card.suit}`).join(', ')}`;
await sentMessage.edit({ content, components: [] });
return `Dealer wins! Player went bust and lost ${toKMB(amount)}.`;
}
// Player hasn't bust yet, update message content with the new full hand
content = `Dealer Card: ${dealerCard.value} of ${dealerCard.suit}\n**Your Hand**: ${playerHand
.map(card => `${card.value} of ${card.suit}`)
.join(', ')}`;
await sentMessage.edit({ content, components: returnButtons });
break;
}

case 'stand': {
// Player stands
while (calculateHandValue(dealerHand) < 17) {
dealerHand.push(dealCard(deck));
}

// Update message content to show dealer's hand and player's final hand
content = `**Dealer's Hand**: ${dealerHand
.map(card => `${card.value} of ${card.suit}`)
.join(', ')}\n**Your Final Hand**: ${playerHand
.map(card => `${card.value} of ${card.suit}`)
.join(', ')}`;
await sentMessage.edit({ content, components: [] });

// Determine the winner
const playerHandValue = calculateHandValue(playerHand);
if (playerHandValue > 21) {
return `Dealer wins! Player went bust and lost ${toKMB(amount)}.`;
}
const dealerHandValue = calculateHandValue(dealerHand);
if (dealerHandValue > 21 || playerHandValue > dealerHandValue) {
await user.addItemsToBank({ items: new Bank().add('Coins', winnings), collectionLog: false });
return `Player wins ${toKMB(amount)}!`;
}
return `Dealer wins! Player lost ${toKMB(amount)}.`;
}
}
}
} catch (err: unknown) {
console.error(err);
} finally {
await sentMessage.edit({ components: [] });
}
return `Timed out, Dealer wins! Player lost ${toKMB(amount)}.`;
}
Loading