use crate::config::{Channel, GlobalData}; use crate::error::Error; use crate::user::{User, UserError}; use crate::{command, group}; use rand::seq::IteratorRandom; use rand::{thread_rng, Rng}; use serenity::builder::EditMessage; 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::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(UserError), 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: UserError) -> Self { Self::BetFundError(value) } } impl std::error::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.get() & 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(); User::try_take_funds(&global.db, bet.author, bet.amount).map_err(|err| match err { Error::UserError(e) => RaceError::BetFundError(e), _ => panic!("Recv'ed error when trying to bet: {}", err), })?; 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_id.unwrap(); let channels = guild.channels(&ctx.http).await?; let race_channel = channels.get(&msg.channel_id).unwrap(); let emojis = guild.emojis(&ctx.http).await.unwrap(); let racers: Vec<_> = 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 race_msg = recv.recv().await.unwrap(); match race_msg { RaceMessage::StartRace => { race_channel.say(&ctx, "And they're off!").await?; break; } RaceMessage::Bet(bet) => { let emoji = match msg.guild_id.unwrap().emoji(&ctx.http, bet.emoji).await { Ok(emoji) => emoji, Err(_) => { race_channel .say(&ctx, "That's a weird looking racer, pick a normal one!") .await?; continue; } }; 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!", emoji) } 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, emoji, ), ) .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, EditMessage::new().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(""); User::give_funds(&global_data.db, winner, payout as u32)?; } } } 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 = match args.parse::() { Ok(emoji) => emoji, Err(e) => { msg.reply(&ctx.http, format!("Invalid emoji: {}", e)) .await?; return Ok(()); } }; args.advance(); let amount = match args.parse::() { Ok(amount) => amount, Err(err) => { msg.reply(&ctx.http, format!("Invalid bet amount: {}", err)) .await?; return Ok(()); } }; let send = race_msg_channel.send.lock().await; send.send(RaceMessage::Bet(Bet { emoji: racer.id, amount, author: msg.author.id, })) .await?; Ok(()) }