use rand::RngExt;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DieRoll {
pub value: u32,
pub sides: u32,
pub is_critical_success: bool,
pub is_critical_failure: bool,
}
impl DieRoll {
pub fn new(value: u32, sides: u32) -> Self {
Self {
value,
sides,
is_critical_success: value == sides,
is_critical_failure: sides > 1 && value == 1,
}
}
pub fn meets_target(&self, target: u32) -> bool {
self.value >= target
}
pub fn in_range(&self, min: u32, max: u32) -> bool {
self.value >= min && self.value <= max
}
}
impl fmt::Display for DieRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RollResult {
pub rolls: Vec<DieRoll>,
pub total: i64,
pub description: String,
pub success: Option<bool>,
pub success_count: Option<u32>,
pub failure_count: Option<u32>,
}
impl RollResult {
pub fn new(rolls: Vec<DieRoll>, total: i64, description: String) -> Self {
Self {
rolls,
total,
description,
success: None,
success_count: None,
failure_count: None,
}
}
pub fn with_success(
rolls: Vec<DieRoll>,
total: i64,
description: String,
success: bool,
success_count: u32,
failure_count: u32,
) -> Self {
Self {
rolls,
total,
description,
success: Some(success),
success_count: Some(success_count),
failure_count: Some(failure_count),
}
}
pub fn values(&self) -> Vec<u32> {
self.rolls.iter().map(|r| r.value).collect()
}
pub fn dice_count(&self) -> usize {
self.rolls.len()
}
pub fn has_critical_success(&self) -> bool {
self.rolls.iter().any(|r| r.is_critical_success)
}
pub fn has_critical_failure(&self) -> bool {
self.rolls.iter().any(|r| r.is_critical_failure)
}
}
impl fmt::Display for RollResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} = {}", self.description, self.total)
}
}
pub trait Dice: Send + Sync {
fn roll(&self) -> RollResult;
fn describe(&self) -> String;
fn expected_value(&self) -> f64;
fn min_value(&self) -> i64;
fn max_value(&self) -> i64;
}
pub trait ModifiableDice: Dice {
fn keep_highest(self, n: u32) -> Box<dyn Dice>;
fn keep_lowest(self, n: u32) -> Box<dyn Dice>;
fn drop_highest(self, n: u32) -> Box<dyn Dice>;
fn drop_lowest(self, n: u32) -> Box<dyn Dice>;
fn reroll_below(self, threshold: u32) -> Box<dyn Dice>;
fn reroll_above(self, threshold: u32) -> Box<dyn Dice>;
fn explode_above(self, threshold: u32) -> Box<dyn Dice>;
fn explode_below(self, threshold: u32) -> Box<dyn Dice>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiceConfig {
pub default_sides: u32,
pub detect_criticals: bool,
pub seed: Option<u64>,
pub max_dice: u32,
pub max_sides: u32,
}
impl Default for DiceConfig {
fn default() -> Self {
Self {
default_sides: 20, detect_criticals: true,
seed: None,
max_dice: 1000,
max_sides: 10000,
}
}
}
#[derive(Debug, Clone)]
pub struct DiceContext {
pub config: DiceConfig,
pub rng: rand::rngs::ThreadRng,
}
impl DiceContext {
pub fn new() -> Self {
Self {
config: DiceConfig::default(),
rng: rand::rng(),
}
}
pub fn with_config(config: DiceConfig) -> Self {
Self {
config,
rng: rand::rng(),
}
}
pub fn roll_die(&mut self, sides: u32) -> DieRoll {
if sides == 0 {
return DieRoll::new(0, 0);
}
let value = self.rng.random_range(1..=sides);
DieRoll::new(value, sides)
}
pub fn roll_dice(&mut self, count: u32, sides: u32) -> Vec<DieRoll> {
(0..count).map(|_| self.roll_die(sides)).collect()
}
}
impl Default for DiceContext {
fn default() -> Self {
Self::new()
}
}