use crate::core::{Dice, DiceContext, DieRoll, RollResult};
use crate::error::{DiceError, DiceResult};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct ExplodingDice {
pub count: u32,
pub sides: u32,
pub explode_threshold: u32,
pub max_explosions: Option<u32>,
pub explode_below: bool,
}
impl ExplodingDice {
pub fn new(count: u32, sides: u32, explode_threshold: u32) -> Self {
Self {
count,
sides,
explode_threshold,
max_explosions: None,
explode_below: false,
}
}
pub fn with_max_explosions(mut self, max: u32) -> Self {
self.max_explosions = Some(max);
self
}
pub fn with_explode_below(mut self, threshold: u32) -> Self {
self.explode_threshold = threshold;
self.explode_below = true;
self
}
pub fn validate(&self) -> DiceResult<()> {
if self.sides == 0 {
return Err(DiceError::InvalidDice(
"Dice must have at least 1 side".into(),
));
}
if self.explode_threshold == 0 || self.explode_threshold > self.sides {
return Err(DiceError::InvalidParameter(format!(
"Explode threshold must be between 1 and {}",
self.sides
)));
}
Ok(())
}
fn roll_exploding_die(&self, ctx: &mut DiceContext) -> Vec<DieRoll> {
let mut rolls = Vec::new();
let mut explosion_count = 0;
loop {
let roll = ctx.roll_die(self.sides);
rolls.push(roll);
let should_explode = if self.explode_below {
roll.value <= self.explode_threshold
} else {
roll.value >= self.explode_threshold
};
if !should_explode {
break;
}
explosion_count += 1;
if let Some(max) = self.max_explosions {
if explosion_count >= max {
break;
}
}
}
rolls
}
}
impl Dice for ExplodingDice {
fn roll(&self) -> RollResult {
self.validate().unwrap();
let mut ctx = DiceContext::new();
let mut all_rolls = Vec::new();
let mut total = 0i64;
for _ in 0..self.count {
let die_rolls = self.roll_exploding_die(&mut ctx);
let die_total: u32 = die_rolls.iter().map(|r| r.value).sum();
all_rolls.extend(die_rolls);
total += die_total as i64;
}
let direction = if self.explode_below { "≤" } else { "≥" };
let mut description = format!(
"{}d{}!{}{}",
self.count, self.sides, direction, self.explode_threshold
);
if let Some(max) = self.max_explosions {
description.push_str(&format!(" (max {} explosions)", max));
}
description.push_str(" [");
let mut current_die = 0;
let mut die_start = 0;
while die_start < all_rolls.len() {
if current_die > 0 {
description.push_str(" | ");
}
let mut die_end = die_start;
while die_end < all_rolls.len() {
if die_end + 1 < all_rolls.len() {
let next_roll = &all_rolls[die_end + 1];
let _current_roll = &all_rolls[die_end];
if next_roll.value >= self.explode_threshold && !self.explode_below
|| next_roll.value <= self.explode_threshold && self.explode_below
{
die_end += 1;
} else {
break;
}
} else {
break;
}
}
let die_rolls = &all_rolls[die_start..=die_end];
for (i, roll) in die_rolls.iter().enumerate() {
if i > 0 {
description.push('!');
}
description.push_str(&roll.value.to_string());
}
die_start = die_end + 1;
current_die += 1;
}
description.push(']');
RollResult::new(all_rolls, total, description)
}
fn describe(&self) -> String {
let direction = if self.explode_below { "≤" } else { "≥" };
let mut desc = format!(
"{}d{} exploding on {} {}",
self.count, self.sides, direction, self.explode_threshold
);
if let Some(max) = self.max_explosions {
desc.push_str(&format!(" (max {} explosions)", max));
}
desc
}
fn expected_value(&self) -> f64 {
let avg_per_roll = (self.sides as f64 + 1.0) / 2.0;
let explosion_prob = if self.explode_below {
self.explode_threshold as f64 / self.sides as f64
} else {
(self.sides - self.explode_threshold + 1) as f64 / self.sides as f64
};
let expected_rolls_per_die = 1.0 / (1.0 - explosion_prob);
let expected_rolls = if let Some(max) = self.max_explosions {
let max_rolls = (max + 1) as f64; expected_rolls_per_die.min(max_rolls)
} else {
expected_rolls_per_die
};
avg_per_roll * expected_rolls * self.count as f64
}
fn min_value(&self) -> i64 {
self.count as i64
}
fn max_value(&self) -> i64 {
if let Some(max_explosions) = self.max_explosions {
let total_rolls = self.count * (max_explosions + 1);
(total_rolls * self.sides) as i64
} else {
i64::MAX }
}
}
impl fmt::Display for ExplodingDice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.describe())
}
}