1792 lines
30 KiB
JavaScript
1792 lines
30 KiB
JavaScript
// utils/nutBlackjack.js
|
||
|
||
const {
|
||
EmbedBuilder,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle
|
||
} = require("discord.js");
|
||
|
||
const {
|
||
randomUUID
|
||
} = require("node:crypto");
|
||
|
||
const {
|
||
getCollections,
|
||
getNutBalance,
|
||
transferNuts,
|
||
temporaryEphemeralReply,
|
||
getUnixTime
|
||
} = require("./nutEconomy");
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CONFIG
|
||
* ============================================================
|
||
*/
|
||
|
||
const SESSION_COLLECTION =
|
||
"sessions_blackjack";
|
||
|
||
|
||
const BLACKJACK_BETS = [
|
||
10,
|
||
50,
|
||
100,
|
||
500,
|
||
1000
|
||
];
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SHOW BLACKJACK MENU
|
||
* ============================================================
|
||
*/
|
||
|
||
async function showNutBlackjack(
|
||
interaction
|
||
) {
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Nut-Hub-Session."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Resume active hand if one exists.
|
||
*/
|
||
|
||
const active =
|
||
await getActiveBlackjack(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (active) {
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
active
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
active
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
const balance =
|
||
await getNutBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Nut Blackjack"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"Schlag den Dealer ohne über **21** zu kommen.",
|
||
"",
|
||
"**Dealer stands on 17.**",
|
||
"**Blackjack pays 3:2.**",
|
||
"**Push returns your wager.**",
|
||
"",
|
||
"Choose your wager."
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields({
|
||
name:
|
||
"Deine Nüsse",
|
||
|
||
value:
|
||
`**${balance.toLocaleString()} 🥜**`
|
||
})
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutBlackjackMenu:${interaction.user.id}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildBetButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* START BLACKJACK
|
||
* ============================================================
|
||
*/
|
||
|
||
async function startNutBlackjack(
|
||
interaction,
|
||
bet
|
||
) {
|
||
if (
|
||
!BLACKJACK_BETS.includes(
|
||
bet
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Dieser Blackjack-Einsatz ist nicht verfügbar."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const ownerId =
|
||
getBlackjackSessionOwner(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
ownerId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht dein Blackjack-Tisch."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Prevent multiple active hands.
|
||
*/
|
||
|
||
const existing =
|
||
await getActiveBlackjack(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (existing) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Du hast bereits eine aktive Blackjack-Hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerId =
|
||
interaction.user.id;
|
||
|
||
|
||
const houseId =
|
||
interaction.client.user.id;
|
||
|
||
|
||
const [
|
||
playerBalance,
|
||
houseBalance
|
||
] =
|
||
await Promise.all([
|
||
getNutBalance(
|
||
playerId
|
||
),
|
||
|
||
getNutBalance(
|
||
houseId
|
||
)
|
||
]);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
bet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`Du brauchst **${bet.toLocaleString()} Nüsse**.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Natural Blackjack can require 2.5x total
|
||
* returned to the player.
|
||
*
|
||
* Since the player's wager moves into the house
|
||
* before resolving, the house needs enough liquidity
|
||
* to cover the extra 1.5x.
|
||
*/
|
||
|
||
if (
|
||
houseBalance <
|
||
Math.ceil(
|
||
bet *
|
||
1.5
|
||
)
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Arthonor's Nut Stash kann diesen Einsatz aktuell nicht absichern."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
try {
|
||
await transferNuts(
|
||
playerId,
|
||
houseId,
|
||
bet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_NUTS"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Dein Nut-Bestand hat sich verändert."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Fresh real 52-card deck.
|
||
*/
|
||
|
||
const deck =
|
||
createDeck();
|
||
|
||
|
||
shuffleDeck(
|
||
deck
|
||
);
|
||
|
||
|
||
const playerCards = [
|
||
deck.pop(),
|
||
deck.pop()
|
||
];
|
||
|
||
|
||
const dealerCards = [
|
||
deck.pop(),
|
||
deck.pop()
|
||
];
|
||
|
||
|
||
const game = {
|
||
sessionId:
|
||
randomUUID(),
|
||
|
||
userId:
|
||
playerId,
|
||
|
||
state:
|
||
"Playing",
|
||
|
||
bet,
|
||
|
||
originalBet:
|
||
bet,
|
||
|
||
doubled:
|
||
false,
|
||
|
||
deck,
|
||
|
||
playerCards,
|
||
|
||
dealerCards,
|
||
|
||
createdAt:
|
||
getUnixTime()
|
||
};
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
await db
|
||
.collection(
|
||
SESSION_COLLECTION
|
||
)
|
||
.insertOne(
|
||
game
|
||
);
|
||
|
||
|
||
/*
|
||
* Natural Blackjack check.
|
||
*/
|
||
|
||
const playerBlackjack =
|
||
isBlackjack(
|
||
playerCards
|
||
);
|
||
|
||
|
||
const dealerBlackjack =
|
||
isBlackjack(
|
||
dealerCards
|
||
);
|
||
|
||
|
||
if (
|
||
playerBlackjack ||
|
||
dealerBlackjack
|
||
) {
|
||
if (
|
||
playerBlackjack &&
|
||
dealerBlackjack
|
||
) {
|
||
return finishNutBlackjack(
|
||
interaction,
|
||
game,
|
||
"Push"
|
||
);
|
||
}
|
||
|
||
|
||
if (playerBlackjack) {
|
||
return finishNutBlackjack(
|
||
interaction,
|
||
game,
|
||
"Blackjack"
|
||
);
|
||
}
|
||
|
||
|
||
return finishNutBlackjack(
|
||
interaction,
|
||
game,
|
||
"DealerBlackjack"
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
game
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
game
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* HIT
|
||
* ============================================================
|
||
*/
|
||
|
||
async function hitNutBlackjack(
|
||
interaction
|
||
) {
|
||
const game =
|
||
await getBlackjackFromInteraction(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
!game ||
|
||
game.state !==
|
||
"Playing"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Diese Blackjack-Hand existiert nicht mehr."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Blackjack-Hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const deck =
|
||
[
|
||
...game.deck
|
||
];
|
||
|
||
|
||
const playerCards =
|
||
[
|
||
...game.playerCards
|
||
];
|
||
|
||
|
||
playerCards.push(
|
||
deck.pop()
|
||
);
|
||
|
||
|
||
const updated = {
|
||
...game,
|
||
deck,
|
||
playerCards
|
||
};
|
||
|
||
|
||
await updateBlackjackSession(
|
||
game.sessionId,
|
||
{
|
||
deck,
|
||
playerCards
|
||
}
|
||
);
|
||
|
||
|
||
const score =
|
||
getHandValue(
|
||
playerCards
|
||
);
|
||
|
||
|
||
if (
|
||
score >
|
||
21
|
||
) {
|
||
return finishNutBlackjack(
|
||
interaction,
|
||
updated,
|
||
"Bust"
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Auto-stand on 21.
|
||
*/
|
||
|
||
if (
|
||
score ===
|
||
21
|
||
) {
|
||
return standNutBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
buildGameEmbed(
|
||
updated
|
||
)
|
||
],
|
||
|
||
components:
|
||
buildGameButtons(
|
||
updated
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* STAND
|
||
* ============================================================
|
||
*/
|
||
|
||
async function standNutBlackjack(
|
||
interaction,
|
||
suppliedGame =
|
||
null
|
||
) {
|
||
const game =
|
||
suppliedGame
|
||
??
|
||
await getBlackjackFromInteraction(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (!game) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Diese Blackjack-Hand existiert nicht mehr."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Blackjack-Hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const deck =
|
||
[
|
||
...game.deck
|
||
];
|
||
|
||
|
||
const dealerCards =
|
||
[
|
||
...game.dealerCards
|
||
];
|
||
|
||
|
||
/*
|
||
* Dealer stands on all 17s.
|
||
*/
|
||
|
||
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 finishNutBlackjack(
|
||
interaction,
|
||
updated,
|
||
result
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DOUBLE
|
||
* ============================================================
|
||
*/
|
||
|
||
async function doubleNutBlackjack(
|
||
interaction
|
||
) {
|
||
const game =
|
||
await getBlackjackFromInteraction(
|
||
interaction
|
||
);
|
||
|
||
|
||
if (
|
||
!game ||
|
||
game.state !==
|
||
"Playing"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Diese Blackjack-Hand existiert nicht mehr."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
if (
|
||
game.userId !==
|
||
interaction.user.id
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Das ist nicht deine Blackjack-Hand."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Double is only possible on the original hand.
|
||
*/
|
||
|
||
if (
|
||
game.playerCards.length !==
|
||
2
|
||
||
|
||
game.doubled
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Double ist nur mit den ersten zwei Karten möglich."
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
const playerBalance =
|
||
await getNutBalance(
|
||
interaction.user.id
|
||
);
|
||
|
||
|
||
if (
|
||
playerBalance <
|
||
game.originalBet
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
`Du brauchst weitere **${game.originalBet.toLocaleString()} Nüsse** für Double.`
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
try {
|
||
await transferNuts(
|
||
interaction.user.id,
|
||
interaction.client.user.id,
|
||
game.originalBet
|
||
);
|
||
|
||
} catch (error) {
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_NUTS"
|
||
) {
|
||
return temporaryEphemeralReply(
|
||
interaction,
|
||
{
|
||
content:
|
||
"Du hast nicht mehr genug Nüsse für 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
|
||
};
|
||
|
||
|
||
await updateBlackjackSession(
|
||
game.sessionId,
|
||
{
|
||
deck,
|
||
playerCards,
|
||
|
||
bet:
|
||
updated.bet,
|
||
|
||
doubled:
|
||
true
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
getHandValue(
|
||
playerCards
|
||
)
|
||
>
|
||
21
|
||
) {
|
||
return finishNutBlackjack(
|
||
interaction,
|
||
updated,
|
||
"Bust"
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* Double automatically stands.
|
||
*/
|
||
|
||
return standNutBlackjack(
|
||
interaction,
|
||
updated
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* FINISH BLACKJACK
|
||
* ============================================================
|
||
*/
|
||
|
||
async function finishNutBlackjack(
|
||
interaction,
|
||
game,
|
||
result
|
||
) {
|
||
let payout =
|
||
0;
|
||
|
||
|
||
switch (result) {
|
||
/*
|
||
* Normal win:
|
||
*
|
||
* return original wager + winnings.
|
||
*/
|
||
|
||
case "Win":
|
||
case "DealerBust":
|
||
payout =
|
||
game.bet *
|
||
2;
|
||
break;
|
||
|
||
|
||
/*
|
||
* Push:
|
||
*
|
||
* wager returned.
|
||
*/
|
||
|
||
case "Push":
|
||
payout =
|
||
game.bet;
|
||
break;
|
||
|
||
|
||
/*
|
||
* Natural Blackjack:
|
||
*
|
||
* wager + 3:2 winnings
|
||
* = 2.5x returned.
|
||
*/
|
||
|
||
case "Blackjack":
|
||
payout =
|
||
Math.floor(
|
||
game.bet *
|
||
2.5
|
||
);
|
||
break;
|
||
|
||
|
||
/*
|
||
* Bust / Loss / Dealer Blackjack:
|
||
* house keeps wager.
|
||
*/
|
||
|
||
default:
|
||
payout =
|
||
0;
|
||
}
|
||
|
||
|
||
if (
|
||
payout >
|
||
0
|
||
) {
|
||
try {
|
||
await transferNuts(
|
||
interaction.client.user.id,
|
||
game.userId,
|
||
payout
|
||
);
|
||
|
||
} catch (error) {
|
||
console.error(
|
||
"[Nut Blackjack] Payout failed:",
|
||
error
|
||
);
|
||
|
||
|
||
const errorEmbed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Blackjack Error"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"The hand resolved, but Arthonor's Nut Stash could not cover the payout.",
|
||
"",
|
||
"Please contact an administrator."
|
||
].join("\n")
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutBlackjackMenu:${game.userId}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
errorEmbed
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* sessions_* represents currently active state.
|
||
*
|
||
* Finished hand = remove session.
|
||
*/
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
await db
|
||
.collection(
|
||
SESSION_COLLECTION
|
||
)
|
||
.deleteOne({
|
||
sessionId:
|
||
game.sessionId
|
||
});
|
||
|
||
|
||
const balance =
|
||
await getNutBalance(
|
||
game.userId
|
||
);
|
||
|
||
|
||
const profit =
|
||
payout -
|
||
game.bet;
|
||
|
||
|
||
const embed =
|
||
new EmbedBuilder()
|
||
.setTitle(
|
||
getResultTitle(
|
||
result
|
||
)
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"**Dealer**",
|
||
formatCards(
|
||
game.dealerCards
|
||
),
|
||
`Score: **${getHandValue(game.dealerCards)}**`,
|
||
"",
|
||
"**Du**",
|
||
formatCards(
|
||
game.playerCards
|
||
),
|
||
`Score: **${getHandValue(game.playerCards)}**`
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Einsatz",
|
||
|
||
value:
|
||
`${game.bet.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Payout",
|
||
|
||
value:
|
||
`${payout.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Profit",
|
||
|
||
value:
|
||
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Deine Nüsse",
|
||
|
||
value:
|
||
`${balance.toLocaleString()} 🥜`
|
||
}
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutBlackjackMenu:${game.userId}`
|
||
});
|
||
|
||
|
||
return interaction.update({
|
||
embeds: [
|
||
embed
|
||
],
|
||
|
||
components:
|
||
buildResultButtons()
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* ACTIVE GAME EMBED
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildGameEmbed(
|
||
game
|
||
) {
|
||
const playerScore =
|
||
getHandValue(
|
||
game.playerCards
|
||
);
|
||
|
||
|
||
return new EmbedBuilder()
|
||
.setTitle(
|
||
"🃏 Nut Blackjack"
|
||
)
|
||
|
||
.setDescription(
|
||
[
|
||
"**Dealer**",
|
||
`${formatCard(game.dealerCards[0])} 🂠`,
|
||
"",
|
||
"**Du**",
|
||
formatCards(
|
||
game.playerCards
|
||
),
|
||
`Score: **${playerScore}**`
|
||
].join("\n")
|
||
)
|
||
|
||
.addFields(
|
||
{
|
||
name:
|
||
"Einsatz",
|
||
|
||
value:
|
||
`${game.bet.toLocaleString()} 🥜`,
|
||
|
||
inline:
|
||
true
|
||
},
|
||
|
||
{
|
||
name:
|
||
"Status",
|
||
|
||
value:
|
||
"**Dein Zug**",
|
||
|
||
inline:
|
||
true
|
||
}
|
||
)
|
||
|
||
.setFooter({
|
||
text:
|
||
`nutBlackjack:${game.sessionId}:${game.userId}`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* BET BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildBetButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
...BLACKJACK_BETS.map(
|
||
bet =>
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
`nutBlackjackBet${bet}`
|
||
)
|
||
.setLabel(
|
||
bet.toLocaleString()
|
||
)
|
||
.setEmoji(
|
||
"🥜"
|
||
)
|
||
.setStyle(
|
||
bet >=
|
||
500
|
||
? ButtonStyle.Primary
|
||
: ButtonStyle.Secondary
|
||
)
|
||
)
|
||
),
|
||
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubBack"
|
||
)
|
||
.setLabel(
|
||
"Nut Hub"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* GAME BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildGameButtons(
|
||
game
|
||
) {
|
||
const canDouble =
|
||
game.playerCards.length ===
|
||
2
|
||
&&
|
||
!game.doubled;
|
||
|
||
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutBlackjackHit"
|
||
)
|
||
.setLabel(
|
||
"Hit"
|
||
)
|
||
.setEmoji(
|
||
"➕"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutBlackjackStand"
|
||
)
|
||
.setLabel(
|
||
"Stand"
|
||
)
|
||
.setEmoji(
|
||
"✋"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Success
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutBlackjackDouble"
|
||
)
|
||
.setLabel(
|
||
"Double"
|
||
)
|
||
.setEmoji(
|
||
"💰"
|
||
)
|
||
.setDisabled(
|
||
!canDouble
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Danger
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RESULT BUTTONS
|
||
* ============================================================
|
||
*/
|
||
|
||
function buildResultButtons() {
|
||
return [
|
||
new ActionRowBuilder()
|
||
.addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubBlackjack"
|
||
)
|
||
.setLabel(
|
||
"Play Again"
|
||
)
|
||
.setEmoji(
|
||
"🃏"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Primary
|
||
),
|
||
|
||
new ButtonBuilder()
|
||
.setCustomId(
|
||
"nutHubBack"
|
||
)
|
||
.setLabel(
|
||
"Nut Hub"
|
||
)
|
||
.setEmoji(
|
||
"◀️"
|
||
)
|
||
.setStyle(
|
||
ButtonStyle.Secondary
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* DECK
|
||
* ============================================================
|
||
*/
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
/*
|
||
* Fisher-Yates shuffle.
|
||
*/
|
||
|
||
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]
|
||
];
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* HAND VALUE
|
||
* ============================================================
|
||
*/
|
||
|
||
function getHandValue(
|
||
cards
|
||
) {
|
||
let total =
|
||
0;
|
||
|
||
|
||
let aces =
|
||
0;
|
||
|
||
|
||
for (
|
||
const card
|
||
of cards
|
||
) {
|
||
if (
|
||
card.rank ===
|
||
"A"
|
||
) {
|
||
total +=
|
||
11;
|
||
|
||
|
||
aces++;
|
||
}
|
||
|
||
|
||
else if (
|
||
[
|
||
"J",
|
||
"Q",
|
||
"K"
|
||
].includes(
|
||
card.rank
|
||
)
|
||
) {
|
||
total +=
|
||
10;
|
||
}
|
||
|
||
|
||
else {
|
||
total +=
|
||
Number(
|
||
card.rank
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* Convert Aces from 11 → 1 as required.
|
||
*/
|
||
|
||
while (
|
||
total >
|
||
21
|
||
&&
|
||
aces >
|
||
0
|
||
) {
|
||
total -=
|
||
10;
|
||
|
||
|
||
aces--;
|
||
}
|
||
|
||
|
||
return total;
|
||
}
|
||
|
||
|
||
function isBlackjack(
|
||
cards
|
||
) {
|
||
return (
|
||
cards.length ===
|
||
2
|
||
&&
|
||
getHandValue(
|
||
cards
|
||
) ===
|
||
21
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* CARD DISPLAY
|
||
* ============================================================
|
||
*/
|
||
|
||
function formatCard(
|
||
card
|
||
) {
|
||
return `**${card.rank}${card.suit}**`;
|
||
}
|
||
|
||
|
||
function formatCards(
|
||
cards
|
||
) {
|
||
return cards
|
||
.map(
|
||
formatCard
|
||
)
|
||
.join(
|
||
" "
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* ACTIVE SESSION
|
||
* ============================================================
|
||
*/
|
||
|
||
async function getActiveBlackjack(
|
||
userId
|
||
) {
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
return db
|
||
.collection(
|
||
SESSION_COLLECTION
|
||
)
|
||
.findOne({
|
||
userId,
|
||
|
||
state:
|
||
"Playing"
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* GET SESSION FROM MESSAGE
|
||
* ============================================================
|
||
*/
|
||
|
||
async function getBlackjackFromInteraction(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
if (
|
||
parts[0] !==
|
||
"nutBlackjack"
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
|
||
const sessionId =
|
||
parts[1];
|
||
|
||
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
return db
|
||
.collection(
|
||
SESSION_COLLECTION
|
||
)
|
||
.findOne({
|
||
sessionId,
|
||
|
||
state:
|
||
"Playing"
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* UPDATE SESSION
|
||
* ============================================================
|
||
*/
|
||
|
||
async function updateBlackjackSession(
|
||
sessionId,
|
||
fields
|
||
) {
|
||
const {
|
||
db
|
||
} =
|
||
getCollections();
|
||
|
||
|
||
return db
|
||
.collection(
|
||
SESSION_COLLECTION
|
||
)
|
||
.updateOne(
|
||
{
|
||
sessionId,
|
||
|
||
state:
|
||
"Playing"
|
||
},
|
||
{
|
||
$set:
|
||
fields
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* SESSION OWNERSHIP
|
||
* ============================================================
|
||
*/
|
||
|
||
function getBlackjackSessionOwner(
|
||
interaction
|
||
) {
|
||
const footer =
|
||
interaction.message
|
||
?.embeds?.[0]
|
||
?.footer
|
||
?.text;
|
||
|
||
|
||
const parts =
|
||
footer?.split(":")
|
||
??
|
||
[];
|
||
|
||
|
||
/*
|
||
* Nut Hub:
|
||
*
|
||
* nutHub:<userId>
|
||
*/
|
||
|
||
if (
|
||
parts[0] ===
|
||
"nutHub"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
/*
|
||
* Blackjack menu/result:
|
||
*
|
||
* nutBlackjackMenu:<userId>
|
||
*/
|
||
|
||
if (
|
||
parts[0] ===
|
||
"nutBlackjackMenu"
|
||
) {
|
||
return parts[1];
|
||
}
|
||
|
||
|
||
/*
|
||
* Active Blackjack:
|
||
*
|
||
* nutBlackjack:<sessionId>:<userId>
|
||
*/
|
||
|
||
if (
|
||
parts[0] ===
|
||
"nutBlackjack"
|
||
) {
|
||
return parts[2];
|
||
}
|
||
|
||
|
||
return null;
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* RESULT TITLES
|
||
* ============================================================
|
||
*/
|
||
|
||
function getResultTitle(
|
||
result
|
||
) {
|
||
switch (
|
||
result
|
||
) {
|
||
case "Blackjack":
|
||
return "🃏 BLACKJACK!";
|
||
|
||
|
||
case "Win":
|
||
return "🎉 Du gewinnst!";
|
||
|
||
|
||
case "DealerBust":
|
||
return "💥 Dealer Bust!";
|
||
|
||
|
||
case "Push":
|
||
return "🤝 Push";
|
||
|
||
|
||
case "Bust":
|
||
return "💥 Bust!";
|
||
|
||
|
||
case "DealerBlackjack":
|
||
return "🃏 Dealer Blackjack";
|
||
|
||
|
||
default:
|
||
return "🃏 Dealer gewinnt";
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ============================================================
|
||
* EXPORTS
|
||
* ============================================================
|
||
*/
|
||
|
||
module.exports = {
|
||
showNutBlackjack,
|
||
startNutBlackjack,
|
||
hitNutBlackjack,
|
||
standNutBlackjack,
|
||
doubleNutBlackjack
|
||
}; |