Skip to content

Instantly share code, notes, and snippets.

@melmsie
Last active June 29, 2024 10:43
Show Gist options
  • Save melmsie/8de434115b7ceb0b7f554b68c041ee08 to your computer and use it in GitHub Desktop.
Save melmsie/8de434115b7ceb0b7f554b68c041ee08 to your computer and use it in GitHub Desktop.
Dank Memer Blackjack Command Files
import * as Constants from './constants';
const randomInArray = <T>(arr: readonly T[]): T =>
arr[Math.floor(Math.random() * arr.length)];
export interface Card {
suit: typeof Constants.SUITS[number];
face: typeof Constants.FACES[number];
baseValue: number;
};
const countHandRaw = (cards: Card[]): number =>
cards.reduce((acc, curr) => curr.baseValue + acc, 0);
export const countHand = (hand: Card[]): number => {
for (const card of hand) {
if (card.face === 'A') {
card.baseValue = Constants.BJ_ACE_MAX;
}
}
let lowerAce: Card;
while (
countHandRaw(hand) > Constants.BJ_WIN &&
(lowerAce = hand.find(card => card.face === 'A' && card.baseValue !== Constants.BJ_ACE_MIN))
) {
lowerAce.baseValue = Constants.BJ_ACE_MIN;
}
return countHandRaw(hand);
};
export const deal = (hand: Card[], initial: boolean): void => {
const face = randomInArray(Constants.FACES);
const suit = randomInArray(Constants.SUITS);
if (hand.find(card => card.face === face && card.suit === suit)) {
return deal(hand, initial);
}
const card: Card = {
face,
suit,
baseValue: typeof face === 'number'
? face
: (face === 'A' ? Constants.BJ_ACE_MIN : Constants.BJ_FACE),
};
if (initial && countHand([ ...hand, card ]) >= Constants.BJ_WIN) {
return deal(hand, initial);
}
hand.push(card);
};
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 },
};
export type OutcomeResult = {
outcome: Outcome;
reason: string;
extra?: string;
} | null;
/*
- NOTE TO THE READER -
This code is (for the most part), extremely old. We are aware that some things are less than idea, outdated, or both.
We are rewriting the entire bot right now, which includes this command, so we aren't spending time improving this current implementation.
This file is available for transparency with people claiming the command is rigged, and this gist will be updated every time the command is.
This is not meant to be "understandable" by those who don't know javascript or the discord api, so please don't make inferences unless you know what you're talking about.
We welcome suggestions on how to fix known existing bugs in this on our subreddit if you so chose, thank you.
*/
const GenericCurrencyCommand = require('../../../models/GenericCurrencyCommand');
const { components: { Button } } = require('dawn/eris-interop/components');
const util = require('./cards');
const Constants = require('./constants');
const logic = require('./logic');
module.exports = new GenericCurrencyCommand(
async ({ Memer, msg, addCD, Currency, userEntry, donor, isGlobalPremiumGuild }) => {
if (userEntry.props.pocket >= Currency.constants.MAX_SAFE_COIN_AMOUNT) {
return msg.reply(`**Uh oh, looks like someone is too rich for their own good!**\nRather than wiping coins caused by inflation, we are now capping how many you can hold at one time.\nYou can either get rid of some coins (share, buy, donate them to the bot, prestige, etc) or you can just preserve your balance in the state that it's currently in.\nYou are not able to do any commands that gain you coins until you are under the cap of *${Currency.constants.MAX_SAFE_COIN_AMOUNT.toLocaleString()} coins*`);
}
const user = msg.author;
const coins = userEntry.props.pocket;
let multi = await Memer.calcMultiplier(Memer, msg.author, userEntry, donor, msg, isGlobalPremiumGuild);
multi = Math.max(Math.min(multi.total, 500), 0);
multi = (multi / 500) * 100;
// Multi is now a percent of having a full multiplier. To get 100% multi when gambling, you'll now need a 500% multi. Since it's not easy to get a 500% multi, I've upped the base pay again by a bit.
let maxAmount = Currency.constants.MAX_SAFE_COMMAND_AMOUNT;
if (userEntry.hasInventoryItem('pepecrown')) {
maxAmount = Currency.constants.MAX_SAFE_COMMAND_AMOUNT * 2;
}
if (coins >= maxAmount) {
return msg.reply('You are too rich to play! Why don\'t you go and do something productive with your coins smh');
}
let bet = msg.args.args[0];
if (!bet) {
return msg.reply('You need to bet something, seems like common sense tbh.');
}
if (bet < 1 || !Number.isInteger(Number(bet))) {
if (bet && bet.toLowerCase().includes('k')) {
const givenKay = bet.replace(/k/g, '');
if (!Number.isInteger(Number(givenKay * 1000)) || isNaN(givenKay * 1000)) {
return msg.reply('You have to to actually bet a whole number, dummy. Not ur dumb feelings');
} else {
bet = givenKay * 1000;
}
} else if (bet.toLowerCase() === 'all') {
bet = coins;
} else if (bet.toLowerCase() === 'max') {
bet = Math.min(coins, Currency.constants.MAX_SAFE_BET_AMOUNT);
} else if (bet.toLowerCase() === 'half') {
bet = Math.round(coins / 2);
} else {
return msg.reply('You have to bet actual coins, dont try to break me.');
}
}
if (coins === 0) {
return msg.reply('You have no coins in your wallet to gamble with lol.');
}
if (bet > coins) {
return msg.reply(`You only have ${coins.toLocaleString()} coins, dont try and lie to me hoe.`);
}
if (bet > Currency.constants.MAX_SAFE_BET_AMOUNT) {
return msg.reply(`You can't bet more than **${Currency.constants.MAX_SAFE_BET_AMOUNT.toLocaleString()} coins** at once, sorry not sorry`);
}
if (bet < Currency.constants.MIN_SAFE_BET_AMOUNT) {
return msg.reply(`You can't bet less than **${Currency.constants.MIN_SAFE_BET_AMOUNT.toLocaleString()} coins**, sorry not sorry`);
}
await addCD();
// initial state
let stood = false;
/** @type {import('./constants').OutcomeResult} */
let outcome = null;
/** @type {Record<'player' | 'dealer', import('./cards').Card[]>} */
const hands = {
player: [],
dealer: []
};
for (let i = 0; i < 2; i++) {
util.deal(hands.player, true);
util.deal(hands.dealer, true);
}
const getEmbed = () => ({
embed: logic.renderEmbed(user, hands.player, hands.dealer, stood, outcome),
components: [
new Button('Hit', 'hit'),
new Button('Stand', 'stand'),
new Button('Forfeit', 'end')
].map(b => b.setDisabled(!!outcome))
});
// main logic
await msg.collectComponentInteractions(getEmbed(), {}, (ctx, msg) => {
try {
if (outcome) {
return ctx.ack();
}
/**
* @param {{ ctx: import('dawn/src/eris-interop/components/ResponseContext').CollectorResponseContext, msg: import('eris').Message }} param0
*/
const update = async ({ ctx, msg }) => {
outcome ??= logic.getOutcome(hands.player, hands.dealer, stood);
switch (outcome?.outcome) {
case Constants.Outcome.WIN: {
let winnings = Math.ceil(bet * (Math.random() + 0.35)); // "Base Multi" will pay between 35% of the bet and 135% of the bet
winnings = Math.min(Currency.constants.MAX_SAFE_WIN_AMOUNT, winnings + Math.ceil(winnings * (multi / 100))); // This brings in the user's secret multi (pls multi)
Memer.ddog.increment('BJ.WON');
Memer.ddog.incrementBy('BJ.WON.TOTAL', winnings);
outcome.extra = `You won **⏣ ${winnings.toLocaleString()}**. You now have ⏣ ${(userEntry.props.pocket + winnings).toLocaleString()}.`;
await userEntry
.addPocket(winnings)
.calculateExperienceGain()
.updateGambleStats(true, winnings, 'blackjackStats')
.save();
break;
}
case Constants.Outcome.OTHER: {
outcome.extra = 'The dealer is keeping your money to deal with your bullcrap.';
await userEntry
.removePocket(bet, 'blackjack')
.calculateExperienceGain()
.updateGambleStats(false, bet, 'blackjackStats')
.save();
break;
}
case Constants.Outcome.LOSS: {
Memer.ddog.increment('BJ.LOST');
Memer.ddog.incrementBy('BJ.TOTAL.LOST', bet);
outcome.extra ??= `You lost **⏣ ${Number(bet).toLocaleString()}**. You now have ${(userEntry.props.pocket - bet).toLocaleString()}.`;
await userEntry
.removePocket(bet, 'blackjack')
.calculateExperienceGain()
.updateGambleStats(false, bet, 'blackjackStats')
.save();
break;
}
case Constants.Outcome.TIE: {
Memer.ddog.increment('BJ.TIE');
outcome.extra = `Your wallet hasn't changed! You have **⏣ ${userEntry.props.pocket.toLocaleString()}** still.`;
break;
}
}
if (ctx) {
await ctx.editOriginal(getEmbed(outcome));
} else {
await msg.edit(getEmbed(outcome));
}
if (outcome) {
ctx?.end();
}
};
if (ctx === null) {
outcome = { outcome: Constants.Outcome.OTHER, reason: 'You didn\'t respond in time. ' };
return update({ ctx, msg });
}
if (ctx.member.user.id !== user.id) {
return ctx.respond({
ephemeral: true,
content: 'Go start your own game of blackjack.'
});
}
switch (ctx.customID) {
case 'hit':
util.deal(hands.player, false);
return update({ ctx, msg });
case 'stand':
stood = true;
while (util.countHand(hands.dealer) < Constants.BJ_DEALER_MAX) {
util.deal(hands.dealer, false);
}
return update({ ctx, msg });
case 'end':
outcome = {
outcome: Constants.Outcome.OTHER,
reason: 'You ended the game.'
};
return update({ ctx, msg });
}
} catch (e) {}
});
},
{
triggers: ['blackjack', 'bj'],
cooldown: 10 * 1000,
donorCD: 5 * 1000,
usage: '{command} <number>',
shortDescription: 'Play and bet against the bot in blackjack!',
description: 'Take your chances and test your skills at blackjack. Warning, I am very good at stealing your money. Learn to play blackjack [here](https://www.youtube.com/watch?v=VB-6MvXvsKo). (Multiplier affects this command up to 100% max)',
cooldownMessage: 'If I let you bet whenever you wanted, you\'d be a lot more poor. Wait ',
missingArgs: 'You gotta gamble some of ur coins bro'
}
);
import { EmbedOptions, User } from 'eris';
import { Card, countHand } from './cards';
import { BJ_WIN, Outcome, OutcomeResult, Outcomes } from './constants';
const renderCard = (card: Card, idx: number, hide: boolean): string =>
`[\`${
idx > 0 && hide
? '?'
: `${card.suit} ${card.face}`
}\`](https://i.imgur.com/1Ob4BIs.png)`
const renderHand = (hand: Card[], hide: boolean): string =>
`Cards - **${
hand
.map((card, idx) => renderCard(card, idx, hide))
.join(' ')
}**\nTotal - \`${hide ? '` ? `' : countHand(hand)}\``;
export const renderEmbed = (
author: User,
playerHand: Card[],
dealerHand: Card[],
stood: boolean,
outcome: OutcomeResult,
): EmbedOptions => ({
author: {
name: `${author.username}'s blackjack game`,
icon_url: author.dynamicAvatarURL()
},
color: outcome ? Outcomes[outcome.outcome].color : 0x26A69A,
description: !outcome
? ''
: `**${Outcomes[outcome.outcome].message} ${outcome.reason}**\n${outcome.extra ?? ''}`,
fields: [ {
name: `${author.username} (Player)`,
value: renderHand(playerHand, false),
inline: true,
}, {
name: `Dank Memer (Dealer)`,
value: renderHand(dealerHand, outcome ? false : !stood),
inline: true
} ],
footer: {
text: !outcome ? 'K, Q, J = 10 | A = 1 or 11' : ''
}
});
const win = (reason: string): OutcomeResult => ({
outcome: Outcome.WIN,
reason,
});
const loss = (reason: string): OutcomeResult => ({
outcome: Outcome.LOSS,
reason,
});
const tie = (reason: string): OutcomeResult => ({
outcome: Outcome.TIE,
reason,
});
export const getOutcome = (
playerHand: Card[],
dealerHand: Card[],
stood: boolean,
): OutcomeResult => {
const playerScore = countHand(playerHand);
const dealerScore = countHand(dealerHand);
if (playerScore === BJ_WIN) {
return win('You got to 21.');
} else if (dealerScore === BJ_WIN) {
return loss('The dealer got to 21 before you.');
} else if (playerScore <= BJ_WIN && playerHand.length === 5) {
return win('You took 5 cards without going over 21.');
} else if (dealerScore <= BJ_WIN && dealerHand.length === 5) {
return loss('The dealer took 5 cards without going over 21.');
} else if (playerScore > BJ_WIN) {
return loss('You went over 21 and busted.');
} else if (dealerScore > BJ_WIN) {
return win('The dealer went over 21 and busted.');
} else if (stood && playerScore > dealerScore) {
return win(`You stood with a higher score (\`${playerScore}\`) than the dealer (\`${dealerScore}\`)`);
} else if (stood && dealerScore > playerScore) {
return loss(`You stood with a lower score (\`${playerScore}\`) than the dealer (\`${dealerScore}\`)`);
} else if (stood && playerScore === dealerScore) {
return tie('You tied with the dealer.');
}
return null;
}
@BrianWasTaken
Copy link

Hi @aetheryx, do you mind telling me the npm package you use for eris components? it's fine if you won't.

We don't use an npm package, we built out eris components ourselves with an internal library

alright thank you.

@BrianWasTaken
Copy link

Uhh hey @aetheryx one last question, what does ctx.ack() do? Just curious though.

@melmsie
Copy link
Author

melmsie commented Sep 8, 2021

Uhh hey @aetheryx one last question, what does ctx.ack() do? Just curious though.

@BrianWasTaken this isn't a support line, so you won't get any more answers after this.

that's us ACKing the ping, which you can read about here by ctrl+f and typing ack: https://discord.com/developers/docs/interactions/receiving-and-responding#responding-to-an-interaction

@BrianWasTaken
Copy link

Got it, I'll dip.

@Militia21
Copy link

wooo typescript!!!!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment