diff --git a/src/commands/gambling/blackjack.rs b/src/commands/gambling/blackjack.rs new file mode 100644 index 0000000..b31cc10 --- /dev/null +++ b/src/commands/gambling/blackjack.rs @@ -0,0 +1,310 @@ +use crate::common::{Context, Error, Data}; +use std::{cmp::Ordering, fmt::Display, time::Duration}; +use poise::serenity_prelude::{self as serenity}; +use rand::seq::SliceRandom; + +#[derive(Clone)] +enum Suite { + Hearts, + Diamonds, + Clubs, + Spades, +} + +impl Display for Suite { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::Hearts => "\u{2665}", + Self::Diamonds => "\u{2666}", + Self::Clubs => "\u{2663}", + Self::Spades => "\u{2660}", + }) + } +} + +impl Suite { + fn suites() -> impl Iterator { + [Self::Hearts, Self::Diamonds, Self::Clubs, Self::Spades].iter().cloned() + } +} + +#[derive(Clone)] +enum Rank { + Pip(u8), + Jack, + King, + Queen, + Ace, +} + +impl Display for Rank { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pip(n) => write!(f, "{}", n), + Self::Jack => write!(f, "J"), + Self::King => write!(f, "K"), + Self::Queen => write!(f, "Q"), + Self::Ace => write!(f, "A"), + } + } +} + +impl Rank { + fn ranks() -> impl Iterator { + (2..=10).map(|n| Self::Pip(n)) + .chain(vec![Self::Jack, Self::King, Self::Queen, Self::Ace]) + } + + fn value(&self, ace_would_bust: bool) -> u8 { + match self { + Self::Ace if ace_would_bust => 1, + Self::Pip(n) => *n, + Self::Jack | Self::King | Self::Queen => 10, + Self::Ace => 11, + } + } +} + +#[derive(Clone)] +struct Card { + suite: Suite, + rank: Rank, +} + +impl Display for Card { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.suite, self.rank) + } +} + +impl Card { + fn new(suite: Suite, rank: Rank) -> Self { + Self { suite, rank } + } + + fn deck() -> impl Iterator { + let mut deck = vec![]; + + for rank in Rank::ranks() { + for suite in Suite::suites() { + deck.push(Card::new(suite, rank.clone())); + } + } + + deck.into_iter() + } + + fn value(&self, ace_would_bust: bool) -> u8 { + self.rank.value(ace_would_bust) + } +} + +#[derive(Debug, poise::Modal)] +struct BlackJackAction {} + +#[poise::command(slash_command)] +pub async fn modal(ctx: poise::ApplicationContext<'_, Data, Error>) -> Result<(), Error> { + use poise::Modal as _; + + let data = BlackJackAction::execute(ctx).await?; + println!("Got data: {:?}", data); + + Ok(()) +} + +/// Blackjack! +#[poise::command(slash_command, prefix_command, aliases("bj", "21"))] +pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error> +{ + let mut tx = ctx.data().database.begin().await?; + let mut balance = super::get_balance(ctx.author().id, &mut *tx).await?; + + let amount = match amount.to_lowercase().as_str() { + "all" => balance, + "half" => balance / 2, + input => { + if input.ends_with('%') { + let percent: f64 = match input[..input.len() - 1].parse::() { + Ok(x) => x, + Err(_) => { + ctx.reply(format!("{input} is not a valid percent.")).await?; + return Ok(()); + } + } / 100f64; + + (balance as f64 * percent) as i32 + } else { + match input.parse() { + Ok(n) => n, + Err(_) => { + ctx.reply("Any one of a number, all, half, or a percent are allowed as arguments.").await?; + return Ok(()); + } + } + } + } + }; + + if amount < 1 { + ctx.reply("You cannot wager less than 1 token.").await?; + return Ok(()); + } + + if balance < amount { + ctx.reply(format!("You do not have enough tokens (**{balance}**) to wager this amount.")).await?; + return Ok(()); + } + + let mut deck: Vec<_> = Card::deck() + .filter(|card| !matches!(card, Card { rank: Rank::Jack, .. })) + .collect(); + + deck.shuffle(&mut rand::thread_rng()); + + let dealers_hand: Vec = vec![deck.pop().unwrap(), deck.pop().unwrap()]; + let mut players_hand: Vec = vec![deck.pop().unwrap(), deck.pop().unwrap()]; + + let msg = ctx.reply("Just a second...").await?; + + loop { + let dealers_count = dealers_hand.iter().fold(0, |acc, card| acc + card.value(acc + 11 > 21)); + let players_count = players_hand.iter().fold(0, |acc, card| acc + card.value(acc + 11 > 21)); + + if players_count > 21 { + msg.edit(ctx, poise::CreateReply::default() + .components(vec![]) + .content(format!( + concat!( + "**Dealer's hand**: {} ({})\n", + "**Your hand**: {} ({})\n\n", + "**Bet**: {}\n", + "Bust! You've lost **{}** tokens." + ), + dealers_hand.iter().map(|card| format!("`{card}`")).collect::>().join(", "), + dealers_count, + players_hand.iter().map(|card| format!("`{card}`")).collect::>().join(", "), + players_count, + amount, amount + ))).await?; + + balance -= amount; + break; + } + + let reply = { + let components = vec![serenity::CreateActionRow::Buttons(vec![ + serenity::CreateButton::new("blackjack_hit") + .label("Hit") + .style(poise::serenity_prelude::ButtonStyle::Primary), + serenity::CreateButton::new("blackjack_hold") + .label("Hold") + .style(poise::serenity_prelude::ButtonStyle::Primary), + ])]; + + poise::CreateReply::default() + .content( + format!( + concat!( + "**Dealer's hand**: {} ({})\n", + "**Your hand**: {} ({})\n\n", + "**Bet**: {}" + ), + format!("{}, `XX`", dealers_hand[0]), + dealers_hand[0].value(matches!(dealers_hand[0], Card { rank: Rank::Ace, .. })), + players_hand.iter().map(|card| format!("`{card}`")).collect::>().join(", "), + players_count, + amount, + ) + ) + .components(components) + }; + + msg.edit(ctx, reply).await?; + + let Some(mci) = serenity::ComponentInteractionCollector::new(ctx.serenity_context()) + .timeout(Duration::from_secs(120)) + .filter(move |mci| mci.data.custom_id.starts_with("blackjack")).await else { + ctx.reply("failed interaction!").await?; + return Ok(()); + }; + + mci.create_response(ctx, serenity::CreateInteractionResponse::Acknowledge).await?; + + match &mci.data.custom_id[..] { + "blackjack_hit" => { + players_hand.push(deck.pop().unwrap()); + } + "blackjack_hold" => { + let dealers_hand = dealers_hand.into_iter() + .chain(deck.into_iter()) + .scan(0u8, |acc, card| { + if *acc >= 17 { + None + } else { + *acc += card.value(*acc + 11 > 21); + Some(card) + } + }).collect::>(); + + let dealers_count = dealers_hand.iter() + .fold(0, |acc, card| acc + card.value(acc + 11 > 21)); + + let s = match dealers_count.cmp(&players_count) { + Ordering::Less => { + if players_count == 21 { + let amount = amount * 3 / 2; + balance += amount * 3 / 2; + format!("You've won with a Blackjack! You've gained **{amount}** tokens.") + } else { + balance += amount; + format!("You've won! **{amount}** tokens have been added to your account.") + } + } + Ordering::Greater if dealers_count > 21 => { + balance += amount; + format!("You've won! **{amount}** tokens have been added to your account.") + } + Ordering::Equal => { + format!("A draw!") + } + Ordering::Greater => { + balance -= amount; + format!("You've lost. **{amount}** tokens to the dealer.") + } + }; + + super::change_balance(ctx.author().id, balance, &mut *tx).await?; + tx.commit().await?; + + msg.edit(ctx, poise::CreateReply::default() + .components(vec![]) + .content( + format!( + concat!( + "**Dealer's hand**: {} ({})\n", + "**Your hand**: {} ({})\n\n", + "**Bet**: {}\n", + "{}" + ), + dealers_hand.iter().map(|card| format!("`{card}`")).collect::>().join(", "), + dealers_count, + players_hand.iter().map(|card| format!("`{card}`")).collect::>().join(", "), + players_count, + amount, s + ) + )).await?; + + return Ok(()); + } + _ => { + ctx.reply("Invalid interaction response.").await?; + return Ok(()); + }, + } + } + + super::change_balance(ctx.author().id, balance, &mut *tx).await?; + tx.commit().await?; + + Ok(()) +} diff --git a/src/commands/gambling/mod.rs b/src/commands/gambling/mod.rs index 4418afd..dc09d4e 100644 --- a/src/commands/gambling/mod.rs +++ b/src/commands/gambling/mod.rs @@ -5,6 +5,7 @@ pub mod wager; pub mod daily; pub mod leaderboard; pub mod shop; +pub mod blackjack; use crate::{inventory::{self, Inventory}, common::{Context, Error}}; use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId}; diff --git a/src/commands/gambling/wager.rs b/src/commands/gambling/wager.rs index 2234929..8e43c6f 100644 --- a/src/commands/gambling/wager.rs +++ b/src/commands/gambling/wager.rs @@ -92,7 +92,6 @@ pub async fn wager( } super::change_balance(ctx.author().id, balance, &mut *tx).await?; - tx.commit().await?; Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5cc61fe..1fec11d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -34,6 +34,7 @@ pub fn commands() -> Vec> { gambling::daily::daily(), gambling::leaderboard::leaderboard(), gambling::shop::buy(), + gambling::blackjack::blackjack(), eval::eval(), self_roles::role(), settings::setting(),