Files
flopobot_v2/src/server/routes/poker.js
2026-02-11 18:01:45 +01:00

575 lines
17 KiB
JavaScript

import express from "express";
import { v4 as uuidv4 } from "uuid";
import { adjectives, uniqueNamesGenerator } from "unique-names-generator";
import pkg from "pokersolver";
import { pokerRooms } from "../../game/state.js";
import {
checkEndOfBettingRound,
checkRoomWinners,
getFirstActivePlayerAfterDealer,
getNextActivePlayer,
initialShuffledCards,
} from "../../game/poker.js";
import * as userService from "../../services/user.service.js";
import * as logService from "../../services/log.service.js";
import { sleep } from "openai/core";
import { client } from "../../bot/client.js";
import { emitPokerToast, emitPokerUpdate } from "../socket.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { formatAmount } from "../../utils/index.js";
import { requireAuth } from "../middleware/auth.js";
const { Hand } = pkg;
const router = express.Router();
/**
* Factory function to create and configure the poker API routes.
* @param {object} client - The Discord.js client instance.
* @param {object} io - The Socket.IO server instance.
* @returns {object} The configured Express router.
*/
export function pokerRoutes(client, io) {
// --- Room Management Endpoints ---
router.get("/", (req, res) => {
res.status(200).json({ rooms: pokerRooms });
});
router.get("/:id", (req, res) => {
const room = pokerRooms[req.params.id];
if (room) {
res.status(200).json({ room });
} else {
res.status(404).json({ message: "Poker room not found." });
}
});
router.post("/create", requireAuth, async (req, res) => {
const creatorId = req.userId;
const { minBet, fakeMoney } = req.body;
if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) {
return res.status(403).json({ message: "You are already in a poker room." });
}
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const creator = await client.users.fetch(creatorId);
const id = uuidv4();
const name = uniqueNamesGenerator({
dictionaries: [adjectives, ["Poker"]],
separator: " ",
style: "capital",
});
pokerRooms[id] = {
id,
host_id: creatorId,
host_name: creator.globalName || creator.username,
name,
created_at: Date.now(),
last_move_at: null,
players: {},
queue: {},
afk: {},
pioche: initialShuffledCards(),
tapis: [],
dealer: null,
sb: null,
bb: null,
highest_bet: 0,
current_player: null,
current_turn: null,
playing: false,
winners: [],
waiting_for_restart: false,
fakeMoney: fakeMoney,
minBet: minBet,
};
await joinRoom(id, creatorId, io); // Auto-join the creator
await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" });
try {
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
const embed = new EmbedBuilder()
.setTitle("Flopoker 🃏")
.setDescription(`<@${creatorId}> a créé une table de poker`)
.addFields(
{ name: `Nom`, value: `**${name}**`, inline: true },
{
name: `${fakeMoney ? "Mise initiale" : "Prix d'entrée"}`,
value: `**${formatAmount(minBet)}** 🪙`,
inline: true,
},
{
name: `Fake Money`,
value: `${fakeMoney ? "**Oui** ✅" : "**Non** ❌"}`,
inline: true,
},
)
.setColor("#5865f2")
.setTimestamp(new Date());
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel(`Rejoindre la table ${name}`)
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
.setStyle(ButtonStyle.Link),
);
await generalChannel.send({ embeds: [embed], components: [row] });
} catch (e) {
console.log(`[${Date.now()}]`, e);
}
res.status(201).json({ roomId: id });
});
router.post("/join", requireAuth, async (req, res) => {
const userId = req.userId;
const { roomId } = req.body;
if (!roomId) return res.status(400).json({ message: "Room ID is required." });
if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." });
if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) {
return res.status(403).json({ message: "You are already in a room or queue." });
}
if (
!pokerRooms[roomId].fakeMoney &&
pokerRooms[roomId].minBet > ((await userService.getUser(userId))?.coins ?? 0)
) {
return res.status(403).json({ message: "You do not have enough coins to join this room." });
}
await joinRoom(roomId, userId, io);
res.status(200).json({ message: "Successfully joined." });
});
router.post("/accept", requireAuth, async (req, res) => {
const hostId = req.userId;
const { playerId, roomId } = req.body;
const room = pokerRooms[roomId];
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
return res.status(403).json({ message: "Unauthorized or player not in queue." });
}
if (!room.fakeMoney) {
const userDB = await userService.getUser(playerId);
if (userDB) {
await userService.updateUserCoins(playerId, userDB.coins - room.minBet);
await logService.insertLog({
id: `${playerId}-poker-${Date.now()}`,
userId: playerId,
targetUserId: null,
action: "POKER_JOIN",
coinsAmount: -room.minBet,
userNewAmount: userDB.coins - room.minBet,
});
}
}
room.players[playerId] = room.queue[playerId];
delete room.queue[playerId];
await emitPokerUpdate({ room: room, type: "player-accepted" });
res.status(200).json({ message: "Player accepted." });
});
router.post("/leave", requireAuth, async (req, res) => {
const userId = req.userId;
const { roomId } = req.body;
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
if (
pokerRooms[roomId].playing &&
pokerRooms[roomId].current_turn !== null &&
pokerRooms[roomId].current_turn !== 4
) {
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId];
try {
pokerRooms[roomId].players[userId].folded = true;
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn;
if (pokerRooms[roomId].current_player === userId) {
await checkRoundCompletion(pokerRooms[roomId], io);
}
} catch (e) {
console.log(`[${Date.now()}]`, e);
}
await emitPokerUpdate({ type: "player-afk" });
return res.status(200);
}
try {
await updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
);
delete pokerRooms[roomId].players[userId];
if (userId === pokerRooms[roomId].host_id) {
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
if (!newHostId) {
delete pokerRooms[roomId];
} else {
pokerRooms[roomId].host_id = newHostId;
}
}
} catch (e) {
console.log(`[${Date.now()}]`, e);
}
await emitPokerUpdate({ type: "player-left" });
return res.status(200);
});
router.post("/kick", requireAuth, async (req, res) => {
const commandUserId = req.userId;
const { userId, roomId } = req.body;
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" });
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: "Seul l'host peut kick" });
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
if (
pokerRooms[roomId].playing &&
pokerRooms[roomId].current_turn !== null &&
pokerRooms[roomId].current_turn !== 4
) {
return res.status(403).send({ message: "Playing" });
}
try {
await updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
);
delete pokerRooms[roomId].players[userId];
if (userId === pokerRooms[roomId].host_id) {
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
if (!newHostId) {
delete pokerRooms[roomId];
} else {
pokerRooms[roomId].host_id = newHostId;
}
}
} catch (e) {
console.log(`[${Date.now()}]`, e);
}
await emitPokerUpdate({ type: "player-kicked" });
return res.status(200);
});
// --- Game Action Endpoints ---
router.post("/start", requireAuth, async (req, res) => {
const { roomId } = req.body;
const room = pokerRooms[roomId];
if (!room) return res.status(404).json({ message: "Room not found." });
if (Object.keys(room.players).length < 2) return res.status(400).json({ message: "Not enough players to start." });
await startNewHand(room, io);
res.status(200).json({ message: "Game started." });
});
// NEW: Endpoint to start the next hand
router.post("/next-hand", requireAuth, async (req, res) => {
const { roomId } = req.body;
const room = pokerRooms[roomId];
if (!room || !room.waiting_for_restart) {
return res.status(400).json({ message: "Not ready for the next hand." });
}
await startNewHand(room, io);
res.status(200).json({ message: "Next hand started." });
});
router.post("/action/:action", requireAuth, async (req, res) => {
const playerId = req.userId;
const { amount, roomId } = req.body;
const { action } = req.params;
const room = pokerRooms[roomId];
if (!room || !room.players[playerId] || room.current_player !== playerId) {
return res.status(403).json({ message: "It's not your turn or you are not in this game." });
}
const player = room.players[playerId];
switch (action) {
case "fold":
player.folded = true;
await emitPokerToast({
type: "player-fold",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
});
break;
case "check":
if (player.bet < room.highest_bet) return res.status(400).json({ message: "Cannot check." });
await emitPokerToast({
type: "player-check",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
});
break;
case "call":
const callAmount = Math.min(room.highest_bet - player.bet, player.bank);
player.bank -= callAmount;
player.bet += callAmount;
if (player.bank === 0) player.allin = true;
await emitPokerToast({
type: "player-call",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
});
break;
case "raise":
if (amount <= 0 || amount > player.bank || player.bet + amount <= room.highest_bet) {
return res.status(400).json({ message: "Invalid raise amount." });
}
player.bank -= amount;
player.bet += amount;
if (player.bank === 0) player.allin = true;
room.highest_bet = player.bet;
await emitPokerToast({
type: "player-raise",
amount: amount,
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
});
break;
default:
return res.status(400).json({ message: "Invalid action." });
}
player.last_played_turn = room.current_turn;
await checkRoundCompletion(room, io);
res.status(200).json({ message: `Action '${action}' successful.` });
});
return router;
}
// --- Helper Functions ---
async function joinRoom(roomId, userId, io) {
const user = await client.users.fetch(userId);
const userDB = await userService.getUser(userId);
const room = pokerRooms[roomId];
const playerObject = {
id: userId,
globalName: user.globalName || user.username,
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
hand: [],
bank: room.minBet,
bet: 0,
folded: false,
allin: false,
last_played_turn: null,
solve: null,
};
if (room.playing) {
room.queue[userId] = playerObject;
} else {
room.players[userId] = playerObject;
if (!room.fakeMoney) {
await userService.updateUserCoins(userId, userDB.coins - room.minBet);
await logService.insertLog({
id: `${userId}-poker-${Date.now()}`,
userId: userId,
targetUserId: null,
action: "POKER_JOIN",
coinsAmount: -room.minBet,
userNewAmount: userDB.coins - room.minBet,
});
}
}
await emitPokerUpdate({ room: room, type: "player-joined" });
}
async function startNewHand(room, io) {
const playerIds = Object.keys(room.players);
if (playerIds.length < 2) {
room.playing = false; // Not enough players to continue
await emitPokerUpdate({ room: room, type: "new-hand" });
return;
}
room.playing = true;
room.current_turn = 0; // Pre-flop
room.pioche = initialShuffledCards();
room.tapis = [];
room.winners = [];
room.waiting_for_restart = false;
room.highest_bet = 20;
room.last_move_at = Date.now();
// Rotate dealer
const oldDealerIndex = playerIds.indexOf(room.dealer);
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
Object.values(room.players).forEach((p) => {
p.hand = [room.pioche.pop(), room.pioche.pop()];
p.bet = 0;
p.folded = false;
p.allin = false;
p.last_played_turn = null;
});
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
// Handle blinds based on new dealer
const dealerIndex = playerIds.indexOf(room.dealer);
const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]];
const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]];
room.sb = sbPlayer.id;
room.bb = bbPlayer.id;
sbPlayer.bank -= 10;
sbPlayer.bet = 10;
bbPlayer.bank -= 20;
bbPlayer.bet = 20;
bbPlayer.last_played_turn = 0;
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
await emitPokerUpdate({ room: room, type: "room-started" });
}
async function checkRoundCompletion(room, io) {
room.last_move_at = Date.now();
const roundResult = checkEndOfBettingRound(room);
if (roundResult.endRound) {
if (roundResult.winner) {
await handleShowdown(room, io, [roundResult.winner]);
} else {
await advanceToNextPhase(room, io, roundResult.nextPhase);
}
} else {
room.current_player = getNextActivePlayer(room);
await emitPokerUpdate({ room: room, type: "round-continue" });
}
}
async function advanceToNextPhase(room, io, phase) {
Object.values(room.players).forEach((p) => {
if (!p.folded) p.last_played_turn = null;
});
switch (phase) {
case "flop":
room.current_turn = 1;
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
break;
case "turn":
room.current_turn = 2;
room.tapis.push(room.pioche.pop());
break;
case "river":
room.current_turn = 3;
room.tapis.push(room.pioche.pop());
break;
case "showdown":
await handleShowdown(room, io, checkRoomWinners(room));
return;
case "progressive-showdown":
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
while (room.tapis.length < 5) {
await sleep(500);
room.tapis.push(room.pioche.pop());
updatePlayerHandSolves(room);
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
}
await handleShowdown(room, io, checkRoomWinners(room));
return;
}
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
room.current_player = getFirstActivePlayerAfterDealer(room);
await emitPokerUpdate({ room: room, type: "phase-advanced" });
}
async function handleShowdown(room, io, winners) {
room.current_turn = 4;
room.playing = false;
room.waiting_for_restart = true;
room.winners = winners;
room.current_player = null;
let totalPot = 0;
Object.values(room.players).forEach((p) => {
totalPot += p.bet;
});
const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0;
winners.forEach((winnerId) => {
const winnerPlayer = room.players[winnerId];
if (winnerPlayer) {
winnerPlayer.bank += winAmount;
}
});
await clearAfkPlayers(room);
//await pokerEloHandler(room);
await emitPokerUpdate({ room: room, type: "showdown" });
await emitPokerToast({
type: "player-winner",
playerIds: winners,
roomId: room.id,
amount: winAmount,
});
}
// NEW: Function to calculate and update hand strength for all players
function updatePlayerHandSolves(room) {
const communityCards = room.tapis;
for (const player of Object.values(room.players)) {
if (!player.folded) {
const allCards = [...communityCards, ...player.hand];
player.solve = Hand.solve(allCards).descr;
}
}
}
async function updatePlayerCoins(player, amount, isFake) {
if (isFake) return;
const user = await userService.getUser(player.id);
if (!user) return;
const userDB = await userService.getUser(player.id);
await userService.updateUserCoins(player.id, userDB.coins + amount);
await logService.insertLog({
id: `${player.id}-poker-${Date.now()}`,
userId: player.id,
targetUserId: null,
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
coinsAmount: amount,
userNewAmount: userDB.coins + amount,
});
}
async function clearAfkPlayers(room) {
for (const playerId of Object.keys(room.afk)) {
if (room.players[playerId]) {
await updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
delete room.players[playerId];
}
}
room.afk = {};
}