From 65ea879ffa0ffd5c224fc2873c53f54678f7e773 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sat, 24 Dec 2022 13:26:56 -0600 Subject: [PATCH] Added races + fmt + clippy --- src/config.rs | 38 ++-- src/discord/emoji_race.rs | 362 ++++++++++++++++++++++++++++++++++++++ src/discord/mod.rs | 8 +- src/discord/story.rs | 14 +- src/main.rs | 11 +- src/wallet/mod.rs | 30 ++-- 6 files changed, 414 insertions(+), 49 deletions(-) create mode 100644 src/discord/emoji_race.rs diff --git a/src/config.rs b/src/config.rs index 655c8a3..40e3733 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,8 +10,10 @@ use serenity::model::prelude::UserId; use serenity::prelude::TypeMapKey; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::Arc; use structopt::StructOpt; use tera::Tera; +use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::Mutex; #[derive(Debug, StructOpt)] @@ -168,29 +170,23 @@ impl TypeMapKey for GlobalData { type Value = GlobalData; } -#[derive(Debug)] -pub struct StoryRecv { - pub recv: tokio::sync::mpsc::Receiver, +#[derive(Debug, Clone)] +pub struct Channel { + pub recv: Arc>>, + pub send: Arc>>, } -impl TypeMapKey for StoryRecv { - type Value = Mutex; +impl Channel { + pub fn new() -> Self { + let (send, recv) = channel::(10); + + Self { + recv: Arc::new(Mutex::new(recv)), + send: Arc::new(Mutex::new(send)), + } + } } -#[derive(Debug)] -pub struct StorySend { - pub send: tokio::sync::mpsc::Sender, -} - -impl TypeMapKey for StorySend { - type Value = Mutex; -} - -pub fn create_story_channel() -> (Mutex, Mutex) { - let (send, recv) = tokio::sync::mpsc::channel::(10); - - ( - Mutex::new(StorySend { send }), - Mutex::new(StoryRecv { recv }), - ) +impl TypeMapKey for Channel { + type Value = Channel; } diff --git a/src/discord/emoji_race.rs b/src/discord/emoji_race.rs new file mode 100644 index 0000000..53f2b7e --- /dev/null +++ b/src/discord/emoji_race.rs @@ -0,0 +1,362 @@ +use crate::config::{Channel, GlobalData}; +use crate::wallet::WalletError; +use crate::{command, group}; +use rand::seq::IteratorRandom; +use rand::{thread_rng, Rng}; +use serenity::client::Context; +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::id::{EmojiId, UserId}; +use serenity::model::misc::EmojiIdentifier; +use serenity::model::prelude::{Emoji, Message}; +use serenity::prelude::Mentionable; +use serenity::utils::MessageBuilder; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::time::Duration; + +const RACE_SIZE: usize = 25; +const BASE_SPEED: f32 = 0.5; +const NUMBER_OF_RACERS: usize = 5; + +#[group] +#[commands(race, start_race, bet)] +pub struct EmojiRace; + +#[derive(Clone, Debug)] +pub enum RaceError { + RacerNotFound, + BetFundError(WalletError), + UserHasBet, +} + +impl Display for RaceError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RaceError::RacerNotFound => write!(f, "Racer not found"), + RaceError::BetFundError(e) => write!(f, "Fund error: {}", e), + RaceError::UserHasBet => write!(f, "User already bet"), + } + } +} + +impl From for RaceError { + fn from(value: WalletError) -> Self { + Self::BetFundError(value) + } +} + +impl Error for RaceError {} + +#[derive(Clone, Debug)] +pub enum RaceMessage { + StartRace, + Bet(Bet), +} + +#[derive(Clone, Debug)] +pub struct Bet { + pub emoji: EmojiId, + pub amount: u32, + pub author: UserId, +} + +#[derive(Clone, Debug)] +struct Racer { + pub emoji: Emoji, + pub speed: f32, + pub pos: f32, +} + +impl PartialEq for Racer { + fn eq(&self, other: &Self) -> bool { + self.emoji.id == other.emoji.id + } +} + +impl Eq for Racer {} + +impl Racer { + pub fn new(emoji: Emoji) -> Self { + let genetic_stat = (emoji.id.0 & 0xff) as f32 / 255.0; + let random_stat = thread_rng().gen_range(0.0..0.5); + let speed = BASE_SPEED + (0.50 * genetic_stat) + (0.50 * random_stat); + + Racer { + emoji, + speed, + pos: 0.0, + } + } + + pub fn update_pos(&mut self) { + let speed_boost = thread_rng().gen_range(-0.75..0.75); + self.pos += self.speed + (self.speed * speed_boost); + + self.pos = self.pos.clamp(0.0, RACE_SIZE as f32); + } + + pub fn get_race_pos(&self) -> usize { + self.pos.floor() as usize + } +} + +fn draw_race(racers: &Vec) -> String { + let mut msg = MessageBuilder::default(); + + for racer in racers { + let mut race_line = MessageBuilder::new(); + race_line.push("|"); + race_line.push("=".repeat(racer.get_race_pos())); + race_line.push("E"); + + if race_line.0.len() < RACE_SIZE { + let padding_needed = RACE_SIZE - race_line.0.len() - 1; + race_line.push("=".repeat(padding_needed)); + race_line.push("🔳"); + } + + let line = race_line.build(); + let line = line.replace('E', &racer.emoji.to_string()); + msg.push_line(line); + } + + msg.build() +} + +async fn add_bet( + ctx: &Context, + bet: Bet, + bets: &mut Vec, + racers: &[Racer], +) -> Result<(), RaceError> { + let mut data = ctx.data.write().await; + + let global = data.get_mut::().unwrap(); + + global + .cfg + .wallet_manager + .try_take_funds(bet.author, bet.amount)?; + + if !racers.iter().any(|r| r.emoji.id == bet.emoji) { + return Err(RaceError::RacerNotFound); + } + + if bets.iter().any(|b| b.author == bet.author) { + return Err(RaceError::UserHasBet); + } + + bets.push(bet); + + Ok(()) +} + +#[command] +#[description("Create a race")] +#[only_in(guilds)] +async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let race_msg_channel = { + let data = ctx.data.read().await; + + data.get::>().unwrap().clone() + }; + + let mut recv = match race_msg_channel.recv.try_lock() { + Ok(recv) => recv, + Err(_) => { + msg.reply(&ctx.http, "Race in progress").await?; + return Ok(()); + } + }; + + let guild = msg.guild(&ctx.cache).unwrap(); + let channels = guild.channels(&ctx.http).await?; + let race_channel = channels.get(&msg.channel_id).unwrap(); + + let racers: Vec<_> = guild + .emojis + .iter() + .choose_multiple(&mut thread_rng(), NUMBER_OF_RACERS); + + let mut racers: Vec = racers + .iter() + .map(|(_, e)| Racer::new((*e).clone())) + .collect(); + + let mut racers_msg = MessageBuilder::new(); + racers_msg.push_bold_line("Welcome to the Emoji Races, the racers today are:"); + for racer in &racers { + racers_msg.push_line(format!("* {}", racer.emoji)); + } + racers_msg.push_line("Place your bets using !bet. And start the race with !start_race"); + race_channel.say(&ctx.http, racers_msg.build()).await?; + + let mut bets: Vec = Vec::new(); + loop { + let msg = recv.recv().await.unwrap(); + + match msg { + RaceMessage::StartRace => { + race_channel.say(&ctx, "And they're off!").await?; + break; + } + RaceMessage::Bet(bet) => { + if let Err(e) = add_bet(ctx, bet.clone(), &mut bets, &racers).await { + let msg = match e { + RaceError::RacerNotFound => { + format!("{} is not in this race!", bet.emoji.mention()) + } + RaceError::BetFundError(_) => { + "Only rich people can bet in this race, sorry".to_string() + } + RaceError::UserHasBet => { + "You can't bet more than once in this race, that's against the law and you are going to jail!".to_string() + } + }; + + race_channel + .say(&ctx.http, format!("{} {}", bet.author.mention(), msg)) + .await?; + } else { + race_channel + .say( + &ctx.http, + format!( + "{} has put {} on {}", + bet.author.mention(), + bet.amount, + bet.emoji.mention() + ), + ) + .await?; + } + } + } + } + + let mut race_msg = race_channel.say(&ctx.http, draw_race(&racers)).await?; + let mut final_order: Vec = Vec::new(); + while final_order.len() < NUMBER_OF_RACERS { + tokio::time::sleep(Duration::from_millis(500)).await; + for racer in &mut racers { + racer.update_pos(); + } + + race_msg + .edit(&ctx.http, |m| m.content(draw_race(&racers))) + .await?; + + for racer in &racers { + if racer.pos >= (RACE_SIZE as f32) && !final_order.contains(racer) { + final_order.push(racer.clone()) + } + } + } + + let bet_winners: Vec = bets + .iter() + .filter_map(|b| { + if final_order.first().unwrap().emoji.id == b.emoji { + Some(b.author) + } else { + None + } + }) + .collect(); + + let mut winner_msg = MessageBuilder::new(); + + winner_msg.push_bold_line("The final outcome: "); + for (pos, racer) in final_order.iter().enumerate() { + winner_msg.push(format!("{}. ", pos + 1)); + winner_msg.emoji(&racer.emoji); + winner_msg.push_line(""); + } + + let mut payout: i64 = 0; + + for bet in bets { + payout += bet.amount as i64; + } + + if bet_winners.is_empty() { + winner_msg.push_line("Sadly there were no bet winners, better luck next time!"); + } else { + payout /= bet_winners.len() as i64; + + winner_msg.push_bold_line(format!( + "The following winner(s) got a payout of {}:", + payout + )); + { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + for winner in bet_winners { + winner_msg.push("* "); + winner_msg.mention(&winner); + winner_msg.push_line(""); + + global_data + .cfg + .wallet_manager + .get_user_wallet(winner) + .coin_count += payout; + } + } + } + + race_channel.say(&ctx.http, winner_msg.build()).await?; + + Ok(()) +} + +fn check_if_race_in_progress(race_channel: &Channel) -> bool { + race_channel.recv.try_lock().is_err() +} + +#[command] +#[description("Start a race")] +#[only_in(guilds)] +async fn start_race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let data = ctx.data.read().await; + let race_msg_channel = data.get::>().unwrap(); + + if !check_if_race_in_progress(race_msg_channel) { + msg.reply(&ctx.http, "Race not in progress").await?; + } + + let send = race_msg_channel.send.lock().await; + + send.send(RaceMessage::StartRace).await?; + + Ok(()) +} + +#[command] +#[description("Bet on a race")] +#[usage(" ")] +#[usage(":) 69")] +#[only_in(guilds)] +async fn bet(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + + let race_msg_channel = data.get::>().unwrap(); + if !check_if_race_in_progress(race_msg_channel) { + msg.reply(&ctx.http, "Race not in progress").await?; + } + + let racer = args.parse::()?; + args.advance(); + let amount = args.parse::()?; + + let send = race_msg_channel.send.lock().await; + + send.send(RaceMessage::Bet(Bet { + emoji: racer.id, + amount, + author: msg.author.id, + })) + .await?; + + Ok(()) +} diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 615cbbb..a89a6d6 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -2,6 +2,7 @@ pub mod admin; pub mod album; pub mod celeryman; pub mod color; +pub mod emoji_race; pub mod fren_coin; pub mod joke; pub mod story; @@ -102,7 +103,7 @@ impl EventHandler for Handler { new_message.react(&ctx.http, '😭').await.unwrap(); } - give_coin(&ctx, new_message.author.id, 0.05, 5) + give_coin(&ctx, new_message.author.id, 0.05, 10) .await .unwrap(); } @@ -122,10 +123,9 @@ pub async fn after( match command_result { Ok(()) => { println!("Processed command '{}'", command_name); - give_coin(ctx, msg.author.id, 0.10, 10).await.unwrap(); - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); global_data .cfg .save(&global_data.args.cfg_path) diff --git a/src/discord/story.rs b/src/discord/story.rs index 3424a89..9fe8da4 100644 --- a/src/discord/story.rs +++ b/src/discord/story.rs @@ -1,4 +1,4 @@ -use crate::config::{BotConfig, StoryRecv, StorySend}; +use crate::config::{BotConfig, Channel}; use crate::{command, group, GlobalData}; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -87,9 +87,9 @@ async fn story(ctx: &Context, msg: &Message, args: Args) -> CommandResult { } }; - let story_recv = data.get::().unwrap(); + let story_channel = data.get::>().unwrap(); - let mut story_recv = story_recv.lock().await; + let mut story_recv = story_channel.recv.lock().await; let stories = get_all_stories(&global_data.cfg).await; @@ -133,7 +133,7 @@ async fn story(ctx: &Context, msg: &Message, args: Args) -> CommandResult { .say(&ctx.http, format!("Give me {}", global)) .await?; - story_globals.insert(global, story_recv.recv.recv().await.unwrap()); + story_globals.insert(global, story_recv.recv().await.unwrap()); } for (prompt, response) in story_globals { @@ -169,13 +169,13 @@ async fn word(ctx: &Context, msg: &Message, args: Args) -> CommandResult { return Ok(()); } - let story_send = data.get::().unwrap(); + let story_channel = data.get::>().unwrap(); - let story_send = story_send.lock().await; + let story_send = story_channel.send.lock().await; let resp = MessageBuilder::default().push_safe(args.rest()).build(); - story_send.send.send(resp).await?; + story_send.send(resp).await?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 1f5a011..6c848ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,8 @@ mod error; mod imgur; mod wallet; -use crate::config::{create_story_channel, Args, BotConfig, GlobalData, StoryRecv, StorySend}; +use crate::config::{Args, BotConfig, Channel, GlobalData}; +use crate::discord::emoji_race::RaceMessage; use crate::discord::unrecognised_command_hook; use serenity::framework::standard::macros::{command, group, help, hook}; use serenity::framework::standard::StandardFramework; @@ -42,20 +43,22 @@ async fn main() { .group(&discord::admin::ADMIN_GROUP) .group(&discord::story::STORY_GROUP) .group(&discord::fren_coin::FRENCOIN_GROUP) + .group(&discord::emoji_race::EMOJIRACE_GROUP) .unrecognised_command(unrecognised_command_hook) .bucket("bad_apple", |b| b.delay(60 * 10)) .await .help(&discord::MY_HELP) .after(discord::after); - let (send, recv) = create_story_channel(); + let story_channel = Channel::::new(); + let race_channel = Channel::::new(); let intents = GatewayIntents::all(); let mut client = Client::builder(&global_data.cfg.bot_token, intents) .framework(framework) .type_map_insert::(global_data) - .type_map_insert::(send) - .type_map_insert::(recv) + .type_map_insert::>(story_channel) + .type_map_insert::>(race_channel) .event_handler(discord::Handler) .await .expect("Unable to create client."); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 4ddef1f..413ee04 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -49,20 +49,24 @@ impl WalletManager { dest: UserId, amount: u32, ) -> Result<(), WalletError> { - { - let src_wallet = self.get_user_wallet(src); - if src_wallet.coin_count < amount as i64 { - return Err(WalletError::NotEnoughFunds); - } - src_wallet.coin_count -= amount as i64; - } - - { - let dest_wallet = self.get_user_wallet(dest); - - dest_wallet.coin_count += amount as i64; - } + self.try_take_funds(src, amount)?; + self.give_funds(dest, amount); Ok(()) } + + pub fn give_funds(&mut self, discord_id: UserId, amount: u32) { + let mut wallet = self.get_user_wallet(discord_id); + wallet.coin_count += amount as i64; + } + + pub fn try_take_funds(&mut self, discord_id: UserId, amount: u32) -> Result<(), WalletError> { + let mut wallet = self.get_user_wallet(discord_id); + if wallet.coin_count < amount as i64 { + Err(WalletError::NotEnoughFunds) + } else { + wallet.coin_count -= amount as i64; + Ok(()) + } + } }