snapshot_poker_phase3

This commit is contained in:
DeSqBlocki
2026-09-08 19:37:02 +02:00
parent 995a2317b9
commit 903a2f3081
25 changed files with 7457 additions and 60 deletions
+247
View File
@@ -268,6 +268,251 @@ async function changeCookies(
}
/*
* ============================================================
* IDEMPOTENT COOKIE CHANGE
* ============================================================
*
* Applies a Cookie mutation at most once for a user-scoped
* operationId. This is useful for persistent game buy-ins and
* cash-outs that may be retried after a crash or restart.
*
* The Cookie balance mutation and operation marker are written
* atomically to the same user document.
*/
async function changeCookiesOnce(
userId,
amount,
operationId
) {
if (
!Number.isSafeInteger(
amount
)
) {
throw new Error(
"INVALID_COOKIE_AMOUNT"
);
}
if (
typeof operationId !==
"string"
||
!/^[A-Za-z0-9:_-]{1,160}$/.test(
operationId
)
) {
throw new Error(
"INVALID_COOKIE_OPERATION_ID"
);
}
if (
amount ===
0
) {
return {
applied:
false,
balance:
await getCookieBalance(
userId
)
};
}
const {
cookies
} =
getCollections();
const filter = {
userId,
appliedCookieOperations: {
$ne:
operationId
}
};
if (
amount <
0
) {
filter.cookies = {
$gte:
Math.abs(
amount
)
};
}
const update = {
$inc: {
cookies:
amount
},
$addToSet: {
appliedCookieOperations:
operationId
}
};
if (
amount >
0
) {
update.$setOnInsert = {
userId
};
}
try {
const result =
await cookies.updateOne(
filter,
update,
{
upsert:
amount > 0
}
);
if (
result.modifiedCount ===
1
||
result.upsertedCount ===
1
) {
return {
applied:
true,
balance:
await getCookieBalance(
userId
)
};
}
}
catch (error) {
/*
* A concurrent positive retry can race an upsert.
* The unique userId index may reject the losing insert.
* Re-read below and treat an already-recorded operation
* as a successful idempotent retry.
*/
if (
error?.code !==
11000
) {
throw error;
}
}
const document =
await cookies.findOne(
{
userId
},
{
projection: {
cookies:
1,
appliedCookieOperations:
1
}
}
);
const operations =
Array.isArray(
document
?.appliedCookieOperations
)
? document
.appliedCookieOperations
: [];
if (
operations.includes(
operationId
)
) {
return {
applied:
false,
balance:
Math.max(
0,
Number(
document?.cookies ??
0
)
)
};
}
if (
amount <
0
) {
throw new Error(
"INSUFFICIENT_COOKIES"
);
}
throw new Error(
"COOKIE_OPERATION_FAILED"
);
}
async function hasCookieOperation(
userId,
operationId
) {
if (
typeof operationId !==
"string"
||
!/^[A-Za-z0-9:_-]{1,160}$/.test(
operationId
)
) {
throw new Error(
"INVALID_COOKIE_OPERATION_ID"
);
}
const {
cookies
} =
getCollections();
const document =
await cookies.findOne(
{
userId,
appliedCookieOperations:
operationId
},
{
projection: {
_id:
1
}
}
);
return Boolean(
document
);
}
/*
* ============================================================
* COOKIE TRANSFER
@@ -1188,6 +1433,8 @@ module.exports = {
getCookieBalance,
changeCookies,
changeCookiesOnce,
hasCookieOperation,
transferCookies,
getCooldown,
+2730
View File
File diff suppressed because it is too large Load Diff
+196 -56
View File
@@ -14,48 +14,88 @@ const {
requireInteractionSession
} = require("./interactionSession");
async function showCrumbSaucer(interaction) {
if (interaction.isButton()) {
const session = await requireInteractionSession(interaction);
async function showCrumbSaucer(
interaction
) {
if (
interaction.isButton()
) {
const session =
await requireInteractionSession(
interaction
);
if (!session) {
return;
}
const balance = await getCookieBalance(interaction.user.id);
const embed = buildMainMenuEmbed(interaction.user, balance);
const balance =
await getCookieBalance(
interaction.user.id
);
const embed =
buildMainMenuEmbed(
interaction.user,
balance
);
return interaction.update({
embeds: [embed],
components: buildMainMenuButtons()
embeds: [
embed
],
components:
buildMainMenuButtons()
});
}
const balance = await getCookieBalance(interaction.user.id);
const embed = buildMainMenuEmbed(interaction.user, balance);
const balance =
await getCookieBalance(
interaction.user.id
);
const embed =
buildMainMenuEmbed(
interaction.user,
balance
);
await interaction.reply({
embeds: [embed],
components: buildMainMenuButtons()
embeds: [
embed
],
components:
buildMainMenuButtons()
});
const message = await interaction.fetchReply();
const message =
await interaction.fetchReply();
await createInteractionSession(
interaction,
{
messageId: message.id,
ownerId: interaction.user.id,
kind: "crumbSaucer"
messageId:
message.id,
ownerId:
interaction.user.id,
kind:
"crumbSaucer"
}
);
return message;
}
function buildMainMenuEmbed(user, balance) {
function buildMainMenuEmbed(
user,
balance
) {
return new EmbedBuilder()
.setTitle("🎰 The Crumb Saucer")
.setTitle(
"🎰 The Crumb Saucer"
)
.setDescription(
[
`Welcome, ${user}!`,
@@ -78,79 +118,178 @@ function buildMainMenuEmbed(user, balance) {
"### 🏦 Vault Heist",
"Attempt to breach ShaiBot's Cookie Vault.",
"",
"### ♠️ Poker",
"Create or join a 2–6 player Cookie Poker table.",
"",
"### 🛟 Bankruptcy",
"At exactly 0 Cookies, claim a 1,000 Cookie starter kit."
].join("\n")
)
.addFields({
name: "Your Vault",
value: `**${balance.toLocaleString()} 🍪**`
name:
"Your Vault",
value:
`**${balance.toLocaleString()} 🍪**`
})
.setFooter({
text: `crumbSaucer:${user.id}`
text:
`crumbSaucer:${user.id}`
});
}
function buildMainMenuButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId("crumbSaucerSlots")
.setLabel("Slots")
.setEmoji("🎰")
.setStyle(ButtonStyle.Primary),
.setCustomId(
"crumbSaucerSlots"
)
.setLabel(
"Slots"
)
.setEmoji(
"🎰"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId("crumbSaucerBlackjack")
.setLabel("Blackjack")
.setEmoji("🃏")
.setStyle(ButtonStyle.Primary),
.setCustomId(
"crumbSaucerBlackjack"
)
.setLabel(
"Blackjack"
)
.setEmoji(
"🃏"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId("crumbSaucerRoulette")
.setLabel("Roulette")
.setEmoji("🎡")
.setStyle(ButtonStyle.Primary),
.setCustomId(
"crumbSaucerRoulette"
)
.setLabel(
"Roulette"
)
.setEmoji(
"🎡"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId("crumbSaucerHeist")
.setLabel("Vault Heist")
.setEmoji("🏦")
.setStyle(ButtonStyle.Danger)
.setCustomId(
"crumbSaucerHeist"
)
.setLabel(
"Vault Heist"
)
.setEmoji(
"🏦"
)
.setStyle(
ButtonStyle.Danger
),
new ButtonBuilder()
.setCustomId(
"crumbSaucerPoker"
)
.setLabel(
"Poker"
)
.setEmoji(
"♠️"
)
.setStyle(
ButtonStyle.Success
)
),
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId("crumbSaucerCollectMenu")
.setLabel("Cookie Bakery")
.setEmoji("🍪")
.setStyle(ButtonStyle.Secondary),
.setCustomId(
"crumbSaucerCollectMenu"
)
.setLabel(
"Cookie Bakery"
)
.setEmoji(
"🍪"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId("crumbSaucerBankruptcy")
.setLabel("Bankruptcy")
.setEmoji("🛟")
.setStyle(ButtonStyle.Secondary),
.setCustomId(
"crumbSaucerBankruptcy"
)
.setLabel(
"Bankruptcy"
)
.setEmoji(
"🛟"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId("crumbSaucerProfile")
.setLabel("Profile")
.setEmoji("👤")
.setStyle(ButtonStyle.Secondary),
.setCustomId(
"crumbSaucerProfile"
)
.setLabel(
"Profile"
)
.setEmoji(
"👤"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId("sessionClose")
.setLabel("Close")
.setEmoji("✖️")
.setStyle(ButtonStyle.Danger)
.setCustomId(
"sessionClose"
)
.setLabel(
"Close"
)
.setEmoji(
"✖️"
)
.setStyle(
ButtonStyle.Danger
)
)
];
}
function getCrumbSaucerSessionOwner(interaction) {
const footer = interaction.message?.embeds?.[0]?.footer?.text;
const parts = footer?.split(":") ?? [];
switch (parts[0]) {
function getCrumbSaucerSessionOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":") ?? [];
switch (
parts[0]
) {
case "crumbSaucer":
case "crumbSaucerCollect":
case "crumbSaucerSlots":
@@ -171,6 +310,7 @@ function getCrumbSaucerSessionOwner(interaction) {
}
}
module.exports = {
showCrumbSaucer,
buildMainMenuEmbed,
File diff suppressed because it is too large Load Diff
+2149
View File
File diff suppressed because it is too large Load Diff
+168
View File
@@ -0,0 +1,168 @@
const {
getPokerService
} = require("./crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage
} = require("./pokerDiscord");
async function executePokerAction(
interaction,
action,
successText
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker control is no longer attached to an active table.",
embeds: [],
components: []
});
}
try {
const service =
getPokerService();
const game =
await service.act({
gameId,
userId:
interaction.user.id,
action
});
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
typeof successText ===
"function"
? successText(game)
: successText,
embeds: [],
components: []
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
async function executeMinimumRaise(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker control is no longer attached to an active table.",
embeds: [],
components: []
});
}
try {
const service =
getPokerService();
const legal =
await service.getLegalPlayerActions({
gameId,
userId:
interaction.user.id
});
const type =
legal.actions?.includes(
"bet"
)
? "bet"
: legal.actions?.includes(
"raise"
)
? "raise"
: null;
if (
!type
||
!Number.isSafeInteger(
legal.minRaiseTo
)
) {
return interaction.editReply({
content:
"❌ A bet or raise is not available right now.",
embeds: [],
components: []
});
}
const game =
await service.act({
gameId,
userId:
interaction.user.id,
action: {
type,
amount:
legal.minRaiseTo
}
});
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
`✅ ${type === "bet" ? "Bet" : "Raised to"} ${legal.minRaiseTo.toLocaleString()} 🍪`,
embeds: [],
components: []
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
module.exports = {
executePokerAction,
executeMinimumRaise
};