add blackjack
This commit is contained in:
310
src/commands/gambling/blackjack.rs
Normal file
310
src/commands/gambling/blackjack.rs
Normal 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(())
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -92,7 +92,6 @@ pub async fn wager(
|
||||
}
|
||||
|
||||
super::change_balance(ctx.author().id, balance, &mut *tx).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -34,6 +34,7 @@ pub fn commands() -> Vec<Command<Data, Error>> {
|
||||
gambling::daily::daily(),
|
||||
gambling::leaderboard::leaderboard(),
|
||||
gambling::shop::buy(),
|
||||
gambling::blackjack::blackjack(),
|
||||
eval::eval(),
|
||||
self_roles::role(),
|
||||
settings::setting(),
|
||||
|
||||
Reference in New Issue
Block a user