mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
575 lines
17 KiB
JavaScript
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 = {};
|
|
}
|