snapshot_poker_phase3
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
+196
-56
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user