1534 lines
31 KiB
JavaScript
1534 lines
31 KiB
JavaScript
const {
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle
|
||
} = require("discord.js");
|
||
|
||
const {
|
||
randomUUID
|
||
} = require("node:crypto");
|
||
|
||
const {
|
||
getCollections,
|
||
getCookieBalance,
|
||
transferCookies,
|
||
temporaryEphemeralReply,
|
||
getUnixTime
|
||
} = require("./cookieEconomy");
|
||
|
||
const {
|
||
recordGameOutcome,
|
||
trackProfileEvent
|
||
} = require("./profileSystem");
|
||
|
||
const GAME_COLLECTION =
|
||
"items_games";
|
||
|
||
const BLACKJACK_BETS = [
|
||
10,
|
||
50,
|
||
100,
|
||
500,
|
||
1000
|
||
];
|
||
|
||
async function showBlackjack(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Crumb Saucer session."
|
||
}
|
||
);
|
||
}
|
||
|
||
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()
|
||
});
|
||
}
|
||
|
||
async function startBlackjack(
|
||
interaction,
|
||
bet,
|
||
allowCustomBet = false,
|
||
source = "normal",
|
||
previousAllIn = false,
|
||
previousBet = 0
|
||
) {
|
||
if (
|
||
!allowCustomBet &&
|
||
!BLACKJACK_BETS.includes(bet)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"That wager isn't available."
|
||
}
|
||
);
|
||
}
|
||
|
||
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
|
||
);
|
||
|
||
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)
|
||
]);
|
||
|
||
const allIn =
|
||
bet === playerBalance;
|
||
|
||
if (playerBalance < bet) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`You need **${bet.toLocaleString()} Cookies** for that wager.`
|
||
}
|
||
);
|
||
}
|
||
|
||
if (
|
||
houseBalance <
|
||
Math.ceil(bet * 1.5)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"The Crumb Saucer vault can't currently cover that 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;
|
||
}
|
||
|
||
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,
|
||
allIn,
|
||
vaultAtBet:
|
||
playerBalance,
|
||
source,
|
||
previousAllIn,
|
||
previousBet,
|
||
deck,
|
||
playerCards,
|
||
dealerCards,
|
||
createdAt:
|
||
getUnixTime()
|
||
};
|
||
|
||
await games.insertOne(game);
|
||
|
||
await trackProfileEvent(
|
||
interaction,
|
||
playerId,
|
||
{
|
||
type: "game_start",
|
||
game: "blackjack",
|
||
source,
|
||
wager: bet,
|
||
allIn,
|
||
previousAllIn,
|
||
previousBet
|
||
}
|
||
);
|
||
|
||
const playerBlackjack =
|
||
isBlackjack(playerCards);
|
||
|
||
const dealerBlackjack =
|
||
isBlackjack(dealerCards);
|
||
|
||
if (
|
||
playerBlackjack ||
|
||
dealerBlackjack
|
||
) {
|
||
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)
|
||
});
|
||
}
|
||
|
||
async function startBlackjackAllIn(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This isn't your Blackjack table."
|
||
}
|
||
);
|
||
}
|
||
|
||
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."
|
||
}
|
||
);
|
||
}
|
||
|
||
return startBlackjack(
|
||
interaction,
|
||
balance,
|
||
true,
|
||
"all_in"
|
||
);
|
||
}
|
||
|
||
async function runBlackjackBack(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
const parts =
|
||
footer?.split(":") ?? [];
|
||
|
||
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]);
|
||
|
||
const previousAllIn =
|
||
parts[3] === "1";
|
||
|
||
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."
|
||
}
|
||
);
|
||
}
|
||
|
||
return startBlackjack(
|
||
interaction,
|
||
bet,
|
||
true,
|
||
"run_it_back",
|
||
previousAllIn,
|
||
bet
|
||
);
|
||
}
|
||
|
||
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();
|
||
|
||
await db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.updateOne(
|
||
{
|
||
gameId:
|
||
game.gameId,
|
||
state:
|
||
"Playing"
|
||
},
|
||
{
|
||
$set: {
|
||
deck,
|
||
playerCards
|
||
}
|
||
}
|
||
);
|
||
|
||
const updated = {
|
||
...game,
|
||
deck,
|
||
playerCards
|
||
};
|
||
|
||
if (score > 21) {
|
||
return finishBlackjack(
|
||
interaction,
|
||
updated,
|
||
"Bust"
|
||
);
|
||
}
|
||
|
||
if (score === 21) {
|
||
return standBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(updated)
|
||
],
|
||
components:
|
||
buildGameButtons(updated)
|
||
});
|
||
}
|
||
|
||
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];
|
||
|
||
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";
|
||
}
|
||
|
||
else {
|
||
result = "Push";
|
||
}
|
||
|
||
return finishBlackjack(
|
||
interaction,
|
||
updated,
|
||
result
|
||
);
|
||
}
|
||
|
||
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."
|
||
}
|
||
);
|
||
}
|
||
|
||
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];
|
||
|
||
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"
|
||
);
|
||
}
|
||
|
||
return standBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
async function finishBlackjack(
|
||
interaction,
|
||
game,
|
||
result
|
||
) {
|
||
const { db } = getCollections();
|
||
|
||
const claim = await db
|
||
.collection(GAME_COLLECTION)
|
||
.updateOne(
|
||
{
|
||
gameId: game.gameId,
|
||
state: "Playing"
|
||
},
|
||
{
|
||
$set: {
|
||
state: "Resolving"
|
||
}
|
||
}
|
||
);
|
||
|
||
if (claim.modifiedCount !== 1) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"This Blackjack hand is already being resolved."
|
||
}
|
||
);
|
||
}
|
||
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
let payout = 0;
|
||
|
||
switch (result) {
|
||
case "Win":
|
||
case "DealerBust":
|
||
payout =
|
||
game.bet * 2;
|
||
break;
|
||
|
||
case "Push":
|
||
payout =
|
||
game.bet;
|
||
break;
|
||
|
||
case "Blackjack":
|
||
payout =
|
||
Math.floor(
|
||
game.bet * 2.5
|
||
);
|
||
break;
|
||
|
||
default:
|
||
payout = 0;
|
||
}
|
||
|
||
if (payout > 0) {
|
||
const houseBalance =
|
||
await getCookieBalance(
|
||
houseId
|
||
);
|
||
|
||
if (
|
||
houseBalance <
|
||
payout
|
||
) {
|
||
console.error(
|
||
`[Blackjack] House cannot cover payout ${payout} for ${game.userId}`
|
||
);
|
||
|
||
await db
|
||
.collection(GAME_COLLECTION)
|
||
.updateOne(
|
||
{
|
||
gameId: game.gameId,
|
||
state: "Resolving"
|
||
},
|
||
{
|
||
$set: {
|
||
state: "Finished",
|
||
result: "PayoutError",
|
||
payout: 0,
|
||
finishedAt: getUnixTime()
|
||
}
|
||
}
|
||
);
|
||
|
||
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.originalBet}:${game.allIn ? 1 : 0}`
|
||
})
|
||
],
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
await transferCookies(
|
||
houseId,
|
||
game.userId,
|
||
payout
|
||
);
|
||
}
|
||
|
||
await db
|
||
.collection(
|
||
GAME_COLLECTION
|
||
)
|
||
.updateOne(
|
||
{
|
||
gameId:
|
||
game.gameId,
|
||
state:
|
||
"Resolving"
|
||
},
|
||
{
|
||
$set: {
|
||
state:
|
||
"Finished",
|
||
result,
|
||
payout,
|
||
finishedAt:
|
||
getUnixTime(),
|
||
dealerCards:
|
||
game.dealerCards,
|
||
playerCards:
|
||
game.playerCards
|
||
}
|
||
}
|
||
);
|
||
|
||
const playerBalance =
|
||
await getCookieBalance(
|
||
game.userId
|
||
);
|
||
|
||
const profit =
|
||
payout - game.bet;
|
||
|
||
let profileResult;
|
||
|
||
if (result === "Push") {
|
||
profileResult = "push";
|
||
}
|
||
|
||
else if (
|
||
[
|
||
"Win",
|
||
"DealerBust",
|
||
"Blackjack"
|
||
].includes(result)
|
||
) {
|
||
profileResult = "win";
|
||
}
|
||
|
||
else {
|
||
profileResult = "loss";
|
||
}
|
||
|
||
await recordGameOutcome(
|
||
interaction,
|
||
game.userId,
|
||
{
|
||
game:
|
||
"blackjack",
|
||
result:
|
||
profileResult,
|
||
profit,
|
||
natural:
|
||
result ===
|
||
"Blackjack",
|
||
allIn:
|
||
game.allIn ===
|
||
true
|
||
}
|
||
);
|
||
|
||
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()} 🍪`
|
||
}
|
||
)
|
||
.setFooter({
|
||
text:
|
||
`crumbBlackjackResult:${game.userId}:${game.originalBet}:${game.allIn ? 1 : 0}`
|
||
});
|
||
|
||
return interaction.update({
|
||
embeds: [embed],
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
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}`
|
||
});
|
||
}
|
||
|
||
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
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
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
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
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
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
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(" ");
|
||
}
|
||
|
||
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"
|
||
});
|
||
}
|
||
|
||
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];
|
||
}
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbBlackjack"
|
||
) {
|
||
return parts[2];
|
||
}
|
||
|
||
if (
|
||
parts[0] ===
|
||
"crumbBlackjackResult"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
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";
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
showBlackjack,
|
||
startBlackjack,
|
||
startBlackjackAllIn,
|
||
hitBlackjack,
|
||
standBlackjack,
|
||
doubleBlackjack,
|
||
runBlackjackBack
|
||
};
|