Last active
November 10, 2021 06:46
-
-
Save BrianWasTaken/ed4cc0dca0c830f701ddd58c38b16706 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The latest implementation of blackjack from Dank Memer discord bot. | |
* A bit modified and it's the most complicated shit I've seen yet. | |
* | |
* Credits: https://blackjack.dankmemer.lol | |
*/ | |
import type { CommandOptions, Args } from '@sapphire/framework'; | |
import type { Message } from 'discord.js'; | |
import { ApplyOptions } from '@sapphire/decorators'; | |
import { Command } from '@sapphire/framework'; | |
import type { User, ButtonInteraction, MessageOptions, MessageEmbedOptions } from 'discord.js'; | |
import { Blackjack, Common, Prompt, CurrencyUtil, MultiplierUtil } from '#lava/util'; | |
import { MessageButton, MessageActionRow } from 'discord.js'; | |
import { ArgumentError } from '@sapphire/framework'; | |
@ApplyOptions<CommandOptions>({ | |
name: 'blackjack', | |
aliases: ['bj', 'card'], | |
description: 'Do you have the abilities of a rigged dealer?', | |
detailedDescription: 'Play a game of blackjack, just like the classics. Simply get to 21 first or have 5 cards in your hand without going over 21 before the dealer does. That\'s the rule.', | |
cooldownDelay: 3000 | |
}) | |
export default class extends Command { | |
public async messageRun(msg: Message, args: Args) { | |
const bet = await args.pick('bet').catch((err: ArgumentError) => err); | |
if (bet instanceof ArgumentError) return msg.reply(bet.message); | |
const cc = await msg.client.db.users.fetch(msg.author.id); | |
const bj = new Blackjack({ player: msg.author, dealer: msg.client.user! }); | |
for (let i = 0; i < 2; i++) { | |
bj.deal(bj.player, true); | |
bj.deal(bj.dealer, true); | |
} | |
const getEmbed = (): MessageOptions => ({ | |
embeds: [bj.renderEmbed(bj.stood, bj.outcome)], | |
components: [new MessageActionRow({ | |
components: [ | |
new MessageButton({ label: 'Hit', customId: 'hit', style: 'PRIMARY' }), | |
new MessageButton({ label: 'Stand', customId: 'stand', style: 'PRIMARY' }), | |
new MessageButton({ label: 'Forfeit', customId: 'end', style: 'DANGER' }), | |
].map(btn => btn.setDisabled(!!bj.outcome)) | |
})] | |
}); | |
const prompt = new Prompt({ | |
user: msg.author, | |
content: () => getEmbed(), | |
channel: msg.channel, | |
contextError: 'Go play your own game of blackjack.', | |
}); | |
await prompt.start({ time: 30_000, max: Infinity }, async ctx => { | |
try { | |
const update = async (int: ButtonInteraction) => { | |
bj.getOutcome(); | |
switch(bj.outcome?.outcome) { | |
case Blackjack.Constants.Outcome.WIN: { | |
const winnings = CurrencyUtil.calcWinnings(bet.bet, { cap: !bet.full, multi: MultiplierUtil.calculate(cc, { channel: msg.channel, member: msg.member }).total.unlocked }); | |
bj.outcome.extra = `You won **${winnings.toLocaleString()}** coins. You now have **${(cc.data.props.pocket + winnings).toLocaleString()}** coins.`; | |
await cc.addPocket(winnings).updateGambling('blackjack', true, winnings).calcXpGain().save(); | |
break; | |
} | |
case Blackjack.Constants.Outcome.OTHER: { | |
bj.outcome.extra = 'The dealer is keeping your money to deal with your bullcrap.'; | |
await cc.subPocket(bet.bet).updateGambling('blackjack', false, bet.bet).calcXpGain().save(); | |
break; | |
} | |
case Blackjack.Constants.Outcome.LOSS: { | |
bj.outcome.extra ??= `You lost **${bet.bet.toLocaleString()}** coins. You now have **${(cc.data.props.pocket - bet.bet).toLocaleString()}** coins.`; | |
await cc.subPocket(bet.bet).updateGambling('blackjack', false, bet.bet).calcXpGain().save(); | |
break; | |
} | |
case Blackjack.Constants.Outcome.TIE: { | |
bj.outcome.extra = `Your wallet hasn't changed! You have **${cc.data.props.pocket.toLocaleString()}** coins still.`; | |
break; | |
} | |
} | |
if (!ctx.ended) await int.update(getEmbed()); | |
if (bj.outcome && bj.stood) { | |
ctx.handler.stop('stood'); | |
} | |
}; | |
if (ctx.ended) { | |
if (ctx.reason === 'force') { | |
bj.reason('You ended the game.').other('The dealer is keeping your money to deal with your bullcrap.'); | |
if (ctx.interaction) await ctx.interaction.update(getEmbed()); | |
await cc.subPocket(bet.bet).updateGambling(this.name, false, bet.bet).calcXpGain().save(); | |
} else if (ctx.reason === 'time') { | |
bj.reason('You didn\'t respond in time.').other('The dealer is keeping your money to deal with your bullcrap.'); | |
await ctx.message.edit(getEmbed()); // edit cause not update() not called | |
await cc.subPocket(bet.bet).updateGambling(this.name, false, bet.bet).calcXpGain().save(); | |
} | |
return; | |
} | |
ctx.handler.resetTimer(); | |
switch(ctx.interaction.customId) { | |
case 'hit': | |
bj.deal(bj.player, false); | |
return update(ctx.interaction); | |
case 'stand': | |
bj.stand(); | |
while(bj.countHand(bj.dealer.hand) < Blackjack.Constants.BJ_DEALER_MAX) { | |
bj.deal(bj.dealer, false); | |
} | |
return update(ctx.interaction); | |
case 'end': | |
bj.reason('You ended the game.').other(); | |
return ctx.handler.stop('force'); | |
} | |
} catch {} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { User, MessageEmbedOptions } from 'discord.js'; | |
import { Formatters } from 'discord.js'; | |
import { Common } from './index.js'; | |
/** | |
* The class helper for blackjack games. | |
* Includes the main logic of the game. | |
* @since 4.2.2 | |
*/ | |
export class Blackjack { | |
public dealer: Blackjack.Player; | |
public player: Blackjack.Player; | |
public outcome: Blackjack.Constants.OutcomeResult; | |
public stood: boolean; | |
public constructor(options: Blackjack.Options) { | |
this.dealer = { user: options.dealer, hand: [] }; | |
this.player = { user: options.player, hand: [] }; | |
this.outcome = null; | |
this.stood = false; | |
} | |
/** | |
* Inserts one card to a player's hand. | |
* @param player The player to insert the card to. | |
* @param initial Whether this is an initial deal. | |
*/ | |
public deal(player: Blackjack.Player, initial: boolean): void { | |
const face = Common.randomItem([...Blackjack.Constants.FACES.values()]); | |
const suit = Common.randomItem([...Blackjack.Constants.SUITS.values()]); | |
if (player.hand.find(card => card.face === face && card.suit === suit)) { | |
return this.deal(player, initial); | |
} | |
const card: Blackjack.Cards.Card = { | |
face, | |
suit, | |
baseValue: typeof face === 'number' | |
? face | |
: (face === 'A' ? Blackjack.Constants.BJ_ACE_MIN : Blackjack.Constants.BJ_FACE) | |
}; | |
if (initial && this.countHand([...player.hand, card]) >= Blackjack.Constants.BJ_WIN) { | |
return this.deal(player, initial); | |
} | |
player.hand.push(card); | |
}; | |
/** | |
* Stands. Indicating the end of the game. | |
*/ | |
public stand() { | |
this.stood = true; | |
return this; | |
} | |
/** | |
* Sums up the total value of all cards. | |
* @param cards The cards in a player's hand. | |
*/ | |
protected countHandRaw(cards: Blackjack.Cards.Card[]): number { | |
return cards.reduce((acc, curr) => curr.baseValue + acc, 0); | |
} | |
/** | |
* Counts the player's card on their hands for the charlie rule. | |
* @param hand The cards in a player's hand. | |
*/ | |
public countHand(hand: Blackjack.Cards.Card[]): number { | |
for (const card of hand) { | |
if (card.face === 'A') { | |
card.baseValue = Blackjack.Constants.BJ_ACE_MAX; | |
} | |
} | |
let lowerAce: Blackjack.Cards.Card | undefined; | |
while( | |
this.countHandRaw(hand) > Blackjack.Constants.BJ_WIN && | |
(lowerAce = hand.find(card => card.face === 'A' && card.baseValue !== Blackjack.Constants.BJ_ACE_MIN)) | |
) { | |
lowerAce.baseValue = Blackjack.Constants.BJ_ACE_MIN; | |
} | |
return this.countHandRaw(hand); | |
} | |
/** | |
* Renders the card of the dealer or player in the user's hand. | |
* @param card The card of the dealer or player. | |
* @param index The index of the card in the dealer's hand. | |
* @param hide Whether to hide this card from the hand or not. | |
*/ | |
public renderCard(card: Blackjack.Cards.Card, index: number, hide: boolean): string { | |
return `[${Formatters.inlineCode(index > 0 && hide ? '?' : `${card.suit} ${card.face}`)}](https://google.com)`; | |
} | |
/** | |
* Renders the hand of the player or dealer. | |
* @param hand The hand of the player. | |
* @param hide Whether to hide the player cards or not. | |
*/ | |
public renderHand(hand: Blackjack.Player['hand'], hide: boolean): string { | |
return Common.join([ | |
`Cards - ${Formatters.bold(hand.map((card, idx) => this.renderCard(card, idx, hide)).join(' '))}`, | |
`Total - ${Formatters.inlineCode(hide ? Formatters.inlineCode(' ? ') : this.countHand(hand).toString())}` | |
]); | |
} | |
/** | |
* Renders the blackjack embed. | |
* @param stood Whether the player has stood or not. | |
* @param outcome The result of the blackjack game. | |
*/ | |
public renderEmbed(stood: boolean, outcome: Blackjack.Constants.OutcomeResult): MessageEmbedOptions { | |
return { | |
author: { | |
name: `${this.player.user.username}'s blackjack game`, | |
icon_url: Common.getAvatar(this.player.user) | |
}, | |
color: outcome ? Blackjack.Constants.Outcomes[outcome.outcome].color : 0x26A69A, | |
description: !outcome ? '' : Common.join([ | |
Formatters.bold(`${`${Blackjack.Constants.Outcomes[outcome.outcome].message} ` || ''}${outcome.reason}`), | |
outcome.extra ?? '' | |
]), | |
fields: [{ | |
name: `${this.player.user.username} (Player)`, | |
value: this.renderHand(this.player.hand, false), | |
inline: true | |
}, { | |
name: `${this.dealer.user.username} (Dealer)`, | |
value: this.renderHand(this.dealer.hand, outcome ? false : !stood), | |
inline: true | |
}], | |
footer: { | |
text: !outcome ? 'K, Q, J = 10 | A = 1 OR 11' : '' | |
} | |
}; | |
} | |
/** | |
* Gets the outcome result of the game. | |
* @param reason The reason why the game has ended. | |
*/ | |
public reason(reason: string): Record<'win' | 'loss' | 'tie' | 'other', (extra?: string) => Blackjack.Constants.OutcomeResult> { | |
return { | |
win: () => this.outcome = ({ outcome: Blackjack.Constants.Outcome.WIN, reason }), | |
loss: () => this.outcome = ({ outcome: Blackjack.Constants.Outcome.LOSS, reason }), | |
tie: () => this.outcome = ({ outcome: Blackjack.Constants.Outcome.TIE, reason }), | |
other: (extra?: string) => this.outcome = ({ outcome: Blackjack.Constants.Outcome.OTHER, reason, extra }) | |
}; | |
} | |
/** | |
* Gets the outcome. | |
* @param stood Whether the player has stood or not. | |
*/ | |
public getOutcome(stood = this.stood): Blackjack.Constants.OutcomeResult { | |
const playerScore = this.countHand(this.player.hand); | |
const dealerScore = this.countHand(this.dealer.hand); | |
if (playerScore === Blackjack.Constants.BJ_WIN) { | |
this.outcome = this.reason('You got to 21.').win(); | |
} else if (dealerScore === Blackjack.Constants.BJ_WIN) { | |
this.outcome = this.reason('The dealer got to 21 before you.').loss(); | |
} else if (playerScore <= Blackjack.Constants.BJ_WIN && this.player.hand.length === 5) { | |
this.outcome = this.reason('You took 5 cards without going over 21.').win(); | |
} else if (dealerScore <= Blackjack.Constants.BJ_WIN && this.dealer.hand.length === 5) { | |
this.outcome = this.reason('The dealer took 5 cards without going over 21.').loss(); | |
} else if (playerScore > Blackjack.Constants.BJ_WIN) { | |
this.outcome = this.reason('You went over 21 and busted.').loss(); | |
} else if (dealerScore > Blackjack.Constants.BJ_WIN) { | |
this.outcome = this.reason('The dealer went over 21 and busted.').win(); | |
} else if (stood && playerScore > dealerScore) { | |
this.outcome = this.reason(`You stood with a higher score (\`${playerScore}\`) than the dealer (\`${dealerScore}\`)`).win(); | |
} else if (stood && dealerScore > playerScore) { | |
this.outcome = this.reason(`You stood with a lower score (\`${playerScore}\`) than the dealer (\`${dealerScore}\`)`).loss(); | |
} else if (stood && dealerScore === playerScore) { | |
this.outcome = this.reason('You tied with the dealer.').tie(); | |
} | |
return this.outcome; | |
} | |
} | |
export namespace Blackjack { | |
export interface Options { | |
dealer: User; | |
player: User; | |
} | |
export interface Player { | |
user: User; | |
hand: Blackjack.Cards.Card[]; | |
} | |
} | |
export namespace Blackjack.Constants { | |
export const BJ_WIN = 21; | |
export const BJ_DEALER_MAX = 17; | |
export const BJ_FACE = 10; | |
export const BJ_ACE_MIN = 1; | |
export const BJ_ACE_MAX = 11; | |
export const SUITS = [ | |
'♠', '♥', '♦', '♣' | |
] as const; | |
export const FACES = [ | |
'A', 'J', 'Q', 'K', | |
...Array.from({ length: 9 }, (_, i) => i + 2) | |
] as const; | |
export enum Outcome { | |
WIN = 1, | |
LOSS, | |
TIE, | |
OTHER | |
}; | |
export const Outcomes: Record<Outcome, { | |
message: string; | |
color: number; | |
}> = { | |
[Outcome.WIN]: { message: 'You win!', color: 0x4CAF50 }, | |
[Outcome.LOSS]: { message: 'You lost ):', color: 0xE53935 }, | |
[Outcome.TIE]: { message: 'You tied.', color: 0xFFB300 }, | |
[Outcome.OTHER]: { message: '', color: 0xFFB300 }, | |
}; | |
/** | |
* Represents the result of the game. | |
*/ | |
export type OutcomeResult = { | |
outcome: Outcome; | |
reason: string; | |
extra?: string; | |
} | null; | |
} | |
export namespace Blackjack.Cards { | |
/** | |
* Represents a card within the hand of the player. | |
*/ | |
export interface Card { | |
suit: typeof Constants.SUITS[number]; | |
face: typeof Constants.FACES[number]; | |
baseValue: number; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment