use crate::core::{DieRoll, RollResult};
use crate::error::DiceError;
#[derive(Debug, Clone)]
pub struct RollStatistics {
pub min: u32,
pub max: u32,
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub critical_successes: u32,
pub critical_failures: u32,
}
impl RollStatistics {
pub fn from_result(result: &RollResult) -> Self {
Self::from_rolls(&result.rolls)
}
pub fn from_rolls(rolls: &[DieRoll]) -> Self {
if rolls.is_empty() {
return Self {
min: 0,
max: 0,
mean: 0.0,
median: 0.0,
std_dev: 0.0,
critical_successes: 0,
critical_failures: 0,
};
}
let values: Vec<u32> = rolls.iter().map(|r| r.value).collect();
let min = *values.iter().min().unwrap_or(&0);
let max = *values.iter().max().unwrap_or(&0);
let sum: u32 = values.iter().sum();
let mean = sum as f64 / values.len() as f64;
let variance = values
.iter()
.map(|&v| {
let diff = v as f64 - mean;
diff * diff
})
.sum::<f64>()
/ values.len() as f64;
let std_dev = variance.sqrt();
let mut sorted = values.clone();
sorted.sort();
let median = if sorted.len().is_multiple_of(2) {
let mid = sorted.len() / 2;
(sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
} else {
sorted[sorted.len() / 2] as f64
};
let critical_successes = rolls.iter().filter(|r| r.is_critical_success).count() as u32;
let critical_failures = rolls.iter().filter(|r| r.is_critical_failure).count() as u32;
Self {
min,
max,
mean,
median,
std_dev,
critical_successes,
critical_failures,
}
}
}
pub fn format_detailed(result: &RollResult) -> String {
let mut output = String::new();
output.push_str(&format!("Roll: {}\n", result.description));
output.push_str(&format!("Total: {}\n", result.total));
if !result.rolls.is_empty() {
output.push_str("Individual rolls: ");
for (i, roll) in result.rolls.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
output.push_str(&roll.value.to_string());
if roll.is_critical_success {
output.push_str(" (crit!)");
} else if roll.is_critical_failure {
output.push_str(" (fumble!)");
}
}
output.push('\n');
}
if let Some(success) = result.success {
output.push_str(&format!("Success: {}\n", success));
}
if let Some(success_count) = result.success_count {
output.push_str(&format!("Successes: {}\n", success_count));
}
if let Some(failure_count) = result.failure_count {
output.push_str(&format!("Failures: {}\n", failure_count));
}
output
}
pub fn parse_dice_notation(notation: &str) -> Result<Box<dyn crate::core::Dice>, DiceError> {
let notation = notation.trim();
if notation.to_lowercase().ends_with("df") || notation.to_lowercase().ends_with('f') {
let lower = notation.to_lowercase();
let count_str = if lower.ends_with("df") {
&lower[..lower.len() - 2]
} else {
&lower[..lower.len() - 1]
};
let count = if count_str.is_empty() {
4 } else {
count_str
.parse::<u32>()
.map_err(|e| DiceError::ParseError(e.to_string()))?
};
return Ok(Box::new(crate::dice::FateDice::new(count)));
}
let notation = notation.to_lowercase();
if let Some(stripped) = notation.strip_prefix('d') {
let sides = stripped
.parse::<u32>()
.map_err(|e| DiceError::ParseError(e.to_string()))?;
Ok(Box::new(crate::dice::StandardDice::new(1, sides)))
} else if notation.contains('d') {
let parts: Vec<&str> = notation.split('d').collect();
if parts.len() != 2 {
return Err(DiceError::ParseError("Invalid dice notation".into()));
}
let count = parts[0]
.parse::<u32>()
.map_err(|e| DiceError::ParseError(e.to_string()))?;
let sides = parts[1]
.parse::<u32>()
.map_err(|e| DiceError::ParseError(e.to_string()))?;
Ok(Box::new(crate::dice::StandardDice::new(count, sides)))
} else {
Err(DiceError::ParseError("Unrecognized dice notation".into()))
}
}
pub fn roll_multiple(dice: &[Box<dyn crate::core::Dice>]) -> Vec<RollResult> {
dice.iter().map(|d| d.roll()).collect()
}
pub fn batch_roll(dice: &dyn crate::core::Dice, times: u32) -> Vec<RollResult> {
(0..times).map(|_| dice.roll()).collect()
}
pub fn probability_distribution(
dice: &dyn crate::core::Dice,
samples: u32,
) -> std::collections::HashMap<i64, u32> {
let mut distribution = std::collections::HashMap::new();
for _ in 0..samples {
let result = dice.roll();
*distribution.entry(result.total).or_insert(0) += 1;
}
distribution
}