add blackjack

This commit is contained in:
2025-04-01 22:43:56 -04:00
parent 68221cab92
commit 652458d885
4 changed files with 312 additions and 1 deletions

View File

@@ -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<Item = Self> {
[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<Item = Self> {
(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<Item = Card> {
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::<f64>() {
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<Card> = vec![deck.pop().unwrap(), deck.pop().unwrap()];
let mut players_hand: Vec<Card> = 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::<Vec<String>>().join(", "),
dealers_count,
players_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().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::<Vec<String>>().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::<Vec<_>>();
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::<Vec<String>>().join(", "),
dealers_count,
players_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().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(())
}

View File

@@ -5,6 +5,7 @@ pub mod wager;
pub mod daily; pub mod daily;
pub mod leaderboard; pub mod leaderboard;
pub mod shop; pub mod shop;
pub mod blackjack;
use crate::{inventory::{self, Inventory}, common::{Context, Error}}; use crate::{inventory::{self, Inventory}, common::{Context, Error}};
use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId}; use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId};

View File

@@ -92,7 +92,6 @@ pub async fn wager(
} }
super::change_balance(ctx.author().id, balance, &mut *tx).await?; super::change_balance(ctx.author().id, balance, &mut *tx).await?;
tx.commit().await?; tx.commit().await?;
Ok(()) Ok(())

View File

@@ -34,6 +34,7 @@ pub fn commands() -> Vec<Command<Data, Error>> {
gambling::daily::daily(), gambling::daily::daily(),
gambling::leaderboard::leaderboard(), gambling::leaderboard::leaderboard(),
gambling::shop::buy(), gambling::shop::buy(),
gambling::blackjack::blackjack(),
eval::eval(), eval::eval(),
self_roles::role(), self_roles::role(),
settings::setting(), settings::setting(),