Skip to content

Instantly share code, notes, and snippets.

@BrianWasTaken
Last active November 10, 2021 06:46
Show Gist options
  • Save BrianWasTaken/ed4cc0dca0c830f701ddd58c38b16706 to your computer and use it in GitHub Desktop.
Save BrianWasTaken/ed4cc0dca0c830f701ddd58c38b16706 to your computer and use it in GitHub Desktop.
/**
* 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 {}
});
}
}
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