rubot/src/bot/modules/dice.rs
2026-07-29 16:24:30 +00:00

88 lines
2.3 KiB
Rust

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<Action> {
let spec = cmd.args.first().copied().unwrap_or("1d6");
let reply = match parse_spec(spec) {
Some((count, sides)) => {
let rolls: Vec<u64> = (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<String> = 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
}
}