Merge pull request #51 from cassoule/milo-250923

Milo 250923
This commit is contained in:
Milo Gourvest
2025-10-20 18:50:58 +02:00
committed by GitHub
6 changed files with 237 additions and 77 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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");

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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});