-
Notifications
You must be signed in to change notification settings - Fork 129
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
base: master
Are you sure you want to change the base?
Changes from all commits
439a95a
0dcc952
8e55e37
3f752be
c9ea1f1
188e386
70a0622
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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']; | ||
|
||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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--) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 GitHub Actions / Node v18.12.0 - ubuntu-latest
Check warning on line 148 in src/mahoji/lib/abstracted_commands/blackjackCommand.ts GitHub Actions / Node v20 - ubuntu-latest
|
||
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)}.`; | ||
} |
There was a problem hiding this comment.
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)