FrenBot/src/discord/emoji_race.rs
Joey Hines 67ff69aca0
Updated serenity version and added new commands
+ Added task.rs to handle periodic tasks
2024-01-15 16:53:17 -07:00

382 lines
11 KiB
Rust

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<UserError> 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<Self> 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<Racer>) -> 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<Bet>,
racers: &[Racer],
) -> Result<(), RaceError> {
let mut data = ctx.data.write().await;
let global = data.get_mut::<GlobalData>().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::<Channel<RaceMessage>>().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<Racer> = 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<Bet> = 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<Racer> = 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<UserId> = 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::<GlobalData>().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<RaceMessage>) -> 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::<Channel<RaceMessage>>().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("<emoji> <amount>")]
#[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::<Channel<RaceMessage>>().unwrap();
if !check_if_race_in_progress(race_msg_channel) {
msg.reply(&ctx.http, "Race not in progress").await?;
}
let racer = match args.parse::<EmojiIdentifier>() {
Ok(emoji) => emoji,
Err(e) => {
msg.reply(&ctx.http, format!("Invalid emoji: {}", e))
.await?;
return Ok(());
}
};
args.advance();
let amount = match args.parse::<u32>() {
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(())
}