Added races
+ fmt + clippy
This commit is contained in:
parent
d8489d336f
commit
65ea879ffa
@ -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<String>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Channel<T> {
|
||||
pub recv: Arc<Mutex<Receiver<T>>>,
|
||||
pub send: Arc<Mutex<Sender<T>>>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for StoryRecv {
|
||||
type Value = Mutex<StoryRecv>;
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StorySend {
|
||||
pub send: tokio::sync::mpsc::Sender<String>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for StorySend {
|
||||
type Value = Mutex<StorySend>;
|
||||
}
|
||||
|
||||
pub fn create_story_channel() -> (Mutex<StorySend>, Mutex<StoryRecv>) {
|
||||
let (send, recv) = tokio::sync::mpsc::channel::<String>(10);
|
||||
|
||||
(
|
||||
Mutex::new(StorySend { send }),
|
||||
Mutex::new(StoryRecv { recv }),
|
||||
)
|
||||
impl<T: 'static + Send + Sync> TypeMapKey for Channel<T> {
|
||||
type Value = Channel<T>;
|
||||
}
|
||||
|
||||
362
src/discord/emoji_race.rs
Normal file
362
src/discord/emoji_race.rs
Normal file
@ -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<WalletError> 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<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.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<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();
|
||||
|
||||
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::<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(&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<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 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<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, |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<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("");
|
||||
|
||||
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<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 = args.parse::<EmojiIdentifier>()?;
|
||||
args.advance();
|
||||
let amount = args.parse::<u32>()?;
|
||||
|
||||
let send = race_msg_channel.send.lock().await;
|
||||
|
||||
send.send(RaceMessage::Bet(Bet {
|
||||
emoji: racer.id,
|
||||
amount,
|
||||
author: msg.author.id,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -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::<GlobalData>().unwrap();
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
global_data
|
||||
.cfg
|
||||
.save(&global_data.args.cfg_path)
|
||||
|
||||
@ -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::<StoryRecv>().unwrap();
|
||||
let story_channel = data.get::<Channel<String>>().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::<StorySend>().unwrap();
|
||||
let story_channel = data.get::<Channel<String>>().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(())
|
||||
}
|
||||
|
||||
11
src/main.rs
11
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::<String>::new();
|
||||
let race_channel = Channel::<RaceMessage>::new();
|
||||
|
||||
let intents = GatewayIntents::all();
|
||||
let mut client = Client::builder(&global_data.cfg.bot_token, intents)
|
||||
.framework(framework)
|
||||
.type_map_insert::<GlobalData>(global_data)
|
||||
.type_map_insert::<StorySend>(send)
|
||||
.type_map_insert::<StoryRecv>(recv)
|
||||
.type_map_insert::<Channel<String>>(story_channel)
|
||||
.type_map_insert::<Channel<RaceMessage>>(race_channel)
|
||||
.event_handler(discord::Handler)
|
||||
.await
|
||||
.expect("Unable to create client.");
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user