Add back in race commands

This commit is contained in:
Joey Hines 2025-03-22 16:21:10 -06:00
parent 8c9d98f031
commit 095d6e82a0
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
7 changed files with 290 additions and 302 deletions

View File

@ -11,9 +11,7 @@ use reqwest::Url;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
@ -109,24 +107,3 @@ impl GlobalData {
impl TypeMapKey for GlobalData { impl TypeMapKey for GlobalData {
type Value = GlobalData; type Value = GlobalData;
} }
#[derive(Debug, Clone)]
pub struct Channel<T> {
pub recv: Arc<Mutex<Receiver<T>>>,
pub send: Arc<Mutex<Sender<T>>>,
}
impl<T> Channel<T> {
pub fn new() -> Self {
let (send, recv) = channel::<T>(10);
Self {
recv: Arc::new(Mutex::new(recv)),
send: Arc::new(Mutex::new(send)),
}
}
}
impl<T: 'static + Send + Sync> TypeMapKey for Channel<T> {
type Value = Channel<T>;
}

View File

@ -3,7 +3,7 @@ use crate::error::Error;
use poise::serenity_prelude::builder::EditRole; use poise::serenity_prelude::builder::EditRole;
use poise::serenity_prelude::model::Colour; 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( pub async fn set_color(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Color you want your role to be"] #[description = "Color you want your role to be"]
@ -77,7 +77,7 @@ pub async fn set_color(
Ok(()) Ok(())
} }
#[poise::command(prefix_command, guild_only)] #[poise::command(prefix_command, guild_only, category = "Color")]
pub async fn remove_color(ctx: Context<'_>) -> Result<(), Error> { pub async fn remove_color(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let member = ctx.author_member().await.unwrap(); let member = ctx.author_member().await.unwrap();

View File

@ -1,140 +1,27 @@
use crate::config::{Channel, GlobalData}; use crate::discord::Context;
use crate::error::Error; use crate::error::Error;
use crate::user::{User, UserError}; use crate::models::race;
use crate::{command, group}; 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::seq::IteratorRandom;
use rand::{thread_rng, Rng}; use rand::{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; use std::time::Duration;
const RACE_SIZE: usize = 25; fn cleanup_race(db: &Database) -> Result<(), Error> {
const BASE_SPEED: f32 = 0.5; log::info!("Cleaning up race state");
const NUMBER_OF_RACERS: usize = 5; Racer::clear_racers(db)?;
Bet::clear_bet(db)?;
#[group] Ok(())
#[commands(race, start_race, bet)]
pub struct EmojiRace;
#[derive(Clone, Debug)]
pub enum RaceError {
RacerNotFound,
BetFundError(UserError),
UserHasBet,
} }
impl Display for RaceError { async fn add_bet(ctx: Context<'_>, bet: Bet, racers: &[Racer]) -> Result<(), RaceError> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { User::try_take_funds(&ctx.data().db, bet.author, bet.amount).map_err(|err| match err {
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), Error::UserError(e) => RaceError::BetFundError(e),
_ => panic!("Recv'ed error when trying to bet: {}", err), _ => panic!("Recv'ed error when trying to bet: {}", err),
})?; })?;
@ -143,44 +30,28 @@ async fn add_bet(
return Err(RaceError::RacerNotFound); 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); return Err(RaceError::UserHasBet);
} }
bets.push(bet); Bet::add_bet(&ctx.data().db, bet)?;
Ok(()) Ok(())
} }
#[command] #[poise::command(prefix_command, guild_only, category = "Race")]
#[description("Create a race")] pub async fn race(ctx: Context<'_>) -> Result<(), Error> {
#[only_in(guilds)] cleanup_race(&ctx.data().db)?;
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 emojis = ctx.guild_id().unwrap().emojis(ctx.http()).await?;
};
let mut recv = match race_msg_channel.recv.try_lock() { let racers: Vec<_> = emojis.iter().choose_multiple(&mut rng(), NUMBER_OF_RACERS);
Ok(recv) => recv,
Err(_) => {
msg.reply(&ctx.http, "Race in progress").await?;
return Ok(());
}
};
let guild = msg.guild_id.unwrap(); let racers: Result<Vec<Racer>, Error> = racers
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() .iter()
.choose_multiple(&mut thread_rng(), NUMBER_OF_RACERS); .map(|e| Racer::add_racer(&ctx.data().db, (*e).clone()))
.collect();
let mut racers: Vec<Racer> = racers.iter().map(|e| Racer::new((*e).clone())).collect(); let racers = racers?;
let mut racers_msg = MessageBuilder::new(); let mut racers_msg = MessageBuilder::new();
racers_msg.push_bold_line("Welcome to the Emoji Races, the racers today are:"); 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(format!("* {}", racer.emoji));
} }
racers_msg.push_line("Place your bets using !bet. And start the race with !start_race"); 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<Bet> = Vec::new(); Ok(())
loop { }
let race_msg = recv.recv().await.unwrap();
match race_msg { #[poise::command(prefix_command, guild_only, category = "Race")]
RaceMessage::StartRace => { pub async fn start_race(ctx: Context<'_>) -> Result<(), Error> {
race_channel.say(&ctx, "And they're off!").await?; let mut racers = Racer::get_racers(&ctx.data().db)?;
break;
} if racers.is_empty() {
RaceMessage::Bet(bet) => { ctx.reply("Woops, no race is in progress. Use !race to kick this bad boy off")
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?; .await?;
continue; return Ok(());
} }
};
if let Err(e) = add_bet(ctx, bet.clone(), &mut bets, &racers).await { let race_channel = ctx.channel_id();
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 let mut race_msg = race_channel
.say(&ctx.http, format!("{} {}", bet.author.mention(), msg)) .say(&ctx.http(), race::draw_race(&racers))
.await?; .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(); let mut final_order: Vec<Racer> = Vec::new();
while final_order.len() < NUMBER_OF_RACERS { while final_order.len() < NUMBER_OF_RACERS {
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;
@ -252,7 +87,10 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
} }
race_msg race_msg
.edit(&ctx.http, EditMessage::new().content(draw_race(&racers))) .edit(
ctx.http(),
EditMessage::new().content(race::draw_race(&racers)),
)
.await?; .await?;
for racer in &racers { 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<UserId> = bets let bet_winners: Vec<UserId> = bets
.iter() .iter()
.filter_map(|b| { .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 {}:", "The following winner(s) got a payout of {}:",
payout payout
)); ));
{
let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap();
for winner in bet_winners { for winner in bet_winners {
winner_msg.push("* "); winner_msg.push("* ");
winner_msg.mention(&winner); winner_msg.mention(&winner);
winner_msg.push_line(""); winner_msg.push_line("");
User::give_funds(&global_data.db, winner, payout as u32)?; 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(()) Ok(())
} }
fn check_if_race_in_progress(race_channel: &Channel<RaceMessage>) -> bool { #[poise::command(prefix_command, guild_only, category = "Race")]
race_channel.recv.try_lock().is_err() 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] if racers.is_empty() {
#[description("Start a race")] ctx.reply("No races are in progress use, !race to kick it off!")
#[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?; .await?;
return Ok(()); return Ok(());
} }
let bet = Bet::new(racer.id, bet_amount, ctx.author().id);
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; ctx.reply(resp).await?;
send.send(RaceMessage::Bet(Bet {
emoji: racer.id,
amount,
author: msg.author.id,
}))
.await?;
Ok(()) Ok(())
} }

