diff --git a/Cargo.lock b/Cargo.lock index f3509d7..a0c7d19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,8 +1178,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1189,7 +1199,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1201,6 +1221,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1301,9 +1330,11 @@ dependencies = [ name = "roll-one-ring" version = "0.1.0" dependencies = [ + "bytecount", "color-eyre", "dotenvy", "poise", + "rand 0.9.2", "tokio", "tracing", "tracing-subscriber", @@ -1951,7 +1982,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand", + "rand 0.8.5", "rustls 0.22.4", "rustls-pki-types", "sha1", diff --git a/Cargo.toml b/Cargo.toml index a82d9df..52120a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] +bytecount = "0.6.9" color-eyre = "0.6.5" dotenvy = "0.15.7" poise = "0.6.1" +rand = "0.9.2" tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tracing = "0.1.41" tracing-subscriber = "0.3.20" diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 532bf97..6301604 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -2,7 +2,8 @@ use color_eyre::eyre::{Error, Result}; use poise::serenity_prelude::{self as serenity, FullEvent}; use tracing::info; -mod roll; +mod roll_dice; +mod source; type Context<'a> = poise::Context<'a, (), Error>; @@ -17,7 +18,7 @@ pub async fn make_bot() -> Result { let intents = serenity::GatewayIntents::non_privileged(); let framework = poise::Framework::<(), Error>::builder() .options(poise::FrameworkOptions { - commands: vec![roll::roll()], + commands: vec![roll_dice::roll(), source::source()], event_handler: |ctx, event, _framework: poise::FrameworkContext<'_, (), _>, _data| { Box::pin(async move { event_handler(ctx.clone(), event); diff --git a/src/discord/roll.rs b/src/discord/roll.rs deleted file mode 100644 index 4a2a145..0000000 --- a/src/discord/roll.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::str::FromStr; - -use super::Context; -use color_eyre::eyre::{Error, Result}; -use tracing::info; - -enum Advantage { - None, - Beni, - Maudit, -} - -impl FromStr for Advantage { - type Err = color_eyre::eyre::Report; - - fn from_str(s: &str) -> std::result::Result { - match s { - "aucun" => Ok(Self::None), - "béni" => Ok(Self::Beni), - "maudit" => Ok(Self::Maudit), - other => Err(Error::msg(format!("Could not parse {other} into an Advantage enum"))), - } - } -} - -impl TryFrom> for Advantage { - type Error = color_eyre::eyre::Report; - - fn try_from(value: Option) -> std::result::Result { - match value { - Some(str) => Advantage::from_str(&str), - None => Ok(Advantage::None) - } - } -} - - -impl std::fmt::Display for Advantage { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let str = match self { - Self::None => "aucun".to_string(), - Self::Beni => "béni".to_string(), - Self::Maudit => "maudit".to_string(), - }; - write!(f, "{str}") - } -} - -async fn advantage_autocomplete(_ctx: Context<'_>, _: &str) -> impl Iterator { - [Advantage::None, Advantage::Beni, Advantage::Maudit] - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .into_iter() -} - -#[poise::command(slash_command)] -pub async fn roll( - ctx: Context<'_>, - #[description = "Seuil de réussite"] sr: u32, - #[description = "Maîtrise"] mastery: u32, - #[description = "Avantage"] - #[autocomplete = "advantage_autocomplete"] - advantage: Option, -) -> Result<()> { - info!("Called /roll with following context: {ctx:?}"); - let advantage = Advantage::try_from(advantage)?; - info!("Rolling against SR {sr} with mastery {mastery} and advantage {advantage}"); - Ok(()) -} diff --git a/src/discord/roll_dice/advantage.rs b/src/discord/roll_dice/advantage.rs new file mode 100644 index 0000000..3a3ee42 --- /dev/null +++ b/src/discord/roll_dice/advantage.rs @@ -0,0 +1,41 @@ +use color_eyre::eyre::Error; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq)] +pub enum Advantage { + None, + Beni, + Maudit, +} + +impl FromStr for Advantage { + type Err = color_eyre::eyre::Report; + + fn from_str(s: &str) -> std::result::Result { + match s { + "aucun" => Ok(Self::None), + "béni" => Ok(Self::Beni), + "maudit" => Ok(Self::Maudit), + other => Err(Error::msg(format!("Could not parse {other} into an Advantage enum"))), + } + } +} + +impl TryFrom> for Advantage { + type Error = color_eyre::eyre::Report; + + fn try_from(value: Option) -> std::result::Result { + value.map_or_else(|| Ok(Self::None), |str| Self::from_str(&str)) + } +} + +impl std::fmt::Display for Advantage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let str = match self { + Self::None => "aucun".to_string(), + Self::Beni => "béni".to_string(), + Self::Maudit => "maudit".to_string(), + }; + write!(f, "{str}") + } +} diff --git a/src/discord/roll_dice/mod.rs b/src/discord/roll_dice/mod.rs new file mode 100644 index 0000000..f3deb72 --- /dev/null +++ b/src/discord/roll_dice/mod.rs @@ -0,0 +1,45 @@ +mod advantage; +mod roll; +mod roll_result; +mod success; + +use advantage::Advantage; +use roll::Roll; +use roll_result::RollResult; +use success::Success; + +use super::Context; +use color_eyre::eyre::Result; +use tracing::info; + +async fn advantage_autocomplete(_ctx: Context<'_>, _: &str) -> impl Iterator { + [Advantage::None, Advantage::Beni, Advantage::Maudit] + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .into_iter() +} + +#[poise::command(slash_command)] +pub async fn roll( + ctx: Context<'_>, + #[description = "Seuil de réussite"] + #[min = 0] + #[max = 100] + sr: u8, + #[description = "Maîtrise"] + #[min = 0] + #[max = 6] + mastery: u8, + #[description = "Avantage"] + #[autocomplete = "advantage_autocomplete"] + advantage: Option, +) -> Result<()> { + let advantage = Advantage::try_from(advantage)?; + let roll = Roll::new(sr, mastery, advantage); + info!("Received roll {roll:?}"); + let result: RollResult = roll.into(); + info!("Result: {result:?}"); + ctx.say(result.to_string()).await?; + Ok(()) +} diff --git a/src/discord/roll_dice/roll.rs b/src/discord/roll_dice/roll.rs new file mode 100644 index 0000000..8ba4fa3 --- /dev/null +++ b/src/discord/roll_dice/roll.rs @@ -0,0 +1,14 @@ +use super::Advantage; + +#[derive(Debug)] +pub struct Roll { + pub sr: u8, + pub mastery: u8, + pub advantage: Advantage, +} + +impl Roll { + pub const fn new(sr: u8, mastery: u8, advantage: Advantage) -> Self { + Self { sr, mastery, advantage } + } +} diff --git a/src/discord/roll_dice/roll_result.rs b/src/discord/roll_dice/roll_result.rs new file mode 100644 index 0000000..381519a --- /dev/null +++ b/src/discord/roll_dice/roll_result.rs @@ -0,0 +1,73 @@ +use super::{Advantage, Roll, Success}; + +fn roll_main_dice() -> u8 { + rand::random_range(0..=11) +} + +fn roll_mastery_dice() -> u8 { + rand::random_range(1..=6) +} + +#[derive(Debug)] +pub struct RollResult { + sr: u8, + main_dice: u8, + all_main_dice: Vec, + mastery: Vec, + result: u8, + success: Success, +} + +impl RollResult { + pub fn main_dice_result(main_dice: &[u8], advantage: &Advantage) -> u8 { + match advantage { + Advantage::None => *main_dice.first().unwrap(), + Advantage::Beni => *main_dice.iter().max().unwrap_or(&0), + Advantage::Maudit => *main_dice.iter().min().unwrap_or(&0), + } + } + + fn compute_result(main_dice: &[u8], mastery: &[u8], advantage: &Advantage) -> u8 { + let main_dice = Self::main_dice_result(main_dice, advantage); + main_dice + mastery.iter().sum::() + } +} + +impl From for RollResult { + fn from(value: Roll) -> Self { + let all_main_dice: Vec = if value.advantage == Advantage::None { + vec![roll_main_dice()] + } else { + (0..2).map(|_| roll_mastery_dice()).collect() + }; + let mastery: Vec = std::iter::repeat_with(roll_mastery_dice) + .take(value.mastery.into()) + .collect(); + let sr = value.sr; + let result = Self::compute_result(&all_main_dice, &mastery, &value.advantage); + let main_dice = Self::main_dice_result(&all_main_dice, &value.advantage); + let success = Success::new(main_dice, result, sr, &mastery); + Self { + sr, + main_dice, + all_main_dice, + mastery, + result, + success, + } + } +} + +impl std::fmt::Display for RollResult { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let result = format!( + "Seuil de réussite : {} +Dé(s) du destin: {} ({:?}) +Dé(s) de maîtrise: {:?} +Résultat : {} +Réussite : {}", + self.sr, self.main_dice, self.all_main_dice, self.mastery, self.result, self.success + ); + write!(f, "{result}") + } +} diff --git a/src/discord/roll_dice/success.rs b/src/discord/roll_dice/success.rs new file mode 100644 index 0000000..6ced247 --- /dev/null +++ b/src/discord/roll_dice/success.rs @@ -0,0 +1,59 @@ +#[derive(Debug)] +pub enum Success { + Failure, + Success(SuccessLevel), +} + +impl Success { + pub fn new(main_dice: u8, result: u8, sr: u8, mastery: &[u8]) -> Self { + let success_level = SuccessLevel::from(mastery); + match main_dice { + 11 => Self::Success(success_level), + _ => { + if result >= sr { + Self::Success(success_level) + } else { + Self::Failure + } + } + } + } +} + +impl std::fmt::Display for Success { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let str = match self { + Self::Failure => "échec".to_owned(), + Self::Success(success_level) => success_level.to_string(), + }; + write!(f, "{str}") + } +} + +#[derive(Debug)] +pub enum SuccessLevel { + Normal, + Great, + Incredible, +} + +impl From<&[u8]> for SuccessLevel { + fn from(value: &[u8]) -> Self { + match bytecount::count(value, 6) { + 0 => Self::Normal, + 1 => Self::Great, + _ => Self::Incredible, + } + } +} + +impl std::fmt::Display for SuccessLevel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let level = match self { + Self::Normal => "réussite normale", + Self::Great => "grande réussite", + Self::Incredible => "très grande réussite", + }; + write!(f, "{level}") + } +} diff --git a/src/discord/source.rs b/src/discord/source.rs new file mode 100644 index 0000000..30ec95c --- /dev/null +++ b/src/discord/source.rs @@ -0,0 +1,10 @@ +use color_eyre::eyre::Result; + +use super::Context; + +#[poise::command(slash_command)] +pub async fn source(ctx: Context<'_>) -> Result<()> { + let message = "Mon code source se situe [là](https://labs.phundrak.com/phundrak/roll-one-ring)"; + ctx.say(message).await?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 63f4d8c..a54edc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(missing_docs)] -#![allow(clippy::module_name_repetitions, clippy::redundant_pub_crate)] +#![allow( + clippy::module_name_repetitions, + clippy::redundant_pub_crate, + clippy::enum_variant_names +)] #![allow(clippy::unused_async)] mod discord; mod utils; -use poise::serenity_prelude::{self as serenity}; -use tracing::info; use color_eyre::Result; +use tracing::info; #[tokio::main] async fn main() -> Result<()> {