From af83c9f7c27577213f96051d20680aefec3eaa29 Mon Sep 17 00:00:00 2001 From: Milo Date: Tue, 23 Sep 2025 17:07:53 +0200 Subject: [PATCH 1/9] oyea --- src/game/blackjack.js | 8 +++++++- src/server/routes/blackjack.js | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/game/blackjack.js b/src/game/blackjack.js index acc789b..c14ef7c 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -254,7 +254,8 @@ export async function settleAll(room) { }); allRes[p.id] = res; p.totalDelta += res.delta - if (res.result === 'win' || res.result === 'push') { + p.totalBets++ + if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') { const userDB = getUser.get(p.id); if (userDB) { const coins = userDB.coins; @@ -289,6 +290,11 @@ export async function settleAll(room) { 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()); diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 8069644..85c8fa7 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -129,6 +129,7 @@ export function blackjackRoutes(io) { 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()); From cc917b47140fed8ed2f1a342a1299056149c7e4e Mon Sep 17 00:00:00 2001 From: Milo Date: Wed, 24 Sep 2025 17:23:20 +0200 Subject: [PATCH 2/9] mouais --- src/game/blackjack.js | 155 ++++++++++++++++++++------------- src/server/routes/blackjack.js | 28 ++++-- 2 files changed, 119 insertions(+), 64 deletions(-) diff --git a/src/game/blackjack.js b/src/game/blackjack.js index c14ef7c..9cf8fa8 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -123,6 +123,7 @@ export function publicPlayerView(player) { result: h.result ?? null, total: handValue(h.cards).total, soft: handValue(h.cards).soft, + bet: h.bet, })), }; } @@ -178,7 +179,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; } } @@ -225,8 +226,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,64 +243,65 @@ 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 - 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 + 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 Object.values(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, + }); + 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 - }, - { - 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); + 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); + } } } } @@ -327,6 +328,7 @@ export function applyAction(room, playerId, action) { case "double": { if (!canDouble(hand)) throw new Error("Cannot double now"); hand.doubled = true; + hand.bet*=2 p.currentBet*=2 hand.hasActed = true; // The caller (routes) must also handle additional balance lock on the bet if using real coins @@ -342,6 +344,41 @@ export function applyAction(room, playerId, action) { 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"); } diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 85c8fa7..eb34e26 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -124,7 +124,7 @@ 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, @@ -231,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" }); @@ -247,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 } From 6afa3d8e6b879126745b4723b5a9dcf3ef0c797d Mon Sep 17 00:00:00 2001 From: Milo Date: Fri, 26 Sep 2025 13:23:56 +0200 Subject: [PATCH 3/9] payout fix --- src/game/blackjack.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/blackjack.js b/src/game/blackjack.js index 9cf8fa8..26ab51d 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -201,7 +201,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: 0 } ]; } room.dealer.cards = [draw(room.shoe), draw(room.shoe)]; room.dealer.holeHidden = true; @@ -243,7 +243,7 @@ export async function settleAll(room) { const allRes = {} for (const p of Object.values(room.players)) { if (!p.inRound) continue; - for (const hand of Object.values(p.hands)) { + for (const hand of p.hands) { const res = settleHand({ bet: hand.bet, playerCards: hand.cards, From ebbc28f52ccf4a207ef045b9a6b1dcd6d1ad765d Mon Sep 17 00:00:00 2001 From: Milo Date: Tue, 14 Oct 2025 17:05:15 +0200 Subject: [PATCH 4/9] chore: solitaire auto-complete first steps --- src/bot/handlers/messageCreate.js | 7 +++- src/game/solitaire.js | 65 +++++++++++++++++++++++++++++++ src/server/routes/solitaire.js | 11 +++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index c0af9b0..8777673 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -16,7 +16,7 @@ import { calculateMaxPrice } from '../../utils/index.js'; import { channelPointsHandler, slowmodesHandler, randomSkinPrice, initTodaysSOTD } from '../../game/points.js'; -import { requestTimestamps, activeSlowmodes, activePolls, skins } from '../../game/state.js'; +import {requestTimestamps, activeSlowmodes, activePolls, skins, activeSolitaireGames} from '../../game/state.js'; import { flopoDB, getUser, @@ -27,6 +27,7 @@ import { getAllSkins, hardUpdateSkin } from '../../database/index.js'; import {client} from "../client.js"; +import {autoSolveMoves} from "../../game/solitaire.js"; // Constants for the AI rate limiter const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5"); @@ -280,5 +281,9 @@ async function handleAdminCommands(message) { }) console.log('Reworked', dbSkins.length, 'skins.'); break; + case `${prefix}:solve-solitaire`: + autoSolveMoves( + { "tableauPiles": [ [ { "suit": "d", "rank": "K", "faceUp": true }, { "suit": "s", "rank": "Q", "faceUp": true }, { "suit": "d", "rank": "J", "faceUp": true }, { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], [ { "suit": "h", "rank": "K", "faceUp": true }, { "suit": "c", "rank": "Q", "faceUp": true }, { "suit": "h", "rank": "J", "faceUp": true }, { "suit": "s", "rank": "T", "faceUp": true }, { "suit": "d", "rank": "9", "faceUp": true } ], [ { "suit": "s", "rank": "K", "faceUp": true }, { "suit": "d", "rank": "Q", "faceUp": true }, { "suit": "c", "rank": "J", "faceUp": true }, { "suit": "h", "rank": "T", "faceUp": true }, { "suit": "c", "rank": "9", "faceUp": true }, { "suit": "h", "rank": "8", "faceUp": true } ], [], [], [ { "suit": "c", "rank": "K", "faceUp": true }, { "suit": "h", "rank": "Q", "faceUp": true }, { "suit": "s", "rank": "J", "faceUp": true }, { "suit": "d", "rank": "T", "faceUp": true }, { "suit": "s", "rank": "9", "faceUp": true }, { "suit": "d", "rank": "8", "faceUp": true }, { "suit": "c", "rank": "7", "faceUp": true }, { "suit": "h", "rank": "6", "faceUp": true }, { "suit": "c", "rank": "5", "faceUp": true }, { "suit": "h", "rank": "4", "faceUp": true } ], [ { "suit": "h", "rank": "3", "faceUp": true } ] ], "foundationPiles": [ [ { "suit": "c", "rank": "A", "faceUp": true }, { "suit": "c", "rank": "2", "faceUp": true }, { "suit": "c", "rank": "3", "faceUp": true }, { "suit": "c", "rank": "4", "faceUp": true } ], [ { "suit": "h", "rank": "A", "faceUp": true }, { "suit": "h", "rank": "2", "faceUp": true } ], [ { "suit": "s", "rank": "A", "faceUp": true }, { "suit": "s", "rank": "2", "faceUp": true }, { "suit": "s", "rank": "3", "faceUp": true }, { "suit": "s", "rank": "4", "faceUp": true }, { "suit": "s", "rank": "5", "faceUp": true }, { "suit": "s", "rank": "6", "faceUp": true }, { "suit": "s", "rank": "7", "faceUp": true }, { "suit": "s", "rank": "8", "faceUp": true } ], [ { "suit": "d", "rank": "A", "faceUp": true }, { "suit": "d", "rank": "2", "faceUp": true }, { "suit": "d", "rank": "3", "faceUp": true }, { "suit": "d", "rank": "4", "faceUp": true }, { "suit": "d", "rank": "5", "faceUp": true }, { "suit": "d", "rank": "6", "faceUp": true }, { "suit": "d", "rank": "7", "faceUp": true } ] ], "stockPile": [], "wastePile": [], "seed": "mgqnxweyjp8fggj6ol9", "isSOTD": false, "score": 205, "moves": 90, "hist": [ { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 3, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 4, "cardsMoved": [ { "suit": "c", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 6, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "A", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "c", "rank": "5", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "4", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "h", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 4, "destPileType": "foundationPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "A", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "d", "rank": "8", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "8", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "h", "rank": "2", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "2", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "c", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "c", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "h", "rank": "T", "faceUp": true }, { "suit": "c", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "4", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "s", "rank": "3", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "h", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 3, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "h", "rank": "8", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "h", "rank": "8", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "s", "rank": "A", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "A", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "d", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "4", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "s", "rank": "5", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "2", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 8, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "2", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "c", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 8, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "c", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 1, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "h", "rank": "6", "faceUp": true }, { "suit": "c", "rank": "5", "faceUp": true }, { "suit": "h", "rank": "4", "faceUp": true }, { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "5", "faceUp": true } }, { "move": "draw", "card": { "suit": "d", "rank": "A", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "A", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "c", "rank": "3", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "3", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "tableauPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "6", "faceUp": true } }, { "move": "draw-reset" }, { "move": "draw", "card": { "suit": "h", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "d", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "d", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "s", "rank": "Q", "faceUp": true }, { "suit": "d", "rank": "J", "faceUp": true }, { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true }, { "suit": "c", "rank": "4", "faceUp": true }, { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 2, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "2", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 2, "sourceCardIndex": 8, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 1, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 1, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "2", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 10, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "4", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "s", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "4", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "h", "rank": "Q", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "s", "rank": "J", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "Q", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "c", "rank": "Q", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "s", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "s", "rank": "T", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "6", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "6", "faceUp": true } } ], "hardMode": false, "autocompleting": false } + ); } } \ No newline at end of file diff --git a/src/game/solitaire.js b/src/game/solitaire.js index faf0e82..a054603 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -296,6 +296,71 @@ 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(gameState) { + const moves = []; + const foundations = gameState.foundationPiles; + const tableau = gameState.tableauPiles; + + function canMoveToFoundation(card) { + const foundationPile = foundations.find(pile => pile[pile.length - 1].suit === card.suit || 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 + const foundationIndex = foundations.findIndex(pile => pile[pile.length - 1].suit === card.suit || pile.length === 0); + console.log(card.rank + card.suit + " to " + foundationIndex) + if(canMoveToFoundation(card)) { + tableau[i].pop() + foundations[foundationIndex].push(card) + console.log("moved" + card.rank + card.suit + " to " + foundationIndex) + moved = true; + moves.push({ + sourcePileType: 'tableauPiles', + sourcePileIndex: i, + sourceCardIndex: column.length - 1, + destPileType: 'foundationPiles', + destPileIndex: foundationIndex, + cardsMoved: [card], + cardWasFlipped: false, + points: 11 + }); + } + } + } while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0)); + + console.log("Auto-solve moves:"); + console.log(moves); + return moves; +} + /** * Reverts the game state to its previous state based on the last move in the history. * This function mutates the gameState object directly. diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index a0b579f..0824fa0 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -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,6 +142,13 @@ export function solitaireRoutes(client, io) { moveCard(gameState, moveData); updateGameStats(gameState, 'move', moveData); + const canAutoSolve = checkAutoSolve(gameState); + if (canAutoSolve) { + gameState.autocompleting = true; + // TODO: start auto-completing moves with interval + autoSolveMoves(gameState) + } + const win = checkWinCondition(gameState); if (win) { gameState.isDone = true; From 2cb159b91b0686ec881d87a090eda4124978c2f3 Mon Sep 17 00:00:00 2001 From: Milo Date: Mon, 20 Oct 2025 11:49:10 +0200 Subject: [PATCH 5/9] chore: solitaire auto-complete almost ready --- src/game/solitaire.js | 42 ++++++++++++++++++---------------- src/server/routes/solitaire.js | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/game/solitaire.js b/src/game/solitaire.js index a054603..3d37409 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -1,4 +1,6 @@ // --- Constants for Deck Creation --- +import {sleep} from "openai/core"; + 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']; @@ -311,13 +313,16 @@ export function checkAutoSolve(gameState) { return true; } -export function autoSolveMoves(gameState) { +export async function autoSolveMoves(gameState) { const moves = []; const foundations = gameState.foundationPiles; const tableau = gameState.tableauPiles; function canMoveToFoundation(card) { - const foundationPile = foundations.find(pile => pile[pile.length - 1].suit === card.suit || pile.length === 0); + 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 { @@ -335,29 +340,26 @@ export function autoSolveMoves(gameState) { if (column.length === 0) continue; const card = column[column.length - 1]; // Top card of the tableau column - const foundationIndex = foundations.findIndex(pile => pile[pile.length - 1].suit === card.suit || pile.length === 0); - console.log(card.rank + card.suit + " to " + foundationIndex) + 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)) { - tableau[i].pop() - foundations[foundationIndex].push(card) - console.log("moved" + card.rank + card.suit + " to " + foundationIndex) - moved = true; - moves.push({ - sourcePileType: 'tableauPiles', - sourcePileIndex: i, - sourceCardIndex: column.length - 1, - destPileType: 'foundationPiles', + let moveData = { destPileIndex: foundationIndex, - cardsMoved: [card], - cardWasFlipped: false, - points: 11 - }); + destPileType: 'foundationPiles', + sourceCardIndex: column.length - 1, + sourcePileIndex: i, + sourcePileType: 'tableauPiles', + userId: gameState.userId, + } + moveCard(gameState, moveData); + moved = true; + await sleep(500); // Pause for visualization + //TODO: maybe needs an emit here to update clients? } } } while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0)); - - console.log("Auto-solve moves:"); - console.log(moves); return moves; } diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index 0824fa0..a69194e 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -146,7 +146,7 @@ export function solitaireRoutes(client, io) { if (canAutoSolve) { gameState.autocompleting = true; // TODO: start auto-completing moves with interval - autoSolveMoves(gameState) + await autoSolveMoves(gameState) } const win = checkWinCondition(gameState); From d93c3c063a5159548ab311987c892aed1f6a80a3 Mon Sep 17 00:00:00 2001 From: Milo Date: Mon, 20 Oct 2025 14:09:32 +0200 Subject: [PATCH 6/9] quick fix --- src/game/solitaire.js | 6 ++++-- src/server/socket.js | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/game/solitaire.js b/src/game/solitaire.js index 3d37409..e5adeef 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -1,5 +1,6 @@ // --- 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']; @@ -353,10 +354,11 @@ export async function autoSolveMoves(gameState) { sourcePileType: 'tableauPiles', userId: gameState.userId, } - moveCard(gameState, moveData); + moveCard(gameState, moveData) + emitSolitaireUpdate(gameState.userId, moveData); moved = true; await sleep(500); // Pause for visualization - //TODO: maybe needs an emit here to update clients? + } } } while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0)); diff --git a/src/server/socket.js b/src/server/socket.js index c5fa0db..6343535 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -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); \ No newline at end of file +export const emitToast = (payload) => io.emit("blackjack:toast", payload); + +export const emitSolitaireUpdate = (userId, moveData) => io.emit('solitaire:update', {userId, moveData}); \ No newline at end of file From 59bbca5c00a092e6b0fa92a07053d0812aae84d4 Mon Sep 17 00:00:00 2001 From: milo Date: Mon, 20 Oct 2025 18:47:47 +0200 Subject: [PATCH 7/9] feat: solitaire autocomplete --- src/game/blackjack.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/game/blackjack.js b/src/game/blackjack.js index 26ab51d..7e38c8a 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -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. @@ -201,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, bet: 0 } ]; + 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; @@ -252,7 +253,12 @@ export async function settleAll(room) { surrendered: hand.surrendered, blackjackPayout: room.settings.blackjackPayout, }); - allRes[p.id] = res; + 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') { @@ -323,27 +329,22 @@ 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; hand.bet*=2 - p.currentBet*=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]; From b2df6a5b499c1e2abcb48c510791044f1218ca51 Mon Sep 17 00:00:00 2001 From: milo Date: Mon, 20 Oct 2025 18:48:08 +0200 Subject: [PATCH 8/9] feat: blackjack split --- src/game/solitaire.js | 18 +++++++++--------- src/server/routes/solitaire.js | 12 +++++++----- src/server/socket.js | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/game/solitaire.js b/src/game/solitaire.js index e5adeef..a5f76eb 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -314,10 +314,10 @@ export function checkAutoSolve(gameState) { return true; } -export async function autoSolveMoves(gameState) { +export function autoSolveMoves(userId, gameState) { const moves = []; - const foundations = gameState.foundationPiles; - const tableau = gameState.tableauPiles; + 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); @@ -352,17 +352,17 @@ export async function autoSolveMoves(gameState) { sourceCardIndex: column.length - 1, sourcePileIndex: i, sourcePileType: 'tableauPiles', - userId: gameState.userId, + userId: userId, } - moveCard(gameState, moveData) - emitSolitaireUpdate(gameState.userId, moveData); + tableau[i].pop() + foundations[foundationIndex].push(card) + //moveCard(gameState, moveData) + moves.push(moveData); moved = true; - await sleep(500); // Pause for visualization - } } } while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0)); - return moves; + emitSolitaireUpdate(userId, moves) } /** diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index a69194e..9082d0e 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -142,15 +142,17 @@ export function solitaireRoutes(client, io) { moveCard(gameState, moveData); updateGameStats(gameState, 'move', moveData); - const canAutoSolve = checkAutoSolve(gameState); - if (canAutoSolve) { - gameState.autocompleting = true; - // TODO: start auto-completing moves with interval - await autoSolveMoves(gameState) + 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); } diff --git a/src/server/socket.js b/src/server/socket.js index 6343535..fc3a28b 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -346,4 +346,4 @@ 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 emitSolitaireUpdate = (userId, moveData) => io.emit('solitaire:update', {userId, moveData}); \ No newline at end of file +export const emitSolitaireUpdate = (userId, moves) => io.emit('solitaire:update', {userId, moves}); \ No newline at end of file From 27c9e9e6ad075d6d0df6cdb650c5e5fec828abd9 Mon Sep 17 00:00:00 2001 From: milo Date: Mon, 20 Oct 2025 18:50:37 +0200 Subject: [PATCH 9/9] fix: blackjack playtime 15s -> 20s --- src/game/blackjack.js | 2 +- src/server/routes/blackjack.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/blackjack.js b/src/game/blackjack.js index 7e38c8a..ca3729f 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -141,7 +141,7 @@ export function createBlackjackRoom({ phaseDurations = { bettingMs: 15000, dealMs: 1000, - playMsPerPlayer: 15000, + playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 10000, }, diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index eb34e26..fe8296f 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -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 } });