View File

@ -3,6 +3,7 @@ mod album;
mod birthday; mod birthday;
mod celeryman; mod celeryman;
mod color; mod color;
mod emoji_race;
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::error::Error; use crate::error::Error;
@ -121,6 +122,9 @@ pub async fn run_bot(global_data: GlobalData) {
celeryman::tayne(), celeryman::tayne(),
color::set_color(), color::set_color(),
color::remove_color(), color::remove_color(),
emoji_race::bet(),
emoji_race::race(),
emoji_race::start_race(),
], ],
event_handler: |ctx, event, framework, data| { event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data)) Box::pin(event_handler(ctx, event, framework, data))

View File

@ -7,9 +7,7 @@ mod migrations;
mod models; mod models;
mod user; mod user;
use crate::config::{Args, BotConfig, Channel, GlobalData}; use crate::config::{Args, BotConfig, GlobalData};
//use crate::discord::emoji_race::RaceMessage;
//use crate::discord::{unrecognised_command_hook, Handler};
use crate::discord::run_bot; use crate::discord::run_bot;
use log::{error, info, LevelFilter}; use log::{error, info, LevelFilter};
use magick_rust::magick_wand_genesis; use magick_rust::magick_wand_genesis;

View File

@ -3,5 +3,6 @@ pub mod birthday;
pub mod insult_compliment; pub mod insult_compliment;
pub mod lil_fren; pub mod lil_fren;
pub mod motivation; pub mod motivation;
pub mod race;
pub mod random; pub mod random;
pub mod task; pub mod task;

203
src/models/race.rs Normal file
View File

@ -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<UserError> for RaceError {
fn from(value: UserError) -> Self {
Self::BetFundError(value)
}
}
impl From<Error> 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<u64>,
pub emoji: EmojiId,
pub amount: u32,
pub author: UserId,
}
impl JdbModel for Bet {
fn id(&self) -> Option<u64> {
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<Self, Error> {
Ok(db.insert(bet)?)
}
pub fn get_bets(db: &Database) -> Result<Vec<Self>, 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::<Self>(bet.id().unwrap())?;
}
Ok(())
}
pub fn has_user_bet(db: &Database, user: UserId) -> Result<bool, Error> {
Ok(db
.filter(|_, bet: &Self| bet.author == user)?
.next()
.is_some())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Racer {
id: Option<u64>,
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 = 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<Self, Error> {
let racer = Self::new(emoji);
let racer = db.insert(racer)?;
Ok(racer)
}
pub fn get_racers(db: &Database) -> Result<Vec<Self>, 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::<Self>(racer.id().unwrap())?;
}
Ok(())
}
}
impl JdbModel for Racer {
fn id(&self) -> Option<u64> {
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<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()
}