const { EmbedBuilder } = require("discord.js"); const { getProfileCollections, getDefaultStats, trackProfileEvent, trackGameActivity } = require("./profileSystem"); const { getUnixTime } = require("./cookieEconomy"); const POKER_GAME_KEY = "poker"; const PROFILE_HAND_HISTORY_LIMIT = 100; const PROFILE_ACTION_HISTORY_LIMIT = 250; const ACTION_STAT_PATHS = { fold: "games.poker.folds", check: "games.poker.checks", call: "games.poker.calls", bet: "games.poker.bets", raise: "games.poker.raises", all_in: "games.poker.allInActions" }; const HAND_RANK_STAT_PATHS = { "High Card": "games.poker.handRanks.highCard", Pair: "games.poker.handRanks.pair", "Two Pair": "games.poker.handRanks.twoPair", "Three of a Kind": "games.poker.handRanks.threeOfAKind", Straight: "games.poker.handRanks.straight", Flush: "games.poker.handRanks.flush", "Full House": "games.poker.handRanks.fullHouse", "Four of a Kind": "games.poker.handRanks.fourOfAKind", "Straight Flush": "games.poker.handRanks.straightFlush", "Royal Flush": "games.poker.handRanks.royalFlush" }; async function ensurePokerProfileDocument( userId, guildId ) { const { stats } = getProfileCollections(); await stats.updateOne( { userId, guildId }, { $setOnInsert: getDefaultStats( userId, guildId ) }, { upsert: true } ); } function getPokerHandId( game ) { if ( !game?.gameId || !Number.isSafeInteger( Number( game?.data?.handNumber ) ) ) { return null; } return ( `${game.gameId}:hand:${Number(game.data.handNumber)}` ); } function getPokerActionId( game ) { if ( !game?.gameId || !Number.isSafeInteger( Number(game.version) ) ) { return null; } return ( `${game.gameId}:action:v${Number(game.version)}` ); } function normalizeActionType( action ) { return String( action?.type ?? "" ).toLowerCase(); } function normalizeCardRank( card ) { return String( card?.rank ?? "" ).toUpperCase(); } function isPocketAces( holeCards ) { return ( Array.isArray(holeCards) && holeCards.length === 2 && holeCards.every( card => normalizeCardRank(card) === "A" ) ); } function isSevenDeuce( holeCards ) { if ( !Array.isArray(holeCards) || holeCards.length !== 2 ) { return false; } const ranks = holeCards .map( normalizeCardRank ) .sort(); return ( ranks[0] === "2" && ranks[1] === "7" ); } function getPotSizeFromResult( result ) { return Object.values( result?.payouts ?? {} ).reduce( (sum, value) => sum + Number(value ?? 0), 0 ); } function getPlayerOutcome( game, handPlayer ) { const hand = game?.data?.hand; const result = hand?.result ?? game?.data?.lastHandResult; const winnerSeats = Array.isArray( result?.winnerSeats ) ? result.winnerSeats : []; const isWinner = winnerSeats.includes( handPlayer.seat ); const isSplitWinner = isWinner && winnerSeats.length > 1; const finalStack = Number( handPlayer.stack ?? 0 ); const startingStack = Number( handPlayer.startingStack ?? 0 ); const profit = finalStack - startingStack; const potSize = getPotSizeFromResult( result ); let resultName = "loss"; if (isSplitWinner) { resultName = "push"; } else if (isWinner) { resultName = "win"; } const reachedShowdown = result?.type === "showdown" && handPlayer.folded !== true; return { result, winnerSeats, isWinner, isSplitWinner, resultName, profit, potSize, reachedShowdown, allIn: handPlayer.allIn === true, handRank: handPlayer.handRank ?? null, pocketAces: isPocketAces( handPlayer.holeCards ), sevenDeuce: isSevenDeuce( handPlayer.holeCards ), foldWin: isWinner && result?.type === "fold", fullTableWin: isWinner && Array.isArray(hand?.players) && hand.players.length === 6 }; } async function recordPokerActionProfile( game, userId, action ) { const guildId = game?.guildId; const actionType = normalizeActionType( action ); const statPath = ACTION_STAT_PATHS[ actionType ]; const actionId = getPokerActionId( game ); if ( !guildId || !userId || !statPath || !actionId ) { return false; } await ensurePokerProfileDocument( String(userId), String(guildId) ); const { stats } = getProfileCollections(); const result = await stats.updateOne( { userId: String(userId), guildId: String(guildId), "games.poker.processedActions": { $ne: actionId } }, { $inc: { [statPath]: 1 }, $push: { "games.poker.processedActions": { $each: [ actionId ], $slice: -PROFILE_ACTION_HISTORY_LIMIT } } } ); return ( result.modifiedCount === 1 ); } async function repairProfileBestStreaks( userId, guildId ) { const { stats } = getProfileCollections(); const current = await stats.findOne( { userId, guildId }, { projection: { "games.poker.currentWinStreak": 1, "games.poker.currentLossStreak": 1, "streaks.currentWin": 1, "streaks.currentLoss": 1 } } ); await stats.updateOne( { userId, guildId }, { $max: { "games.poker.bestWinStreak": Number( current ?.games ?.poker ?.currentWinStreak ?? 0 ), "games.poker.bestLossStreak": Number( current ?.games ?.poker ?.currentLossStreak ?? 0 ), "streaks.bestWin": Number( current ?.streaks ?.currentWin ?? 0 ), "streaks.bestLoss": Number( current ?.streaks ?.currentLoss ?? 0 ) } } ); } async function recordPokerHandForPlayer( game, handPlayer ) { const guildId = String( game.guildId ); const userId = String( handPlayer.userId ); const handId = getPokerHandId( game ); if (!handId) { return { processed: false, userId, reason: "INVALID_HAND_ID" }; } await ensurePokerProfileDocument( userId, guildId ); const outcome = getPlayerOutcome( game, handPlayer ); const inc = { "games.total.played": 1, "games.poker.played": 1, "games.poker.handsPlayed": 1 }; if ( outcome.resultName === "win" ) { inc[ "games.total.won" ] = 1; inc[ "games.poker.won" ] = 1; inc[ "games.poker.handsWon" ] = 1; } else if ( outcome.resultName === "loss" ) { inc[ "games.total.lost" ] = 1; inc[ "games.poker.lost" ] = 1; inc[ "games.poker.handsLost" ] = 1; } else { inc[ "games.total.pushed" ] = 1; inc[ "games.poker.pushed" ] = 1; inc[ "games.poker.handsPushed" ] = 1; } const set = {}; if ( outcome.resultName === "win" ) { inc[ "games.poker.currentWinStreak" ] = 1; inc[ "streaks.currentWin" ] = 1; set[ "games.poker.currentLossStreak" ] = 0; set[ "streaks.currentLoss" ] = 0; } else if ( outcome.resultName === "loss" ) { inc[ "games.poker.currentLossStreak" ] = 1; inc[ "streaks.currentLoss" ] = 1; set[ "games.poker.currentWinStreak" ] = 0; set[ "streaks.currentWin" ] = 0; } if ( outcome.profit > 0 ) { inc[ "games.poker.cookiesWon" ] = outcome.profit; inc[ "economy.cookiesObtainedTotal" ] = outcome.profit; } else if ( outcome.profit < 0 ) { const loss = Math.abs( outcome.profit ); inc[ "games.poker.cookiesLost" ] = loss; inc[ "economy.cookiesLostTotal" ] = loss; } if ( outcome.reachedShowdown ) { inc[ "games.poker.showdowns" ] = 1; if (outcome.isWinner) { inc[ "games.poker.showdownWins" ] = 1; } } if ( outcome.allIn ) { inc[ "games.poker.allIns" ] = 1; if (outcome.isWinner) { inc[ "games.poker.allInWins" ] = 1; } } if ( outcome.foldWin ) { inc[ "games.poker.foldWins" ] = 1; } if ( outcome.fullTableWin ) { inc[ "games.poker.fullTableWins" ] = 1; } if ( outcome.isWinner && outcome.pocketAces ) { inc[ "games.poker.pocketAcesWins" ] = 1; } if ( outcome.isWinner && outcome.sevenDeuce ) { inc[ "games.poker.sevenDeuceWins" ] = 1; } const handRankPath = HAND_RANK_STAT_PATHS[ outcome.handRank?.name ]; if ( outcome.reachedShowdown && handRankPath ) { inc[ handRankPath ] = 1; } const { stats } = getProfileCollections(); const update = { $inc: inc, ...(Object.keys(set).length ? { $set: set } : {}), $push: { "games.poker.processedHands": { $each: [ handId ], $slice: -PROFILE_HAND_HISTORY_LIMIT } } }; if (outcome.isWinner) { update.$max = { "games.poker.largestPotWon": outcome.potSize }; } const write = await stats.updateOne( { userId, guildId, "games.poker.processedHands": { $ne: handId } }, update ); if ( write.modifiedCount !== 1 ) { await repairProfileBestStreaks( userId, guildId ); return { processed: false, userId, handId, outcome }; } await repairProfileBestStreaks( userId, guildId ); try { await trackGameActivity( userId, guildId ); } catch (error) { console.error( `[PokerProfile] Activity tracking failed for ${userId}:`, error ); } return { processed: true, userId, handId, outcome }; } async function recordPokerHandProfiles( game ) { const hand = game?.data?.hand; if ( !hand || hand.state !== "PAYOUT" || !hand.result || !Array.isArray( hand.players ) ) { return []; } const results = []; for ( const handPlayer of hand.players ) { try { results.push( await recordPokerHandForPlayer( game, handPlayer ) ); } catch (error) { console.error( `[PokerProfile] Failed to record hand ${getPokerHandId(game) ?? "unknown"} for ${handPlayer.userId}:`, error ); results.push({ processed: false, userId: String( handPlayer.userId ), error }); } } return results; } async function claimPokerBadge( context, userId, badgeId, metadata = null ) { const guildId = context?.guildId; if (!guildId) { return false; } const { badges } = getProfileCollections(); const set = { ownerId: String(userId), unlockedAt: getUnixTime() }; if ( metadata && typeof metadata === "object" ) { set.metadata = metadata; } const badge = await badges.findOneAndUpdate( { guildId: String(guildId), badgeId, ownerId: null }, { $set: set }, { returnDocument: "after" } ); if ( !badge || badge.ownerId !== String(userId) ) { return false; } if ( !context.channel ) { return true; } try { await context.channel.send({ embeds: [ new EmbedBuilder() .setTitle( "🏅 Unique Badge Claimed!" ) .setDescription( [ `${badge.emoji ?? "♠️"} **${badge.name ?? badge.badgeId}**`, "", `<@${userId}> claimed a guild-unique Poker badge!`, "", badge.description ?? "A unique Poker badge has been claimed.", "", "*This badge can no longer be earned by another player.*" ].join("\n") ) .setFooter({ text: `badge:${badge.badgeId}` }) .setTimestamp() ] }); } catch (error) { console.error( `[PokerBadge] Failed to announce ${badgeId} for ${userId}:`, error ); } return true; } async function evaluatePokerBadges( context, userId, outcome ) { const { stats } = getProfileCollections(); const document = await stats.findOne({ userId: String(userId), guildId: String(context.guildId) }); const poker = document?.games?.poker ?? {}; if ( Number( poker.won ?? 0 ) >= 1 ) { await claimPokerBadge( context, userId, "first_poker_win" ); } if ( Number( poker.allInWins ?? 0 ) >= 1 ) { await claimPokerBadge( context, userId, "first_poker_allin_win" ); } if ( Number( poker.pocketAcesWins ?? 0 ) >= 1 ) { await claimPokerBadge( context, userId, "first_poker_pocket_aces_win" ); } if ( Number( poker.handRanks ?.royalFlush ?? 0 ) >= 1 ) { await claimPokerBadge( context, userId, "first_poker_royal_flush" ); } if ( Number( poker.fullTableWins ?? 0 ) >= 1 ) { await claimPokerBadge( context, userId, "first_poker_full_table_win" ); } if ( Number( poker.largestPotWon ?? 0 ) >= 5000 ) { await claimPokerBadge( context, userId, "first_poker_5000_pot", { pot: outcome?.potSize ?? poker.largestPotWon } ); } } async function claimPokerAnnouncement( game, userId ) { const handId = getPokerHandId( game ); if (!handId) { return false; } const { stats } = getProfileCollections(); const result = await stats.updateOne( { userId: String(userId), guildId: String(game.guildId), "games.poker.announcedHands": { $ne: handId } }, { $push: { "games.poker.announcedHands": { $each: [ handId ], $slice: -PROFILE_HAND_HISTORY_LIMIT } } } ); return ( result.modifiedCount === 1 ); } async function announcePokerHandProgress( context, game ) { if ( !context?.guildId || !game?.data?.hand || game.data.hand.state !== "PAYOUT" || !Array.isArray( game.data.hand.players ) ) { return []; } const results = []; for ( const handPlayer of game.data.hand.players ) { const userId = String( handPlayer.userId ); try { const claimed = await claimPokerAnnouncement( game, userId ); if (!claimed) { continue; } const outcome = getPlayerOutcome( game, handPlayer ); await trackProfileEvent( context, userId, { type: "profile_refresh", source: "poker", game: POKER_GAME_KEY, result: outcome.resultName, profit: outcome.profit, allIn: outcome.allIn, handRank: outcome.handRank?.name ?? null, pocketAces: outcome.pocketAces, sevenDeuce: outcome.sevenDeuce, foldWin: outcome.foldWin, fullTableWin: outcome.fullTableWin, pot: outcome.potSize } ); await evaluatePokerBadges( context, userId, outcome ); results.push({ userId, announced: true }); } catch (error) { console.error( `[PokerProfile] Failed to refresh achievements/badges for ${userId}:`, error ); results.push({ userId, announced: false, error }); } } return results; } async function createPokerProfileContext( client, game ) { if ( !client || !game?.guildId ) { return null; } let channel = null; if ( game.channelId ) { try { channel = await client.channels.fetch( game.channelId ); } catch {} } return { guildId: String(game.guildId), channel, replied: false, deferred: false }; } module.exports = { ensurePokerProfileDocument, getPokerHandId, getPokerActionId, getPlayerOutcome, recordPokerActionProfile, recordPokerHandProfiles, announcePokerHandProgress, createPokerProfileContext, claimPokerBadge, evaluatePokerBadges };