mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
blackjack beta 1
This commit is contained in:
@@ -12,7 +12,7 @@ export function initializeEvents(client, io) {
|
||||
// --- on 'ready' ---
|
||||
// This event fires once the bot has successfully logged in and is ready to operate.
|
||||
// It's a good place for setup tasks that require the bot to be online.
|
||||
client.once('ready', async () => {
|
||||
client.once('clientReady', async () => {
|
||||
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
|
||||
console.log('[Startup] Bot is ready, performing initial data sync...');
|
||||
await getAkhys(client);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Core blackjack helpers for a single continuous room.
|
||||
// Inspired by your poker helpers API style.
|
||||
|
||||
import {emitToast} from "../server/socket.js";
|
||||
import {getUser, insertLog, updateUserCoins} from "../database/index.js";
|
||||
|
||||
export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"];
|
||||
export const SUITS = ["d","s","c","h"];
|
||||
|
||||
@@ -88,7 +91,7 @@ export function settleHand({ bet, playerCards, dealerCards, doubled = false, sur
|
||||
if (pBJ && dBJ) return { delta: 0, result: "push" };
|
||||
|
||||
const outcome = compareHands(playerCards, dealerCards);
|
||||
let unit = bet * (doubled ? 2 : 1);
|
||||
let unit = bet;
|
||||
if (outcome === "win") return { delta: unit, result: "win" };
|
||||
if (outcome === "lose") return { delta: -unit, result: "lose" };
|
||||
return { delta: 0, result: "push" };
|
||||
@@ -131,7 +134,16 @@ export function createBlackjackRoom({
|
||||
hitSoft17 = false,
|
||||
blackjackPayout = 1.5,
|
||||
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
|
||||
phaseDurations = { bettingMs: 15000, dealMs: 1000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 2000 }
|
||||
phaseDurations = {
|
||||
bettingMs: 15000,
|
||||
dealMs: 1000,
|
||||
playMsPerPlayer: 15000,
|
||||
revealMs: 1000,
|
||||
payoutMs: 10000,
|
||||
},
|
||||
animation = {
|
||||
dealerDrawMs: 500,
|
||||
}
|
||||
} = {}) {
|
||||
return {
|
||||
id: "blackjack-room",
|
||||
@@ -140,7 +152,7 @@ export function createBlackjackRoom({
|
||||
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
|
||||
phase_ends_at: Date.now() + phaseDurations.bettingMs,
|
||||
minBet, maxBet, fakeMoney,
|
||||
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations },
|
||||
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation },
|
||||
shoe: buildShoe(decks),
|
||||
discard: [],
|
||||
dealer: { cards: [], holeHidden: true },
|
||||
@@ -226,6 +238,7 @@ export function dealerPlay(room) {
|
||||
|
||||
export function settleAll(room) {
|
||||
room.status = "payout";
|
||||
const allRes = {}
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const hand = p.hands[p.activeHand];
|
||||
@@ -237,6 +250,24 @@ export function settleAll(room) {
|
||||
surrendered: hand.surrendered,
|
||||
blackjackPayout: room.settings.blackjackPayout,
|
||||
});
|
||||
allRes[p.id] = res;
|
||||
if (res.result === 'win' || res.result === 'push') {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
try {
|
||||
updateUserCoins.run({ id: p.id, coins: userDB.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: userDB.coins + p.currentBet + res.delta,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
emitToast({ type: `payout-res`, allRes });
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
}
|
||||
@@ -264,6 +295,7 @@ export function applyAction(room, playerId, action) {
|
||||
case "double": {
|
||||
if (!canDouble(hand)) throw new Error("Cannot double now");
|
||||
hand.doubled = true;
|
||||
p.currentBet*=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));
|
||||
|
||||
@@ -22,7 +22,7 @@ const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV :
|
||||
// CORS Middleware
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', FLAPI_URL);
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires');
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// /routes/blackjack.js
|
||||
import express from "express";
|
||||
import { createBlackjackRoom, startBetting, dealInitial, autoActions, everyoneDone, dealerPlay, settleAll, applyAction, publicPlayerView, handValue } from "../../game/blackjack.js";
|
||||
import {
|
||||
createBlackjackRoom,
|
||||
startBetting,
|
||||
dealInitial,
|
||||
autoActions,
|
||||
everyoneDone,
|
||||
dealerPlay,
|
||||
settleAll,
|
||||
applyAction,
|
||||
publicPlayerView,
|
||||
handValue,
|
||||
dealerShouldHit, draw
|
||||
} from "../../game/blackjack.js";
|
||||
|
||||
// Optional: hook into your DB & Discord systems if available
|
||||
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
||||
@@ -19,9 +31,65 @@ export function blackjackRoutes(io) {
|
||||
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
||||
blackjackPayout: 1.5, // 3:2
|
||||
cutCardRatio: 0.25,
|
||||
phaseDurations: { bettingMs: 15000, dealMs: 1000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 2000 },
|
||||
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 7000 },
|
||||
animation: { dealerDrawMs: 1000 }
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
||||
let animatingDealer = false;
|
||||
|
||||
async function runDealerAnimation() {
|
||||
if (animatingDealer) return;
|
||||
animatingDealer = true;
|
||||
|
||||
room.status = "dealer";
|
||||
room.dealer.holeHidden = false;
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000);
|
||||
emitUpdate("dealer-reveal", snapshot(room));
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
|
||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||
room.dealer.cards.push(draw(room.shoe));
|
||||
room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500);
|
||||
emitUpdate("dealer-hit", snapshot(room));
|
||||
await sleep(room.settings.animation?.dealerDrawMs ?? 500);
|
||||
}
|
||||
|
||||
settleAll(room);
|
||||
room.status = "payout";
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
|
||||
emitUpdate("payout", snapshot(room))
|
||||
|
||||
animatingDealer = false;
|
||||
}
|
||||
|
||||
function autoTimeoutAFK(now) {
|
||||
if (room.status !== "playing") return false;
|
||||
if (!room.phase_ends_at || now < room.phase_ends_at) return false;
|
||||
|
||||
let changed = false;
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
||||
h.surrendered = true;
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-timeout", userId: p.id });
|
||||
changed = true;
|
||||
} else if (h.hasActed && !h.stood) {
|
||||
h.stood = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||
return changed;
|
||||
}
|
||||
|
||||
function snapshot(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
@@ -144,7 +212,7 @@ export function blackjackRoutes(io) {
|
||||
|
||||
// --- Game loop ---
|
||||
// Simple phase machine that runs regardless of player count.
|
||||
setInterval(() => {
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
|
||||
if (room.status === "betting" && now >= room.phase_ends_at) {
|
||||
@@ -158,41 +226,28 @@ export function blackjackRoutes(io) {
|
||||
dealInitial(room);
|
||||
autoActions(room);
|
||||
emitUpdate("initial-deal", snapshot(room));
|
||||
|
||||
room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer;
|
||||
emitUpdate("playing-start", snapshot(room));
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.status === "playing") {
|
||||
// When all active players are done, proceed to dealer play
|
||||
if (everyoneDone(room)) {
|
||||
dealerPlay(room);
|
||||
emitUpdate("dealer-start", snapshot(room));
|
||||
}
|
||||
}
|
||||
|
||||
if (room.status === "dealer") {
|
||||
settleAll(room);
|
||||
|
||||
// Apply coin deltas
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (room.settings.fakeMoney) continue;
|
||||
if (typeof h.delta === "number" && h.delta !== 0) {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
updateUserCoins.run({ id: p.id, coins: userDB.coins + h.delta });
|
||||
insertLog.run({
|
||||
id: `${p.id}-blackjack-${Date.now()}`,
|
||||
user_id: p.id, target_user_id: null,
|
||||
action: `BLACKJACK_${h.delta > 0 ? "WIN" : "LOSE"}`,
|
||||
coins_amount: h.delta, user_new_amount: userDB.coins + h.delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
// If the per-round playing timer expired, auto-surrender AFKs (you already added this)
|
||||
if (room.phase_ends_at && now >= room.phase_ends_at) {
|
||||
autoTimeoutAFK(now);
|
||||
}
|
||||
|
||||
room.phase_ends_at = now + room.settings.phaseDurations.payoutMs;
|
||||
emitUpdate("payout", snapshot(room));
|
||||
room.status = "payout";
|
||||
// Everyone acted before the timer? Cut short and go straight to dealer.
|
||||
if (everyoneDone(room) && !animatingDealer) {
|
||||
// Set a new server-driven deadline for the reveal pause,
|
||||
// so the client's countdown immediately reflects the phase change.
|
||||
room.phase_ends_at = Date.now();
|
||||
emitUpdate("playing-cut-short", snapshot(room));
|
||||
|
||||
// Now run the animated dealer with per-step updates
|
||||
runDealerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
if (room.status === "payout" && now >= room.phase_ends_at) {
|
||||
@@ -204,7 +259,7 @@ export function blackjackRoutes(io) {
|
||||
startBetting(room, now);
|
||||
emitUpdate("new-round", snapshot(room));
|
||||
}
|
||||
}, 400);
|
||||
}, 100);
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user