From 03c3fc195465bbd36556179cdbb827854b76f7ff Mon Sep 17 00:00:00 2001 From: Dragon Fire Date: Mon, 11 May 2020 12:09:06 -0400 Subject: [PATCH] Poker Command --- .gitignore | 1 - README.md | 3 +- commands/games-mp/poker.js | 267 +++++++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 commands/games-mp/poker.js diff --git a/.gitignore b/.gitignore index 323c2731..2047fa2f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ report*.json config.json # In-Development Commands -commands/games-mp/poker.js diff --git a/README.md b/README.md index 393e026f..6b781800 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ in the appropriate channel's topic to use it. ## Commands -Total: 416 +Total: 417 ### Utility: @@ -407,6 +407,7 @@ Total: 416 * **gunfight:** Engage in a western gunfight against another user. High noon. * **lie-swatter:** Players are given a fact and must quickly decide if it's True or a Lie. * **pick-a-number:** Two players pick a number between 1 and 10. Whoever's closer wins. +* **poker:** Play poker with up to 3 other users. * **quiz-duel:** Answer a series of quiz questions against an opponent. * **russian-roulette:** Who will pull the trigger and die first? * **tic-tac-toe:** Play a game of tic-tac-toe with another user. diff --git a/commands/games-mp/poker.js b/commands/games-mp/poker.js new file mode 100644 index 00000000..3fddc136 --- /dev/null +++ b/commands/games-mp/poker.js @@ -0,0 +1,267 @@ +const Command = require('../../structures/Command'); +const Collection = require('@discordjs/collection'); +const { Hand } = require('pokersolver'); +const { stripIndents } = require('common-tags'); +const Deck = require('../../structures/cards/Deck'); +const { formatNumber, list, delay } = require('../../util/Util'); +const { SUCCESS_EMOJI_ID } = process.env; +const max = 4; +const min = 2; +const bigBlindAmount = 100; +const smallBlindAmount = 50; +const raiseRegex = /raise (\$?([0-9]+)?,?[0-9]+)/i; + +module.exports = class PokerCommand extends Command { + constructor(client) { + super(client, { + name: 'poker', + aliases: ['texas-hold-em'], + group: 'games-mp', + memberName: 'poker', + description: 'Play poker with up to 3 other users.', + guildOnly: true, + args: [ + { + key: 'playersCount', + prompt: 'How many players are you expecting to have?', + type: 'integer', + min, + max + } + ] + }); + } + + async run(msg, { playersCount }) { + const current = this.client.games.get(msg.channel.id); + if (current) return msg.reply(`Please wait until the current game of \`${current.name}\` is finished.`); + this.client.games.set(msg.channel.id, { name: this.name, data: new Deck() }); + try { + const awaitedPlayers = await this.awaitPlayers(msg, playersCount); + if (!awaitedPlayers) { + this.client.games.delete(msg.channel.id); + return msg.say('Game could not be started...'); + } + const players = new Collection(); + for (const player of awaitedPlayers) { + players.set(player, { + money: 5000, + id: player, + hand: [], + user: this.client.users.cache.get(player), + currentBet: 0 + }); + } + const deck = this.client.games.get(msg.channel.id).data; + let winner = null; + const rotation = players.map(p => p.id); + while (!winner) { + const bigBlind = players.get(rotation[1]); + bigBlind.money -= bigBlindAmount; + bigBlind.currentBet += bigBlindAmount; + const smallBlind = players.get(rotation[2]); + smallBlind.money -= smallBlindAmount; + smallBlind.currentBet += smallBlindAmount; + rotation.push(rotation[0]); + rotation.shift(); + await msg.say('Dealing player hands...'); + for (const player of players.values()) { + player.hand.push(...deck.draw(2)); + try { + await player.user.send(stripIndents` + **Your Poker Hand:** + ${player.hand.map(c => c.textDisplay).join('\n')} + + **Money:** $${formatNumber(player.money)} + ${bigBlind.id === player.id ? '_You are the big blind._' : ''} + ${smallBlind.id === player.id ? '_You are the small blind._' : ''} + `); + } catch { + await msg.say(`${player.user}, I couldn't send your hand! Turn on DMs!`); + } + } + const data = { + pot: bigBlindAmount + smallBlindAmount, + currentBet: bigBlindAmount, + highestBetter: bigBlind + }; + let turnOver = false; + const turnRotation = rotation; + turnRotation.push(turnRotation[0], turnRotation[1]); + turnRotation.shift(); + turnRotation.shift(); + while (!turnOver) turnOver = await this.bettingRound(msg, players, turnRotation, data); + if (turnRotation.length === 1) { + const remainer = players.get(turnRotation[0]); + await msg.say(`${remainer.user} takes the pot.`); + remainer.money += data.pot; + continue; + } + const dealerHand = deck.draw(3); + await msg.say(stripIndents` + **Dealer Hand:** + ${dealerHand.map(card => card.textDisplay).join('\n')} + + _Next betting round begins in 5 seconds._ + `); + await delay(5000); + turnOver = false; + while (!turnOver) turnOver = await this.bettingRound(msg, players, turnRotation, data); + if (turnRotation.length === 1) { + const remainer = players.get(turnRotation[0]); + await msg.say(`${remainer.user} takes the pot.`); + remainer.money += data.pot; + continue; + } + dealerHand.push(deck.draw()); + await msg.say(stripIndents` + **Dealer Hand:** + ${dealerHand.map(card => card.textDisplay).join('\n')} + + _Next betting round begins in 5 seconds._ + `); + await delay(5000); + turnOver = false; + while (!turnOver) turnOver = await this.bettingRound(msg, players, turnRotation, data); + if (turnRotation.length === 1) { + const remainer = players.get(turnRotation[0]); + await msg.say(`${remainer.user} takes the pot.`); + remainer.money += data.pot; + continue; + } + dealerHand.push(deck.draw()); + await msg.say(stripIndents` + **Dealer Hand:** + ${dealerHand.map(card => card.textDisplay).join('\n')} + + _Next betting round begins in 5 seconds._ + `); + await delay(5000); + turnOver = false; + while (!turnOver) turnOver = await this.bettingRound(msg, players, turnRotation, data); + if (turnRotation.length === 1) { + const remainer = players.get(turnRotation[0]); + await msg.say(`${remainer.user} takes the pot.`); + remainer.money += data.pot; + continue; + } + const solved = []; + for (const playerID of turnRotation) { + const player = players.get(playerID); + const solvedHand = Hand.solve( + ...player.hand.map(card => card.pokersolverKey), + ...dealerHand.map(card => card.pokersolverKey) + ); + solvedHand.user = player; + solved.push(solvedHand); + } + const winners = Hand.winners(solved); + if (winners.length > 1) { + await msg.say(stripIndents` + The pot will be split between ${list(winners.map(w => `**${w.user.user}**`))}. + ${winners.map(winner.desc).join(', ')} + `); + const splitPot = data.pot / winners.length; + for (const win of winners) win.user.money += splitPot; + } else { + await msg.say(`${winners[0].user.user} takes the pot, with **${winners[0].desc}**.`); + winners[0].user.money += data.pot; + } + for (const player of players.values()) { + if (player.money <= 0) { + await msg.say(`${player.user} has been kicked.`); + players.delete(player.id); + } + } + if (players.size === 1) winner = players.first(); + } + this.client.games.delete(msg.channel.id); + return msg.say(`Congrats, ${winner.user}!`); + } catch (err) { + this.client.games.delete(msg.channel.id); + throw err; + } + } + + async awaitPlayers(msg, players) { + await msg.say(`You will need at least 1 more player (at max ${players - 1}). To join, type \`join game\`.`); + const joined = []; + joined.push(msg.author.id); + const filter = res => { + if (res.author.bot) return false; + if (joined.includes(res.author.id)) return false; + if (res.content.toLowerCase() !== 'join game') return false; + joined.push(res.author.id); + res.react(SUCCESS_EMOJI_ID || '✅').catch(() => null); + return true; + }; + const verify = await msg.channel.awaitMessages(filter, { max: players - 1, time: 30000 }); + verify.set(msg.id, msg); + if (verify.size < min) return false; + return verify.map(player => player.author.id); + } + + determineActions(turnPlayer, currentBet) { + const actions = []; + if (turnPlayer.currentBet !== currentBet) actions.push('fold'); + if (turnPlayer.money > currentBet) actions.push('raise '); + if (turnPlayer.money >= currentBet && turnPlayer.currentBet !== currentBet) actions.push('call'); + if (currentBet === turnPlayer.currentBet) actions.push('check'); + return actions; + } + + async bettingRound(msg, players, turnRotation, data) { + const turnPlayer = players.get(turnRotation[0]); + const actions = this.determineActions(turnPlayer, data.currentBet); + const displayActions = list(actions.map(action => `\`${action}\``), 'or'); + await msg.say(stripIndents` + **Pot: $${formatNumber(data.currentBet)}** + _Highest Bet: $${formatNumber(data.currentBet)} (${data.highestBetter.user.tag})_ + + ${turnPlayer.user}, what do you want to do? You can ${displayActions}. + `); + const filter = res => { + if (res.author.id !== turnPlayer.id) return false; + let choice = res.content.toLowerCase(); + if (actions.includes(choice) && !choice.startsWith('raise')) return true; + if (choice.startsWith('raise')) { + if (!raiseRegex.test(choice)) return false; + choice = choice.replace(/[$,]/g, ''); + const amount = Number.parseInt(choice.match(raiseRegex)[1], 10); + if (amount + data.currentBet > turnPlayer.money || amount < 1) return false; + return true; + } + return false; + }; + const msgs = await msg.channel.awaitMessages(filter, { max: 1, time: 30000 }); + let choiceAction; + if (!msgs.size) { + if (turnPlayer.currentBet !== data.currentBet) choiceAction = 'fold'; + else if (data.currentBet === turnPlayer.currentBet) choiceAction = 'check'; + else choiceAction = 'fold'; + } + choiceAction = msgs.first().content.toLowerCase().replace(/[$,]/g, ''); + const raiseValue = raiseRegex.test(choiceAction) ? choiceAction.match(raiseRegex)[1] : null; + if (raiseValue) { + data.currentBet += raiseValue; + data.pot += raiseValue + (data.currentBet - turnPlayer.currentBet); + data.highestBetter = turnPlayer; + turnPlayer.money -= raiseValue + (data.currentBet - turnPlayer.currentBet); + turnPlayer.currentBet += raiseValue + (data.currentBet - turnPlayer.currentBet); + await msg.say(`${turnPlayer.user} **raises $${formatNumber(raiseValue)}**.`); + } else if (choiceAction === 'call') { + turnPlayer.money -= data.currentBet; + turnPlayer.currentBet += data.currentBet; + data.pot += data.currentBet; + await msg.say(`${turnPlayer.user} **calls $${formatNumber(data.currentBet)}**.`); + } else if (choiceAction === 'fold') { + await msg.say(`${turnPlayer.user} **folds**.`); + } else if (choiceAction === 'check') { + await msg.say(`${turnPlayer.user} **checks**.`); + } + if (choiceAction !== 'fold') turnRotation.push(turnRotation[0]); + turnRotation.shift(); + return (data.highestBetter.id === turnPlayer.id && choiceAction === 'check') + || (data.highestBetter.currentBet === turnPlayer.currentBet && turnRotation[0] === data.highestBetter.id); + } +}; diff --git a/package.json b/package.json index 716cb966..a6b3e46a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "114.8.8", + "version": "114.9.0", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": {