diff --git a/src/config.rs b/src/config.rs index 31e72a1..0646b17 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,9 +11,7 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -use std::sync::Arc; use structopt::StructOpt; -use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::Mutex; #[derive(Debug, StructOpt)] @@ -109,24 +107,3 @@ impl GlobalData { impl TypeMapKey for GlobalData { type Value = GlobalData; } - -#[derive(Debug, Clone)] -pub struct Channel { - pub recv: Arc>>, - pub send: Arc>>, -} - -impl Channel { - pub fn new() -> Self { - let (send, recv) = channel::(10); - - Self { - recv: Arc::new(Mutex::new(recv)), - send: Arc::new(Mutex::new(send)), - } - } -} - -impl TypeMapKey for Channel { - type Value = Channel; -} diff --git a/src/discord/color.rs b/src/discord/color.rs index 5d1de3a..4362524 100644 --- a/src/discord/color.rs +++ b/src/discord/color.rs @@ -3,7 +3,7 @@ use crate::error::Error; use poise::serenity_prelude::builder::EditRole; use poise::serenity_prelude::model::Colour; -#[poise::command(prefix_command, guild_only)] +#[poise::command(prefix_command, guild_only, category = "Color")] pub async fn set_color( ctx: Context<'_>, #[description = "Color you want your role to be"] @@ -77,7 +77,7 @@ pub async fn set_color( Ok(()) } -#[poise::command(prefix_command, guild_only)] +#[poise::command(prefix_command, guild_only, category = "Color")] pub async fn remove_color(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let member = ctx.author_member().await.unwrap(); diff --git a/src/discord/emoji_race.rs b/src/discord/emoji_race.rs index 0367588..b107118 100644 --- a/src/discord/emoji_race.rs +++ b/src/discord/emoji_race.rs @@ -1,140 +1,27 @@ -use crate::config::{Channel, GlobalData}; +use crate::discord::Context; use crate::error::Error; -use crate::user::{User, UserError}; -use crate::{command, group}; +use crate::models::race; +use crate::models::race::{Bet, RaceError, Racer, NUMBER_OF_RACERS, RACE_SIZE}; +use crate::user::User; +use j_db::database::Database; +use poise::serenity_prelude::builder::EditMessage; +use poise::serenity_prelude::model::id::UserId; +use poise::serenity_prelude::utils::MessageBuilder; +use poise::serenity_prelude::{EmojiIdentifier, Mentionable}; 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 rand::{rng, Rng}; use std::time::Duration; -const RACE_SIZE: usize = 25; -const BASE_SPEED: f32 = 0.5; -const NUMBER_OF_RACERS: usize = 5; +fn cleanup_race(db: &Database) -> Result<(), Error> { + log::info!("Cleaning up race state"); + Racer::clear_racers(db)?; + Bet::clear_bet(db)?; -#[group] -#[commands(race, start_race, bet)] -pub struct EmojiRace; - -#[derive(Clone, Debug)] -pub enum RaceError { - RacerNotFound, - BetFundError(UserError), - UserHasBet, + Ok(()) } -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 { +async fn add_bet(ctx: Context<'_>, bet: Bet, racers: &[Racer]) -> Result<(), RaceError> { + User::try_take_funds(&ctx.data().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), })?; @@ -143,44 +30,28 @@ async fn add_bet( return Err(RaceError::RacerNotFound); } - if bets.iter().any(|b| b.author == bet.author) { + if Bet::has_user_bet(&ctx.data().db, bet.author)? { return Err(RaceError::UserHasBet); } - bets.push(bet); + Bet::add_bet(&ctx.data().db, 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; +#[poise::command(prefix_command, guild_only, category = "Race")] +pub async fn race(ctx: Context<'_>) -> Result<(), Error> { + cleanup_race(&ctx.data().db)?; - data.get::>().unwrap().clone() - }; + let emojis = ctx.guild_id().unwrap().emojis(ctx.http()).await?; - 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 racers: Vec<_> = emojis.iter().choose_multiple(&mut rng(), NUMBER_OF_RACERS); - 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 + let racers: Result, Error> = racers .iter() - .choose_multiple(&mut thread_rng(), NUMBER_OF_RACERS); - - let mut racers: Vec = racers.iter().map(|e| Racer::new((*e).clone())).collect(); + .map(|e| Racer::add_racer(&ctx.data().db, (*e).clone())) + .collect(); + let racers = racers?; let mut racers_msg = MessageBuilder::new(); racers_msg.push_bold_line("Welcome to the Emoji Races, the racers today are:"); @@ -188,62 +59,26 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { 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?; + ctx.say(racers_msg.build()).await?; - let mut bets: Vec = Vec::new(); - loop { - let race_msg = recv.recv().await.unwrap(); + Ok(()) +} - 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; - } - }; +#[poise::command(prefix_command, guild_only, category = "Race")] +pub async fn start_race(ctx: Context<'_>) -> Result<(), Error> { + let mut racers = Racer::get_racers(&ctx.data().db)?; - 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?; - } - } - } + if racers.is_empty() { + ctx.reply("Woops, no race is in progress. Use !race to kick this bad boy off") + .await?; + return Ok(()); } - let mut race_msg = race_channel.say(&ctx.http, draw_race(&racers)).await?; + let race_channel = ctx.channel_id(); + + let mut race_msg = race_channel + .say(&ctx.http(), race::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; @@ -252,7 +87,10 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { } race_msg - .edit(&ctx.http, EditMessage::new().content(draw_race(&racers))) + .edit( + ctx.http(), + EditMessage::new().content(race::draw_race(&racers)), + ) .await?; for racer in &racers { @@ -262,6 +100,8 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { } } + let bets = Bet::get_bets(&ctx.data().db)?; + let bet_winners: Vec = bets .iter() .filter_map(|b| { @@ -297,85 +137,50 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { "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)?; - } + for winner in bet_winners { + winner_msg.push("* "); + winner_msg.mention(&winner); + winner_msg.push_line(""); + + User::give_funds(&ctx.data().db, winner, payout as u32)?; } } - race_channel.say(&ctx.http, winner_msg.build()).await?; + race_channel.say(ctx.http(), winner_msg.build()).await?; + + cleanup_race(&ctx.data().db)?; Ok(()) } -fn check_if_race_in_progress(race_channel: &Channel) -> bool { - race_channel.recv.try_lock().is_err() -} +#[poise::command(prefix_command, guild_only, category = "Race")] +pub async fn bet( + ctx: Context<'_>, + #[description = "Racer to bet on"] racer: EmojiIdentifier, + #[description = "Bet amount"] bet_amount: u32, +) -> Result<(), Error> { + let racers = Racer::get_racers(&ctx.data().db)?; -#[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?; + if racers.is_empty() { + ctx.reply("No races are in progress use, !race to kick it off!") + .await?; + return Ok(()); } - let send = race_msg_channel.send.lock().await; + let bet = Bet::new(racer.id, bet_amount, ctx.author().id); - 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 resp = match add_bet(ctx, bet, &racers).await { + Ok(_) => format!( + "{} has placed {} Fren Coins on {}", + ctx.author().mention(), + bet_amount, + racer + ), + Err(err) => format!("Yeah something's fucked. {}", err), }; - let send = race_msg_channel.send.lock().await; - - send.send(RaceMessage::Bet(Bet { - emoji: racer.id, - amount, - author: msg.author.id, - })) - .await?; + ctx.reply(resp).await?; Ok(()) } diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 7e4e4f5..aea5efd 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -3,6 +3,7 @@ mod album; mod birthday; mod celeryman; mod color; +mod emoji_race; use crate::config::GlobalData; use crate::error::Error; @@ -121,6 +122,9 @@ pub async fn run_bot(global_data: GlobalData) { celeryman::tayne(), color::set_color(), color::remove_color(), + emoji_race::bet(), + emoji_race::race(), + emoji_race::start_race(), ], event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) diff --git a/src/main.rs b/src/main.rs index 66fc724..6ea5e63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,7 @@ mod migrations; mod models; mod user; -use crate::config::{Args, BotConfig, Channel, GlobalData}; -//use crate::discord::emoji_race::RaceMessage; -//use crate::discord::{unrecognised_command_hook, Handler}; +use crate::config::{Args, BotConfig, GlobalData}; use crate::discord::run_bot; use log::{error, info, LevelFilter}; use magick_rust::magick_wand_genesis; diff --git a/src/models/mod.rs b/src/models/mod.rs index 034a1ff..e501737 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,5 +3,6 @@ pub mod birthday; pub mod insult_compliment; pub mod lil_fren; pub mod motivation; +pub mod race; pub mod random; pub mod task; diff --git a/src/models/race.rs b/src/models/race.rs new file mode 100644 index 0000000..8af2640 --- /dev/null +++ b/src/models/race.rs @@ -0,0 +1,203 @@ +use crate::error::Error; +use crate::user::UserError; +use j_db::database::Database; +use j_db::error::JDbError; +use j_db::model::JdbModel; +use poise::serenity_prelude::{Emoji, EmojiId, MessageBuilder, UserId}; +use rand::{rng, Rng}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +pub const RACE_SIZE: usize = 25; +const BASE_SPEED: f32 = 0.5; +pub const NUMBER_OF_RACERS: usize = 5; + +#[derive(Debug)] +pub enum RaceError { + RacerNotFound, + BetFundError(UserError), + UserHasBet, + GenericError(Error), +} + +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"), + RaceError::GenericError(e) => write!(f, "Sorry the horse died: {}", e), + } + } +} + +impl From for RaceError { + fn from(value: UserError) -> Self { + Self::BetFundError(value) + } +} + +impl From for RaceError { + fn from(value: Error) -> Self { + Self::GenericError(value) + } +} + +impl std::error::Error for RaceError {} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Bet { + id: Option, + pub emoji: EmojiId, + pub amount: u32, + pub author: UserId, +} + +impl JdbModel for Bet { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "Bet".to_string() + } +} + +impl Bet { + pub fn new(emoji: EmojiId, amount: u32, author: UserId) -> Self { + Self { + id: None, + emoji, + amount, + author, + } + } + + pub fn add_bet(db: &Database, bet: Self) -> Result { + Ok(db.insert(bet)?) + } + + pub fn get_bets(db: &Database) -> Result, Error> { + Ok(db.filter(|_, _bet: &Self| true)?.collect()) + } + + pub fn clear_bet(db: &Database) -> Result<(), Error> { + let bets = Self::get_bets(db)?; + + for bet in &bets { + db.remove::(bet.id().unwrap())?; + } + + Ok(()) + } + + pub fn has_user_bet(db: &Database, user: UserId) -> Result { + Ok(db + .filter(|_, bet: &Self| bet.author == user)? + .next() + .is_some()) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Racer { + id: Option, + 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 = rng().random_range(0.0..0.5); + let speed = BASE_SPEED + (0.50 * genetic_stat) + (0.50 * random_stat); + + Racer { + id: None, + emoji, + speed, + pos: 0.0, + } + } + + pub fn update_pos(&mut self) { + let speed_boost = 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 + } + + pub fn add_racer(db: &Database, emoji: Emoji) -> Result { + let racer = Self::new(emoji); + let racer = db.insert(racer)?; + Ok(racer) + } + + pub fn get_racers(db: &Database) -> Result, Error> { + Ok(db.filter(|_, _racer: &Self| true)?.collect()) + } + + pub fn clear_racers(db: &Database) -> Result<(), Error> { + let racers = Self::get_racers(db)?; + + for racer in &racers { + db.remove::(racer.id().unwrap())?; + } + + Ok(()) + } +} + +impl JdbModel for Racer { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "Racer".to_string() + } +} + +pub 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() +}