use std::time::{SystemTime, UNIX_EPOCH}; use crate::bot::module::{Action, Command, CommandSpec, Module}; const COMMANDS: &[CommandSpec] = &[CommandSpec { name: "roll", usage: "roll [NdM]", about: "roll N dice of M sides (default 1d6), e.g. roll 2d20", }]; const MAX_DICE: u64 = 100; const MAX_SIDES: u64 = 1000; pub struct Dice { rng: u64, } impl Dice { pub fn new() -> Dice { let seed = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_nanos() as u64) .unwrap_or(0x9E37_79B9_7F4A_7C15); Dice { rng: seed | 1 } } fn next_u64(&mut self) -> u64 { let mut x = self.rng; x ^= x << 13; x ^= x >> 7; x ^= x << 17; self.rng = x; x } fn die(&mut self, sides: u64) -> u64 { self.next_u64() % sides + 1 } } impl Default for Dice { fn default() -> Dice { Dice::new() } } impl Module for Dice { fn name(&self) -> &'static str { "dice" } fn commands(&self) -> &'static [CommandSpec] { COMMANDS } fn on_command(&mut self, cmd: &Command) -> Vec { let spec = cmd.args.first().copied().unwrap_or("1d6"); let reply = match parse_spec(spec) { Some((count, sides)) => { let rolls: Vec = (0..count).map(|_| self.die(sides)).collect(); let total: u64 = rolls.iter().sum(); if count == 1 { format!("{count}d{sides}: {total}") } else { let parts: Vec = rolls.iter().map(u64::to_string).collect(); format!("{count}d{sides}: {} = {total}", parts.join(" + ")) } } None => format!("usage: {p}roll [NdM], e.g. {p}roll 2d6", p = cmd.prefix), }; vec![Action::Reply(reply)] } } fn parse_spec(s: &str) -> Option<(u64, u64)> { let (count, sides) = match s.split_once('d') { Some((c, m)) => { let count = if c.is_empty() { 1 } else { c.parse().ok()? }; (count, m.parse().ok()?) } None => (1, s.parse().ok()?), }; if (1..=MAX_DICE).contains(&count) && (2..=MAX_SIDES).contains(&sides) { Some((count, sides)) } else { None } }