Files
ShaiBot/utils/crumbRoulette.js
T
2026-08-18 21:03:07 +02:00

1128 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js");
const {
getCookieBalance,
transferCookies,
temporaryEphemeralReply
} = require("./cookieEconomy");
/*
* ============================================================
* CONFIG
* ============================================================
*/
const ROULETTE_BETS = [
10,
50,
100,
500,
1000
];
const ROULETTE_CHOICES = {
Red: {
name:
"Red",
emoji:
"🔴",
multiplier:
2
},
Black: {
name:
"Black",
emoji:
"⚫",
multiplier:
2
},
Odd: {
name:
"Odd",
emoji:
"🔢",
multiplier:
2
},
Even: {
name:
"Even",
emoji:
"2️⃣",
multiplier:
2
},
Zero: {
name:
"Zero",
emoji:
"🟢",
multiplier:
36
}
};
/*
* European Roulette red numbers.
*/
const RED_NUMBERS =
new Set([
1,
3,
5,
7,
9,
12,
14,
16,
18,
19,
21,
23,
25,
27,
30,
32,
34,
36
]);
/*
* ============================================================
* SHOW ROULETTE
* ============================================================
*/
async function showRoulette(
interaction
) {
const ownerId =
getRouletteSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
const balance =
await getCookieBalance(
interaction.user.id
);
const embed =
new EmbedBuilder()
.setTitle(
"🎡 Crumb Saucer Roulette"
)
.setDescription(
[
"European Roulette — **0 to 36**.",
"",
"Choose your wager.",
"",
"🔴 **Red** — ×2",
"⚫ **Black** — ×2",
"🔢 **Odd** — ×2",
"2️⃣ **Even** — ×2",
"🟢 **Zero** — ×36"
].join("\n")
)
.addFields({
name:
"Your Vault",
value:
`**${balance.toLocaleString()} 🍪**`
})
.setFooter({
text:
`crumbRouletteMenu:${interaction.user.id}`
});
return interaction.update({
embeds: [
embed
],
components:
buildBetButtons()
});
}
/*
* ============================================================
* CHOOSE WAGER
* ============================================================
*/
async function chooseRouletteBet(
interaction,
bet
) {
if (
!ROULETTE_BETS.includes(
bet
)
) {
return temporaryEphemeralReply(
interaction,
{
content:
"That Roulette wager isn't available."
}
);
}
const ownerId =
getRouletteSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Roulette table."
}
);
}
const balance =
await getCookieBalance(
interaction.user.id
);
if (
balance <
bet
) {
return temporaryEphemeralReply(
interaction,
{
content:
`You need **${bet.toLocaleString()} Cookies** for that wager.`
}
);
}
const embed =
new EmbedBuilder()
.setTitle(
"🎡 Crumb Saucer Roulette"
)
.setDescription(
[
`Wager: **${bet.toLocaleString()} 🍪**`,
"",
"Where do you want to place your Cookies?",
"",
"🔴 **Red** — ×2",
"⚫ **Black** — ×2",
"🔢 **Odd** — ×2",
"2️⃣ **Even** — ×2",
"🟢 **Zero** — ×36"
].join("\n")
)
.setFooter({
text:
`crumbRouletteBet:${interaction.user.id}:${bet}`
});
return interaction.update({
embeds: [
embed
],
components:
buildChoiceButtons()
});
}
/*
* ============================================================
* PLAY ROULETTE
* ============================================================
*/
async function playRoulette(
interaction,
choiceKey
) {
const choice =
ROULETTE_CHOICES[
choiceKey
];
if (!choice) {
return temporaryEphemeralReply(
interaction,
{
content:
"That Roulette bet doesn't exist."
}
);
}
const footer =
interaction.message
.embeds[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
if (
parts[0] !==
"crumbRouletteBet"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This Roulette wager is no longer valid."
}
);
}
const ownerId =
parts[1];
const bet =
Number(
parts[2]
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Roulette table."
}
);
}
if (
!ROULETTE_BETS.includes(
bet
)
) {
return temporaryEphemeralReply(
interaction,
{
content:
"That Roulette wager is invalid."
}
);
}
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 no longer have enough Cookies for that wager."
}
);
}
/*
* Worst payout is Zero at x36.
*
* Once the player's wager is transferred to the house,
* the house must still be able to cover the remaining
* potential payout.
*/
const maximumPayout =
bet *
choice.multiplier;
if (
houseBalance +
bet <
maximumPayout
) {
return temporaryEphemeralReply(
interaction,
{
content:
"The Crumb Saucer vault can't currently cover that Roulette wager."
}
);
}
/*
* Take wager.
*/
try {
await transferCookies(
playerId,
houseId,
bet
);
} catch (error) {
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return temporaryEphemeralReply(
interaction,
{
content:
"Your balance changed before the wheel could spin."
}
);
}
throw error;
}
/*
* ========================================================
* PSEUDO ANIMATION
* ========================================================
*/
await interaction.update({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"The wheel begins to spin..."
)
],
components: []
});
await sleep(
650
);
await interaction.editReply({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"Round and round..."
)
],
components: []
});
await sleep(
650
);
await interaction.editReply({
embeds: [
buildSpinEmbed(
choice,
bet,
randomRouletteNumber(),
"The ball is slowing down..."
)
],
components: []
});
await sleep(
750
);
/*
* The real result is only generated after
* the animation completes.
*/
const result =
randomRouletteNumber();
const won =
didRouletteBetWin(
choiceKey,
result
);
const payout =
won
? bet *
choice.multiplier
: 0;
if (
payout >
0
) {
try {
await transferCookies(
houseId,
playerId,
payout
);
} catch (error) {
console.error(
"[Roulette] Payout failed:",
error
);
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"🎡 Roulette Error"
)
.setDescription(
"The wheel resolved, but the payout failed. Please contact an administrator."
)
],
components:
buildResultButtons()
});
}
}
const [
playerAfter,
houseAfter
] =
await Promise.all([
getCookieBalance(
playerId
),
getCookieBalance(
houseId
)
]);
const profit =
payout -
bet;
const resultEmbed =
new EmbedBuilder()
.setTitle(
won
? "🎉 Roulette Winner!"
: "🎡 Roulette"
)
.setDescription(
[
"## The ball lands on...",
"",
`# ${formatRouletteNumber(result)}`,
"",
`${choice.emoji} Your bet: **${choice.name}**`,
"",
won
? `🎉 **YOU WIN ${payout.toLocaleString()} 🍪!**`
: "The house takes this one."
].join("\n")
)
.addFields(
{
name:
"Wager",
value:
`${bet.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Payout",
value:
`${payout.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Profit",
value:
`${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"Your Vault",
value:
`${playerAfter.toLocaleString()} 🍪`,
inline:
true
},
{
name:
"House Vault",
value:
`${houseAfter.toLocaleString()} 🍪`,
inline:
true
}
)
.setFooter({
text:
`crumbRouletteResult:${interaction.user.id}`
});
return interaction.editReply({
embeds: [
resultEmbed
],
components:
buildResultButtons()
});
}
/*
* ============================================================
* RESULT LOGIC
* ============================================================
*/
function didRouletteBetWin(
choice,
number
) {
switch (choice) {
case "Red":
return (
number !==
0
&&
RED_NUMBERS.has(
number
)
);
case "Black":
return (
number !==
0
&&
!RED_NUMBERS.has(
number
)
);
case "Odd":
return (
number !==
0
&&
number %
2 ===
1
);
case "Even":
return (
number !==
0
&&
number %
2 ===
0
);
case "Zero":
return (
number ===
0
);
default:
return false;
}
}
/*
* ============================================================
* DISPLAY
* ============================================================
*/
function buildSpinEmbed(
choice,
bet,
number,
status
) {
return new EmbedBuilder()
.setTitle(
"🎡 Crumb Saucer Roulette"
)
.setDescription(
[
"## 🎡 Spinning...",
"",
`### ${formatRouletteNumber(number)}`,
"",
`**${status}**`
].join("\n")
)
.addFields(
{
name:
"Bet",
value:
`${choice.emoji} ${choice.name}`,
inline:
true
},
{
name:
"Wager",
value:
`${bet.toLocaleString()} 🍪`,
inline:
true
}
);
}
function formatRouletteNumber(
number
) {
if (
number ===
0
) {
return "🟢 0";
}
if (
RED_NUMBERS.has(
number
)
) {
return `🔴 ${number}`;
}
return `⚫ ${number}`;
}
/*
* ============================================================
* BUTTON BUILDERS
* ============================================================
*/
function buildBetButtons() {
return [
new ActionRowBuilder()
.addComponents(
...ROULETTE_BETS.map(
bet =>
new ButtonBuilder()
.setCustomId(
`crumbRouletteBet${bet}`
)
.setLabel(
bet.toLocaleString()
)
.setEmoji(
"🍪"
)
.setStyle(
bet >=
500
? ButtonStyle.Primary
: ButtonStyle.Secondary
)
)
),
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Back to Crumb Saucer"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
function buildChoiceButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbRouletteRed"
)
.setLabel(
"Red"
)
.setEmoji(
"🔴"
)
.setStyle(
ButtonStyle.Danger
),
new ButtonBuilder()
.setCustomId(
"crumbRouletteBlack"
)
.setLabel(
"Black"
)
.setEmoji(
"⚫"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"crumbRouletteOdd"
)
.setLabel(
"Odd"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbRouletteEven"
)
.setLabel(
"Even"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbRouletteZero"
)
.setLabel(
"Zero"
)
.setEmoji(
"🟢"
)
.setStyle(
ButtonStyle.Success
)
),
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerRoulette"
)
.setLabel(
"Change Wager"
)
.setEmoji(
"↩️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
function buildResultButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerRoulette"
)
.setLabel(
"Play Again"
)
.setEmoji(
"🎡"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Crumb Saucer"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
/*
* ============================================================
* SESSION OWNER
* ============================================================
*/
function getRouletteSessionOwner(
interaction
) {
const footer =
interaction.message
.embeds[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
/*
* Main Saucer
*/
if (
parts[0] ===
"crumbSaucer"
) {
return parts[1];
}
/*
* Roulette wager menu
*/
if (
parts[0] ===
"crumbRouletteMenu"
) {
return parts[1];
}
/*
* Roulette result
*/
if (
parts[0] ===
"crumbRouletteResult"
) {
return parts[1];
}
return null;
}
/*
* ============================================================
* RANDOM
* ============================================================
*/
function randomRouletteNumber() {
return Math.floor(
Math.random() *
37
);
}
function sleep(
ms
) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
module.exports = {
showRoulette,
chooseRouletteBet,
playRoulette
};