2125 lines
36 KiB
JavaScript
2125 lines
36 KiB
JavaScript
const {
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle
|
||
} = require("discord.js");
|
||
|
||
const {
|
||
randomUUID
|
||
} = require("node:crypto");
|
||
|
||
const {
|
||
getCollections,
|
||
getCookieBalance,
|
||
transferCookies,
|
||
temporaryEphemeralReply,
|
||
getUnixTime
|
||
} = require("./cookieEconomy");
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CONFIG
|
||
* ============================================================
|
||
*/
|
||
|
||
const GAME_COLLECTION =
|
||
"items_games";
|
||
|
||
|
||
const BLACKJACK_BETS = [
|
||
10,
|
||
50,
|
||
100,
|
||
500,
|
||
1000
|
||
];
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SHOW BLACKJACK MENU
|
||
* ============================================================
|
||
*/
|
||
|
||
async function showBlackjack(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Crumb Saucer session."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Resume an unfinished game.
|
||
*/
|
||
|
||
const active =
|
||
await getActiveBlackjack(
|
||
interaction.user.id,
|
||
interaction.guildId
|
||
);
|
||
|
||
|
||
if (active) {
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
active
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
active
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Crumb Saucer Blackjack"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"Beat the dealer without going over **21**.",
|
||
"",
|
||
"**Dealer stands on 17.**",
|
||
"**Blackjack pays 3:2.**",
|
||
"**Push refunds your wager.**",
|
||
"",
|
||
"Choose your wager below."
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields({
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} 🍪**`
|
||
})
|
||
|
||
.setFooter({
|
||
text:
|
||
`crumbSaucerBlackjack:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildBetButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* START GAME
|
||
* ============================================================
|
||
*/
|
||
|
||
async function startBlackjack(
|
||
interaction,
|
||
bet,
|
||
allowCustomBet =
|
||
false
|
||
) {
|
||
/*
|
||
* Normal wager buttons must use one of our
|
||
* predefined values.
|
||
*
|
||
* All In and Run It Back are allowed to use
|
||
* custom wager amounts.
|
||
*/
|
||
|
||
if (
|
||
!allowCustomBet
|
||
&&
|
||
!BLACKJACK_BETS.includes(
|
||
bet
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That wager isn't available."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Every wager, predefined or custom, must
|
||
* still be a positive safe integer.
|
||
*/
|
||
|
||
if (
|
||
!Number.isSafeInteger(
|
||
bet
|
||
)
|
||
||
|
||
bet <=
|
||
0
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That wager isn't valid."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack table."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
const games =
|
||
db.collection(
|
||
GAME_COLLECTION
|
||
);
|
||
|
||
|
||
/*
|
||
* Prevent multiple active hands.
|
||
*/
|
||
|
||
const existing =
|
||
await getActiveBlackjack(
|
||
interaction.user.id,
|
||
interaction.guildId
|
||
);
|
||
|
||
|
||
if (existing) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"You already have an active Blackjack hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerId =
|
||
interaction.user.id;
|
||
|
||
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
playerBalance,
|
||
houseBalance
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
playerId
|
||
),
|
||
|
||
getCookieBalance(
|
||
houseId
|
||
)
|
||
]);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
bet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`You need **${bet.toLocaleString()} Cookies** for that wager.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Natural Blackjack returns:
|
||
*
|
||
* wager + 3:2 winnings
|
||
*
|
||
* = 2.5x total payout.
|
||
*
|
||
* The wager will move into the house before
|
||
* resolution, so the house currently needs
|
||
* enough to cover the additional 1.5x.
|
||
*/
|
||
|
||
if (
|
||
houseBalance <
|
||
Math.ceil(
|
||
bet *
|
||
1.5
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"The Crumb Saucer vault can't currently cover that wager."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Take wager.
|
||
*/
|
||
|
||
try {
|
||
await transferCookies(
|
||
playerId,
|
||
houseId,
|
||
bet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Your Cookie balance changed before the hand could begin."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* ========================================================
|
||
* REAL 52-CARD DECK
|
||
* ========================================================
|
||
*/
|
||
|
||
const deck =
|
||
createDeck();
|
||
|
||
|
||
shuffleDeck(
|
||
deck
|
||
);
|
||
|
||
|
||
const playerCards = [
|
||
deck.pop(),
|
||
deck.pop()
|
||
];
|
||
|
||
|
||
const dealerCards = [
|
||
deck.pop(),
|
||
deck.pop()
|
||
];
|
||
|
||
|
||
const game = {
|
||
eventType:
|
||
"Blackjack",
|
||
|
||
gameId:
|
||
randomUUID(),
|
||
|
||
userId:
|
||
playerId,
|
||
|
||
guildId:
|
||
interaction.guildId,
|
||
|
||
channelId:
|
||
interaction.channelId,
|
||
|
||
state:
|
||
"Playing",
|
||
|
||
bet,
|
||
|
||
originalBet:
|
||
bet,
|
||
|
||
doubled:
|
||
false,
|
||
|
||
deck,
|
||
|
||
playerCards,
|
||
|
||
dealerCards,
|
||
|
||
createdAt:
|
||
getUnixTime()
|
||
};
|
||
|
||
|
||
await games.insertOne(
|
||
game
|
||
);
|
||
|
||
|
||
/*
|
||
* ========================================================
|
||
* NATURAL BLACKJACK CHECK
|
||
* ========================================================
|
||
*/
|
||
|
||
const playerBlackjack =
|
||
isBlackjack(
|
||
playerCards
|
||
);
|
||
|
||
|
||
const dealerBlackjack =
|
||
isBlackjack(
|
||
dealerCards
|
||
);
|
||
|
||
|
||
if (
|
||
playerBlackjack ||
|
||
dealerBlackjack
|
||
) {
|
||
/*
|
||
* Both natural Blackjack = Push.
|
||
*/
|
||
|
||
if (
|
||
playerBlackjack &&
|
||
dealerBlackjack
|
||
) {
|
||
return finishBlackjack(
|
||
interaction,
|
||
game,
|
||
"Push"
|
||
);
|
||
}
|
||
|
||
|
||
if (playerBlackjack) {
|
||
return finishBlackjack(
|
||
interaction,
|
||
game,
|
||
"Blackjack"
|
||
);
|
||
}
|
||
|
||
|
||
return finishBlackjack(
|
||
interaction,
|
||
game,
|
||
"DealerBlackjack"
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
game
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
game
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* ALL IN
|
||
* ============================================================
|
||
*/
|
||
|
||
async function startBlackjackAllIn(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack table."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Resolve balance at click time.
|
||
*/
|
||
|
||
const balance =
|
||
await getCookieBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (
|
||
balance <=
|
||
0
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"You don't have any Cookies to go All In with."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* true allows the balance to be used even though
|
||
* it likely isn't one of BLACKJACK_BETS.
|
||
*/
|
||
|
||
return startBlackjack(
|
||
interaction,
|
||
balance,
|
||
true
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RUN IT BACK
|
||
* ============================================================
|
||
*
|
||
* Reuses only the previous BET.
|
||
*
|
||
* A completely new shuffled deck and hand are created.
|
||
*/
|
||
|
||
async function runBlackjackBack(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
/*
|
||
* Expected:
|
||
*
|
||
* crumbBlackjackResult:<userId>:<bet>
|
||
*/
|
||
|
||
if (
|
||
parts[0] !==
|
||
"crumbBlackjackResult"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Blackjack result can no longer be replayed."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
parts[1];
|
||
|
||
|
||
const bet =
|
||
Number(
|
||
parts[2]
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack session."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
!Number.isSafeInteger(
|
||
bet
|
||
)
|
||
||
|
||
bet <=
|
||
0
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"The previous wager is no longer valid."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Reuse the exact previous wager.
|
||
*
|
||
* true is required because a previous All In bet
|
||
* may not be part of BLACKJACK_BETS.
|
||
*/
|
||
|
||
return startBlackjack(
|
||
interaction,
|
||
bet,
|
||
true
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* HIT
|
||
* ============================================================
|
||
*/
|
||
|
||
async function hitBlackjack(
|
||
interaction
|
||
) {
|
||
const game =
|
||
await getGameFromInteraction(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (!game) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Blackjack hand no longer exists."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.state !==
|
||
"Playing"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This hand is already finished."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const deck =
|
||
[
|
||
...game.deck
|
||
];
|
||
|
||
|
||
const playerCards =
|
||
[
|
||
...game.playerCards
|
||
];
|
||
|
||
|
||
playerCards.push(
|
||
deck.pop()
|
||
);
|
||
|
||
|
||
const score =
|
||
getHandValue(
|
||
playerCards
|
||
);
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
const games =
|
||
db.collection(
|
||
GAME_COLLECTION
|
||
);
|
||
|
||
|
||
await games.updateOne(
|
||
{
|
||
gameId:
|
||
game.gameId,
|
||
|
||
state:
|
||
"Playing"
|
||
},
|
||
{
|
||
$set: {
|
||
deck,
|
||
playerCards
|
||
}
|
||
}
|
||
);
|
||
|
||
|
||
const updated = {
|
||
...game,
|
||
deck,
|
||
playerCards
|
||
};
|
||
|
||
|
||
/*
|
||
* Bust.
|
||
*/
|
||
|
||
if (
|
||
score >
|
||
21
|
||
) {
|
||
return finishBlackjack(
|
||
interaction,
|
||
updated,
|
||
"Bust"
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Automatically stand on exactly 21.
|
||
*/
|
||
|
||
if (
|
||
score ===
|
||
21
|
||
) {
|
||
return standBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
updated
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
updated
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* STAND
|
||
* ============================================================
|
||
*/
|
||
|
||
async function standBlackjack(
|
||
interaction,
|
||
suppliedGame =
|
||
null
|
||
) {
|
||
let game =
|
||
suppliedGame;
|
||
|
||
|
||
if (!game) {
|
||
game =
|
||
await getGameFromInteraction(
|
||
interaction
|
||
);
|
||
}
|
||
|
||
|
||
if (!game) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Blackjack hand no longer exists."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const deck =
|
||
[
|
||
...game.deck
|
||
];
|
||
|
||
|
||
const dealerCards =
|
||
[
|
||
...game.dealerCards
|
||
];
|
||
|
||
|
||
/*
|
||
* Dealer stands on 17.
|
||
*/
|
||
|
||
while (
|
||
getHandValue(
|
||
dealerCards
|
||
)
|
||
<
|
||
17
|
||
) {
|
||
dealerCards.push(
|
||
deck.pop()
|
||
);
|
||
}
|
||
|
||
|
||
const updated = {
|
||
...game,
|
||
deck,
|
||
dealerCards
|
||
};
|
||
|
||
|
||
const playerScore =
|
||
getHandValue(
|
||
updated.playerCards
|
||
);
|
||
|
||
|
||
const dealerScore =
|
||
getHandValue(
|
||
dealerCards
|
||
);
|
||
|
||
|
||
let result;
|
||
|
||
|
||
if (
|
||
dealerScore >
|
||
21
|
||
) {
|
||
result =
|
||
"DealerBust";
|
||
}
|
||
|
||
|
||
else if (
|
||
playerScore >
|
||
dealerScore
|
||
) {
|
||
result =
|
||
"Win";
|
||
}
|
||
|
||
|
||
else if (
|
||
playerScore <
|
||
dealerScore
|
||
) {
|
||
result =
|
||
"Loss";
|
||
}
|
||
|
||
|
||
/*
|
||
* Same value = Push.
|
||
*/
|
||
|
||
else {
|
||
result =
|
||
"Push";
|
||
}
|
||
|
||
|
||
return finishBlackjack(
|
||
interaction,
|
||
updated,
|
||
result
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DOUBLE
|
||
* ============================================================
|
||
*/
|
||
|
||
async function doubleBlackjack(
|
||
interaction
|
||
) {
|
||
const game =
|
||
await getGameFromInteraction(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (!game) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Blackjack hand no longer exists."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Double only on initial two cards.
|
||
*/
|
||
|
||
if (
|
||
game.playerCards.length !==
|
||
2
|
||
||
|
||
game.doubled
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"You can only Double on your initial two cards."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerBalance =
|
||
await getCookieBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
game.originalBet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`You need another **${game.originalBet.toLocaleString()} Cookies** to Double.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
interaction.user.id,
|
||
interaction.client.user.id,
|
||
game.originalBet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Your balance changed before you could Double."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
const deck =
|
||
[
|
||
...game.deck
|
||
];
|
||
|
||
|
||
const playerCards =
|
||
[
|
||
...game.playerCards
|
||
];
|
||
|
||
|
||
/*
|
||
* Double receives exactly one additional card.
|
||
*/
|
||
|
||
playerCards.push(
|
||
deck.pop()
|
||
);
|
||
|
||
|
||
const updated = {
|
||
...game,
|
||
|
||
deck,
|
||
playerCards,
|
||
|
||
bet:
|
||
game.bet +
|
||
game.originalBet,
|
||
|
||
doubled:
|
||
true
|
||
};
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
await db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.updateOne(
|
||
{
|
||
gameId:
|
||
game.gameId,
|
||
|
||
state:
|
||
"Playing"
|
||
},
|
||
{
|
||
$set: {
|
||
deck,
|
||
playerCards,
|
||
|
||
bet:
|
||
updated.bet,
|
||
|
||
doubled:
|
||
true
|
||
}
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
getHandValue(
|
||
playerCards
|
||
)
|
||
>
|
||
21
|
||
) {
|
||
return finishBlackjack(
|
||
interaction,
|
||
updated,
|
||
"Bust"
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Double automatically stands.
|
||
*/
|
||
|
||
return standBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* FINISH GAME
|
||
* ============================================================
|
||
*/
|
||
|
||
async function finishBlackjack(
|
||
interaction,
|
||
game,
|
||
result
|
||
) {
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
let payout =
|
||
0;
|
||
|
||
|
||
switch (result) {
|
||
/*
|
||
* Normal win.
|
||
*/
|
||
|
||
case "Win":
|
||
case "DealerBust":
|
||
|
||
payout =
|
||
game.bet *
|
||
2;
|
||
|
||
break;
|
||
|
||
|
||
/*
|
||
* Push:
|
||
*
|
||
* Return exactly the wager.
|
||
*/
|
||
|
||
case "Push":
|
||
|
||
payout =
|
||
game.bet;
|
||
|
||
break;
|
||
|
||
|
||
/*
|
||
* Natural Blackjack:
|
||
*
|
||
* stake + 3:2 winnings.
|
||
*/
|
||
|
||
case "Blackjack":
|
||
|
||
payout =
|
||
Math.floor(
|
||
game.bet *
|
||
2.5
|
||
);
|
||
|
||
break;
|
||
|
||
|
||
/*
|
||
* Bust / Loss / Dealer Blackjack.
|
||
*/
|
||
|
||
default:
|
||
|
||
payout =
|
||
0;
|
||
}
|
||
|
||
|
||
/*
|
||
* Pay player.
|
||
*/
|
||
|
||
if (
|
||
payout >
|
||
0
|
||
) {
|
||
const houseBalance =
|
||
await getCookieBalance(
|
||
houseId
|
||
);
|
||
|
||
|
||
if (
|
||
houseBalance <
|
||
payout
|
||
) {
|
||
console.error(
|
||
`[Blackjack] House cannot cover payout ${payout} for ${game.userId}`
|
||
);
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Blackjack Error"
|
||
)
|
||
|
||
.setDescription(
|
||
"The Crumb Saucer vault couldn't cover the payout. Please contact an administrator."
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`crumbBlackjackResult:${game.userId}:${game.bet}`
|
||
})
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
|
||
await transferCookies(
|
||
houseId,
|
||
game.userId,
|
||
payout
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Mark game finished.
|
||
*/
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
await db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.updateOne(
|
||
{
|
||
gameId:
|
||
game.gameId,
|
||
|
||
state:
|
||
"Playing"
|
||
},
|
||
{
|
||
$set: {
|
||
state:
|
||
"Finished",
|
||
|
||
result,
|
||
|
||
payout,
|
||
|
||
finishedAt:
|
||
getUnixTime(),
|
||
|
||
dealerCards:
|
||
game.dealerCards,
|
||
|
||
playerCards:
|
||
game.playerCards
|
||
}
|
||
}
|
||
);
|
||
|
||
|
||
const playerBalance =
|
||
await getCookieBalance(
|
||
game.userId
|
||
);
|
||
|
||
|
||
const profit =
|
||
payout -
|
||
game.bet;
|
||
|
||
|
||
const description = [
|
||
"**Dealer**",
|
||
formatCards(
|
||
game.dealerCards
|
||
),
|
||
`Score: **${getHandValue(game.dealerCards)}**`,
|
||
"",
|
||
"**You**",
|
||
formatCards(
|
||
game.playerCards
|
||
),
|
||
`Score: **${getHandValue(game.playerCards)}**`
|
||
];
|
||
|
||
|
||
if (
|
||
result ===
|
||
"Push"
|
||
) {
|
||
description.push(
|
||
"",
|
||
"🤝 **Push — your wager has been refunded.**"
|
||
);
|
||
}
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
getResultTitle(
|
||
result
|
||
)
|
||
)
|
||
|
||
.setDescription(
|
||
description.join(
|
||
"\n"
|
||
)
|
||
)
|
||
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Bet",
|
||
|
||
value:
|
||
`${game.bet.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Payout",
|
||
|
||
value:
|
||
`${payout.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Profit",
|
||
|
||
value:
|
||
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Your Vault",
|
||
|
||
value:
|
||
`${playerBalance.toLocaleString()} 🍪`
|
||
}
|
||
)
|
||
|
||
/*
|
||
* Owner + previous exact wager.
|
||
*/
|
||
|
||
.setFooter({
|
||
text:
|
||
`crumbBlackjackResult:${game.userId}:${game.bet}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* GAME EMBED
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildGameEmbed(
|
||
game
|
||
) {
|
||
const playerScore =
|
||
getHandValue(
|
||
game.playerCards
|
||
);
|
||
|
||
|
||
const dealerVisible =
|
||
game.dealerCards[0];
|
||
|
||
|
||
return new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Crumb Saucer Blackjack"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"**Dealer**",
|
||
`${formatCard(dealerVisible)} 🂠`,
|
||
"",
|
||
"**You**",
|
||
formatCards(
|
||
game.playerCards
|
||
),
|
||
`Score: **${playerScore}**`
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Bet",
|
||
|
||
value:
|
||
`**${game.bet.toLocaleString()} 🍪**`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Status",
|
||
|
||
value:
|
||
"**Your turn**",
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`crumbBlackjack:${game.gameId}:${game.userId}`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* BET BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildBetButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
...BLACKJACK_BETS.map(
|
||
bet =>
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
`crumbBlackjackBet${bet}`
|
||
)
|
||
|
||
.setLabel(
|
||
bet.toLocaleString()
|
||
)
|
||
|
||
.setEmoji(
|
||
"🍪"
|
||
)
|
||
|
||
.setStyle(
|
||
bet >=
|
||
500
|
||
? ButtonStyle.Primary
|
||
: ButtonStyle.Secondary
|
||
)
|
||
)
|
||
),
|
||
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbBlackjackAllIn"
|
||
)
|
||
.setLabel(
|
||
"All In"
|
||
)
|
||
.setEmoji(
|
||
"🔥"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerBack"
|
||
)
|
||
.setLabel(
|
||
"Back to Crumb Saucer"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* ACTIVE GAME BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildGameButtons(
|
||
game
|
||
) {
|
||
const canDouble =
|
||
game.playerCards.length ===
|
||
2
|
||
&&
|
||
!game.doubled;
|
||
|
||
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbBlackjackHit"
|
||
)
|
||
.setLabel(
|
||
"Hit"
|
||
)
|
||
.setEmoji(
|
||
"➕"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbBlackjackStand"
|
||
)
|
||
.setLabel(
|
||
"Stand"
|
||
)
|
||
.setEmoji(
|
||
"✋"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbBlackjackDouble"
|
||
)
|
||
.setLabel(
|
||
"Double"
|
||
)
|
||
.setEmoji(
|
||
"💰"
|
||
)
|
||
.setDisabled(
|
||
!canDouble
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RESULT BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildResultButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbBlackjackRunItBack"
|
||
)
|
||
.setLabel(
|
||
"Run It Back"
|
||
)
|
||
.setEmoji(
|
||
"🔁"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerBlackjack"
|
||
)
|
||
.setLabel(
|
||
"Change Bet"
|
||
)
|
||
.setEmoji(
|
||
"🃏"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"crumbSaucerBack"
|
||
)
|
||
.setLabel(
|
||
"Crumb Saucer"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CARDS
|
||
* ============================================================
|
||
*/
|
||
|
||
function createDeck() {
|
||
const suits = [
|
||
"♠️",
|
||
"♥️",
|
||
"♦️",
|
||
"♣️"
|
||
];
|
||
|
||
|
||
const ranks = [
|
||
"A",
|
||
"2",
|
||
"3",
|
||
"4",
|
||
"5",
|
||
"6",
|
||
"7",
|
||
"8",
|
||
"9",
|
||
"10",
|
||
"J",
|
||
"Q",
|
||
"K"
|
||
];
|
||
|
||
|
||
const deck = [];
|
||
|
||
|
||
for (
|
||
const suit
|
||
of suits
|
||
) {
|
||
for (
|
||
const rank
|
||
of ranks
|
||
) {
|
||
deck.push({
|
||
rank,
|
||
suit
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
return deck;
|
||
}
|
||
|
||
|
||
function shuffleDeck(
|
||
deck
|
||
) {
|
||
for (
|
||
let i =
|
||
deck.length -
|
||
1;
|
||
i >
|
||
0;
|
||
i--
|
||
) {
|
||
const j =
|
||
Math.floor(
|
||
Math.random() *
|
||
(
|
||
i +
|
||
1
|
||
)
|
||
);
|
||
|
||
|
||
[
|
||
deck[i],
|
||
deck[j]
|
||
] = [
|
||
deck[j],
|
||
deck[i]
|
||
];
|
||
}
|
||
}
|
||
|
||
|
||
function getHandValue(
|
||
cards
|
||
) {
|
||
let total =
|
||
0;
|
||
|
||
|
||
let aces =
|
||
0;
|
||
|
||
|
||
for (
|
||
const card
|
||
of cards
|
||
) {
|
||
switch (
|
||
card.rank
|
||
) {
|
||
case "A":
|
||
|
||
total +=
|
||
11;
|
||
|
||
aces++;
|
||
|
||
break;
|
||
|
||
|
||
case "K":
|
||
case "Q":
|
||
case "J":
|
||
|
||
total +=
|
||
10;
|
||
|
||
break;
|
||
|
||
|
||
default:
|
||
|
||
total +=
|
||
Number(
|
||
card.rank
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
while (
|
||
total >
|
||
21
|
||
&&
|
||
aces >
|
||
0
|
||
) {
|
||
total -=
|
||
10;
|
||
|
||
aces--;
|
||
}
|
||
|
||
|
||
return total;
|
||
}
|
||
|
||
|
||
function isBlackjack(
|
||
cards
|
||
) {
|
||
return (
|
||
cards.length ===
|
||
2
|
||
&&
|
||
getHandValue(
|
||
cards
|
||
) ===
|
||
21
|
||
);
|
||
}
|
||
|
||
|
||
function formatCard(
|
||
card
|
||
) {
|
||
return `**${card.rank}${card.suit}**`;
|
||
}
|
||
|
||
|
||
function formatCards(
|
||
cards
|
||
) {
|
||
return cards
|
||
.map(
|
||
formatCard
|
||
)
|
||
.join(
|
||
" "
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DATABASE LOOKUPS
|
||
* ============================================================
|
||
*/
|
||
|
||
async function getActiveBlackjack(
|
||
userId,
|
||
guildId
|
||
) {
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
return db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.findOne({
|
||
eventType:
|
||
"Blackjack",
|
||
|
||
userId,
|
||
|
||
guildId,
|
||
|
||
state:
|
||
"Playing"
|
||
});
|
||
}
|
||
|
||
|
||
async function getGameFromInteraction(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
if (
|
||
parts[0] !==
|
||
"crumbBlackjack"
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
|
||
const gameId =
|
||
parts[1];
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
return db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.findOne({
|
||
eventType:
|
||
"Blackjack",
|
||
|
||
gameId,
|
||
|
||
state:
|
||
"Playing"
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SESSION OWNER
|
||
* ============================================================
|
||
*/
|
||
|
||
function getBlackjackSessionOwner(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbSaucer"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbSaucerBlackjack"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
/*
|
||
* Active hand:
|
||
*
|
||
* crumbBlackjack:<gameId>:<userId>
|
||
*/
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbBlackjack"
|
||
) {
|
||
return parts[2];
|
||
}
|
||
|
||
|
||
/*
|
||
* Finished result:
|
||
*
|
||
* crumbBlackjackResult:<userId>:<bet>
|
||
*/
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbBlackjackResult"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
return null;
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RESULT TITLES
|
||
* ============================================================
|
||
*/
|
||
|
||
function getResultTitle(
|
||
result
|
||
) {
|
||
switch (
|
||
result
|
||
) {
|
||
case "Blackjack":
|
||
|
||
return "🃏 BLACKJACK!";
|
||
|
||
|
||
case "Win":
|
||
|
||
return "🎉 You Win!";
|
||
|
||
|
||
case "DealerBust":
|
||
|
||
return "💥 Dealer Bust!";
|
||
|
||
|
||
case "Push":
|
||
|
||
return "🤝 Push — Wager Refunded";
|
||
|
||
|
||
case "Bust":
|
||
|
||
return "💥 Bust!";
|
||
|
||
|
||
case "DealerBlackjack":
|
||
|
||
return "🃏 Dealer Blackjack";
|
||
|
||
|
||
default:
|
||
|
||
return "🃏 Dealer Wins";
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* EXPORTS
|
||
* ============================================================
|
||
*/
|
||
|
||
module.exports = {
|
||
showBlackjack,
|
||
|
||
startBlackjack,
|
||
startBlackjackAllIn,
|
||
|
||
hitBlackjack,
|
||
standBlackjack,
|
||
doubleBlackjack,
|
||
|
||
runBlackjackBack
|
||
}; |