mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-01-18 16:37:40 +01:00
File diff suppressed because one or more lines are too long
@@ -7,7 +7,8 @@ import {getUser, insertLog, updateUserCoins} from "../database/index.js";
|
||||
import {client} from "../bot/client.js";
|
||||
import {EmbedBuilder} from "discord.js";
|
||||
|
||||
export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"];
|
||||
// export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"];
|
||||
export const RANKS = ["A", "2"];
|
||||
export const SUITS = ["d","s","c","h"];
|
||||
|
||||
// Build a single 52-card deck like "Ad","Ts", etc.
|
||||
@@ -123,6 +124,7 @@ export function publicPlayerView(player) {
|
||||
result: h.result ?? null,
|
||||
total: handValue(h.cards).total,
|
||||
soft: handValue(h.cards).soft,
|
||||
bet: h.bet,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -139,7 +141,7 @@ export function createBlackjackRoom({
|
||||
phaseDurations = {
|
||||
bettingMs: 15000,
|
||||
dealMs: 1000,
|
||||
playMsPerPlayer: 15000,
|
||||
playMsPerPlayer: 20000,
|
||||
revealMs: 1000,
|
||||
payoutMs: 10000,
|
||||
},
|
||||
@@ -178,7 +180,7 @@ export function resetForNewRound(room) {
|
||||
for (const p of Object.values(room.players)) {
|
||||
p.inRound = false;
|
||||
p.currentBet = 0;
|
||||
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false } ];
|
||||
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 } ];
|
||||
p.activeHand = 0;
|
||||
}
|
||||
}
|
||||
@@ -200,7 +202,7 @@ export function dealInitial(room) {
|
||||
const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet);
|
||||
for (const p of actives) {
|
||||
p.inRound = true;
|
||||
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false } ];
|
||||
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: p.currentBet } ];
|
||||
}
|
||||
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
|
||||
room.dealer.holeHidden = true;
|
||||
@@ -225,8 +227,7 @@ export function autoActions(room) {
|
||||
export function everyoneDone(room) {
|
||||
return Object.values(room.players).every(p => {
|
||||
if (!p.inRound) return true;
|
||||
const h = p.hands[p.activeHand];
|
||||
return h.stood || h.busted || isBlackjack(h.cards) || h.surrendered;
|
||||
return p.hands.filter(h => !h.stood && !h.busted && !h.surrendered)?.length === 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,58 +244,70 @@ export async function settleAll(room) {
|
||||
const allRes = {}
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const hand = p.hands[p.activeHand];
|
||||
const res = settleHand({
|
||||
bet: p.currentBet,
|
||||
playerCards: hand.cards,
|
||||
dealerCards: room.dealer.cards,
|
||||
doubled: hand.doubled,
|
||||
surrendered: hand.surrendered,
|
||||
blackjackPayout: room.settings.blackjackPayout,
|
||||
});
|
||||
allRes[p.id] = res;
|
||||
p.totalDelta += res.delta
|
||||
if (res.result === 'win' || res.result === 'push') {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
const coins = userDB.coins;
|
||||
try {
|
||||
updateUserCoins.run({ id: p.id, coins: coins + p.currentBet + res.delta });
|
||||
insertLog.run({
|
||||
id: `${p.id}-blackjack-${Date.now()}`,
|
||||
user_id: p.id, target_user_id: null,
|
||||
action: 'BLACKJACK_PAYOUT',
|
||||
coins_amount: res.delta + p.currentBet, user_new_amount: coins + p.currentBet + res.delta,
|
||||
});
|
||||
p.bank = coins + p.currentBet + res.delta
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
for (const hand of p.hands) {
|
||||
const res = settleHand({
|
||||
bet: hand.bet,
|
||||
playerCards: hand.cards,
|
||||
dealerCards: room.dealer.cards,
|
||||
doubled: hand.doubled,
|
||||
surrendered: hand.surrendered,
|
||||
blackjackPayout: room.settings.blackjackPayout,
|
||||
});
|
||||
if (allRes[p.id]) {
|
||||
allRes[p.id].push(res);
|
||||
} else {
|
||||
allRes[p.id] = [res];
|
||||
}
|
||||
|
||||
p.totalDelta += res.delta
|
||||
p.totalBets++
|
||||
if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
const coins = userDB.coins;
|
||||
try {
|
||||
updateUserCoins.run({ id: p.id, coins: coins + hand.bet + res.delta });
|
||||
insertLog.run({
|
||||
id: `${p.id}-blackjack-${Date.now()}`,
|
||||
user_id: p.id, target_user_id: null,
|
||||
action: 'BLACKJACK_PAYOUT',
|
||||
coins_amount: res.delta + hand.bet, user_new_amount: coins + hand.bet + res.delta,
|
||||
});
|
||||
p.bank = coins + hand.bet + res.delta
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emitToast({ type: `payout-res`, allRes });
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
)
|
||||
.setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
emitToast({ type: `payout-res`, allRes });
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${p.totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,25 +329,56 @@ export function applyAction(room, playerId, action) {
|
||||
case "stand": {
|
||||
hand.stood = true;
|
||||
hand.hasActed = true;
|
||||
p.activeHand++;
|
||||
return "stand";
|
||||
}
|
||||
case "double": {
|
||||
if (!canDouble(hand)) throw new Error("Cannot double now");
|
||||
hand.doubled = true;
|
||||
p.currentBet*=2
|
||||
hand.bet*=2
|
||||
p.currentBet+=hand.bet/2
|
||||
hand.hasActed = true;
|
||||
// The caller (routes) must also handle additional balance lock on the bet if using real coins
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
else hand.stood = true;
|
||||
p.activeHand++;
|
||||
return "double";
|
||||
}
|
||||
case "surrender": {
|
||||
if (hand.cards.length !== 2 || hand.hasActed) throw new Error("Cannot surrender now");
|
||||
hand.surrendered = true;
|
||||
hand.stood = true;
|
||||
hand.hasActed = true;
|
||||
return "surrender";
|
||||
case "split": {
|
||||
if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards");
|
||||
const r0 = hand.cards[0][0];
|
||||
const r1 = hand.cards[1][0];
|
||||
if (r0 !== r1) throw new Error("Cannot split: cards not same rank");
|
||||
|
||||
const cardA = hand.cards[0];
|
||||
const cardB = hand.cards[1];
|
||||
|
||||
hand.cards = [cardA];
|
||||
hand.stood = false;
|
||||
hand.busted = false;
|
||||
hand.doubled = false;
|
||||
hand.surrendered = false;
|
||||
hand.hasActed = false;
|
||||
|
||||
const newHand = {
|
||||
cards: [cardB],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
bet: hand.bet,
|
||||
}
|
||||
|
||||
p.currentBet *= 2
|
||||
|
||||
p.hands.splice(p.activeHand + 1, 0, newHand);
|
||||
|
||||
hand.cards.push(draw(room.shoe));
|
||||
newHand.cards.push(draw(room.shoe));
|
||||
|
||||
return "split";
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid action");
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// --- Constants for Deck Creation ---
|
||||
import {sleep} from "openai/core";
|
||||
import {emitSolitaireUpdate, emitUpdate} from "../server/socket.js";
|
||||
|
||||
const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs
|
||||
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
|
||||
|
||||
@@ -296,6 +299,72 @@ export function checkWinCondition(gameState) {
|
||||
return foundationCardCount === 52;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the game can be automatically solved (all tableau cards are face-up).
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
* @returns {boolean} True if the game can be auto-solved.
|
||||
*/
|
||||
export function checkAutoSolve(gameState) {
|
||||
if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false;
|
||||
for (const pile of gameState.tableauPiles) {
|
||||
for (const card of pile) {
|
||||
if (!card.faceUp) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function autoSolveMoves(userId, gameState) {
|
||||
const moves = [];
|
||||
const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles));
|
||||
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
|
||||
|
||||
function canMoveToFoundation(card) {
|
||||
let foundationPile = foundations.find(pile => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (!foundationPile) {
|
||||
foundationPile = foundations.find(pile => pile.length === 0);
|
||||
}
|
||||
if (foundationPile.length === 0) {
|
||||
return card.rank === 'A'; // Only Ace can be placed on empty foundation
|
||||
} else {
|
||||
const topCard = foundationPile[foundationPile.length - 1];
|
||||
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let moved;
|
||||
do {
|
||||
moved = false;
|
||||
|
||||
for (let i = 0; i < tableau.length; i++) {
|
||||
const column = tableau[i];
|
||||
if (column.length === 0) continue;
|
||||
|
||||
const card = column[column.length - 1]; // Top card of the tableau column
|
||||
let foundationIndex = foundations.findIndex(pile => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (foundationIndex === -1) {
|
||||
foundationIndex = foundations.findIndex(pile => pile.length === 0);
|
||||
}
|
||||
if(canMoveToFoundation(card)) {
|
||||
let moveData = {
|
||||
destPileIndex: foundationIndex,
|
||||
destPileType: 'foundationPiles',
|
||||
sourceCardIndex: column.length - 1,
|
||||
sourcePileIndex: i,
|
||||
sourcePileType: 'tableauPiles',
|
||||
userId: userId,
|
||||
}
|
||||
tableau[i].pop()
|
||||
foundations[foundationIndex].push(card)
|
||||
//moveCard(gameState, moveData)
|
||||
moves.push(moveData);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
} while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0));
|
||||
emitSolitaireUpdate(userId, moves)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the game state to its previous state based on the last move in the history.
|
||||
* This function mutates the gameState object directly.
|
||||
|
||||
@@ -32,7 +32,7 @@ export function blackjackRoutes(io) {
|
||||
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
||||
blackjackPayout: 1.5, // 3:2
|
||||
cutCardRatio: 0.25,
|
||||
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 7000 },
|
||||
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 7000 },
|
||||
animation: { dealerDrawMs: 1000 }
|
||||
});
|
||||
|
||||
@@ -124,11 +124,12 @@ export function blackjackRoutes(io) {
|
||||
bank,
|
||||
currentBet: 0,
|
||||
inRound: false,
|
||||
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false }],
|
||||
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 }],
|
||||
activeHand: 0,
|
||||
joined_at: Date.now(),
|
||||
msgId: null,
|
||||
totalDelta: 0,
|
||||
totalBets: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -139,11 +140,16 @@ export function blackjackRoutes(io) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> joue au Blackjack`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor('#5865f2')
|
||||
.setTimestamp(new Date());
|
||||
@@ -176,6 +182,11 @@ export function blackjackRoutes(io) {
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor(room.players[userId].totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
||||
.setTimestamp(new Date());
|
||||
@@ -220,6 +231,7 @@ export function blackjackRoutes(io) {
|
||||
}
|
||||
|
||||
p.currentBet = bet;
|
||||
p.hands[p.activeHand].bet = bet;
|
||||
emitToast({ type: "player-bet", userId, amount: bet });
|
||||
emitUpdate("bet-placed", snapshot(room));
|
||||
return res.status(200).json({ message: "bet-accepted" });
|
||||
@@ -236,15 +248,32 @@ export function blackjackRoutes(io) {
|
||||
if (action === "double" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
if (coins < p.currentBet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - p.currentBet });
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'BLACKJACK_DOUBLE',
|
||||
coins_amount: -p.currentBet, user_new_amount: coins - p.currentBet,
|
||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - p.currentBet;
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
|
||||
if (action === "split" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'BLACKJACK_SPLIT',
|
||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import express from 'express';
|
||||
// --- Game Logic Imports ---
|
||||
import {
|
||||
createDeck, shuffle, deal, isValidMove, moveCard, drawCard,
|
||||
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards
|
||||
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves
|
||||
} from '../../game/solitaire.js';
|
||||
|
||||
// --- Game State & Database Imports ---
|
||||
@@ -60,6 +60,7 @@ export function solitaireRoutes(client, io) {
|
||||
gameState.moves = 0;
|
||||
gameState.hist = [];
|
||||
gameState.hardMode = hardMode ?? false;
|
||||
gameState.autocompleting = false;
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
|
||||
res.json({ success: true, gameState });
|
||||
@@ -94,6 +95,7 @@ export function solitaireRoutes(client, io) {
|
||||
seed: sotd.seed,
|
||||
hist: [],
|
||||
hardMode: false,
|
||||
autocompleting: false,
|
||||
};
|
||||
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
@@ -140,8 +142,17 @@ export function solitaireRoutes(client, io) {
|
||||
moveCard(gameState, moveData);
|
||||
updateGameStats(gameState, 'move', moveData);
|
||||
|
||||
if (!gameState.autocompleting) {
|
||||
const canAutoSolve = checkAutoSolve(gameState);
|
||||
if (canAutoSolve) {
|
||||
gameState.autocompleting = true;
|
||||
autoSolveMoves(userId, gameState)
|
||||
}
|
||||
}
|
||||
|
||||
const win = checkWinCondition(gameState);
|
||||
if (win) {
|
||||
console.log("win")
|
||||
gameState.isDone = true;
|
||||
await handleWin(userId, gameState, io);
|
||||
}
|
||||
|
||||
@@ -344,4 +344,6 @@ export async function emitPokerToast(data) {
|
||||
}
|
||||
|
||||
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
|
||||
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
||||
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
||||
|
||||
export const emitSolitaireUpdate = (userId, moves) => io.emit('solitaire:update', {userId, moves});
|
||||
Reference in New Issue
Block a user