use crate::drop::{DropConfig, Item};
use crate::error::McSimError;
use cached::proc_macro::cached;
use fraction::BigUint;
use fraction::Zero;
use statrs::distribution::{Discrete, NegativeBinomial, Univariate};
type F = fraction::GenericFraction<BigUint>;
#[derive(Debug, Clone, Copy)]
pub struct EnderPearlDistribution {
ender_pearl_target_total: u32,
ender_pearl_target_per_run: u32,
distribution: NegativeBinomial,
}
impl EnderPearlDistribution {
pub fn new(
ender_pearl_target_total: u32,
ender_pearl_target_per_run: u32,
drop_list: &[DropConfig],
) -> Result<Self, McSimError> {
EnderPearlDistribution::create_distribution(
ender_pearl_target_total,
ender_pearl_target_per_run,
drop_list,
)
.map(|distribution| Self {
ender_pearl_target_total,
ender_pearl_target_per_run,
distribution,
})
}
pub fn distribution(&self) -> &NegativeBinomial {
&self.distribution
}
pub fn luck(&self, total_barters_made: u32, successful_barters: u32) -> f64 {
self.distribution
.cdf(total_barters_made as f64 - successful_barters as f64)
}
pub fn probability(&self, total_barters_made: u32, successful_barters: u32) -> f64 {
self.distribution
.pmf((total_barters_made as i32 - successful_barters as i32) as u64)
}
fn create_distribution(
ender_pearl_target_total: u32,
ender_pearl_target_per_run: u32,
drop_list: &[DropConfig],
) -> Result<NegativeBinomial, McSimError> {
let drop_probability = item_drop_probability(drop_list, Item::EnderPearl);
let drop_range = item_drop_range(drop_list, Item::EnderPearl);
let mean_drops_to_reach_target = attempts_to_reach_target(
drop_range.0 as i32,
drop_range.1 as i32,
ender_pearl_target_per_run as i32,
);
NegativeBinomial::new(
ender_pearl_target_total as f64 / ender_pearl_target_per_run as f64
* mean_drops_to_reach_target,
drop_probability,
)
.map_err(|_| McSimError::InvalidDistribution)
}
}
#[derive(Debug, Clone, Copy)]
pub struct BlazeRodDistribution {
blaze_rod_target: u32,
distribution: NegativeBinomial,
}
impl BlazeRodDistribution {
pub fn new(blaze_rod_target: u32, drop_list: &[DropConfig]) -> Result<Self, McSimError> {
BlazeRodDistribution::create_distribution(blaze_rod_target, drop_list).map(|distribution| {
Self {
blaze_rod_target,
distribution,
}
})
}
pub fn distribution(&self) -> &NegativeBinomial {
&self.distribution
}
pub fn luck(&self, total_blazes_killed: u32) -> f64 {
self.distribution
.cdf(total_blazes_killed as f64 - self.blaze_rod_target as f64)
}
pub fn probability(&self, total_blazes_killed: u32) -> f64 {
self.distribution
.pmf((total_blazes_killed as i32 - self.blaze_rod_target as i32) as u64)
}
fn create_distribution(
blaze_rod_target: u32,
drop_list: &[DropConfig],
) -> Result<NegativeBinomial, McSimError> {
NegativeBinomial::new(
blaze_rod_target as f64,
item_drop_average(drop_list, Item::BlazeRod),
)
.map_err(|_| McSimError::InvalidDistribution)
}
}
pub fn item_drop_probability(drop_list: &[DropConfig], item: Item) -> f64 {
let target = drop_list.iter().find(|d| d.item == item).unwrap();
target.weight as f64 / drop_list.iter().map(|d| d.weight as f64).sum::<f64>()
}
pub fn item_drop_average(drop_list: &[DropConfig], item: Item) -> f64 {
let target = drop_list.iter().find(|d| d.item == item).unwrap();
(target.max_count as f64 - target.min_count as f64) / 2.0 + target.min_count as f64
}
pub fn item_drop_range(drop_list: &[DropConfig], item: Item) -> (u32, u32) {
let target = drop_list.iter().find(|d| d.item == item).unwrap();
(target.min_count, target.max_count)
}
pub fn attempts_to_reach_target(min: i32, max: i32, target: i32) -> f64 {
attempts_to_reach_target_cached(min, max, target)
}
#[cached]
fn attempts_to_reach_target_cached(min: i32, max: i32, target: i32) -> f64 {
match target {
_ if target <= 0 => 0.0,
_ => {
1.0 + 1.0 / (max - min + 1) as f64
* (min..(max + 1))
.map(|k| attempts_to_reach_target_cached(min, max, target - k))
.sum::<f64>()
}
}
}
pub struct UniformProbabilityTable {
samples: usize,
distribution_size: usize,
table: Vec<Vec<F>>,
}
impl UniformProbabilityTable {
pub fn generate(samples: u32, distribution_size: u32) -> Self {
let samples = samples as usize;
let distribution_size = distribution_size as usize;
Self {
samples,
distribution_size,
table: UniformProbabilityTable::uniform_probabilities_for_n_samples(
samples,
distribution_size,
),
}
}
pub fn expectation_of_target(&self) -> F {
let probabilities = self.probabilities();
let expectations = self.expectations();
let mut normalization_factor = F::zero();
let mut exp_contrib = F::zero();
for min_dots in 0..self.distribution_size {
let prob = F::new(
(self.distribution_size - min_dots) as u64,
self.distribution_size as u64,
) * probabilities[self.samples - min_dots - 1].clone();
normalization_factor += prob.clone();
exp_contrib += prob * (expectations[self.samples - min_dots - 1].clone() + F::from(1));
}
exp_contrib / normalization_factor
}
fn expectations(&self) -> Vec<F> {
let probabilities = self.probabilities();
let mut expectations = vec![F::zero()];
for num in 1..self.samples {
let mut normalization_factor = F::zero();
let mut exp_contrib = F::zero();
for dots in 1..(std::cmp::min(self.distribution_size, num) + 1) {
let prob = probabilities[num - dots].clone();
normalization_factor += prob.clone();
exp_contrib += prob * (expectations[num - dots].clone() + F::from(1));
}
expectations.push(exp_contrib / normalization_factor);
}
expectations
}
fn probabilities(&self) -> Vec<F> {
(0..self.samples)
.map(|num| self.probability_of_number(num))
.collect()
}
fn probability_of_number(&self, num: usize) -> F {
(0..self.samples)
.map(|throw| self.table[throw][num].clone())
.sum()
}
fn uniform_probabilities_for_n_samples(
samples: usize,
distribution_size: usize,
) -> Vec<Vec<F>> {
let mut probabilities =
UniformProbabilityTable::create_uniform_probabilities_table(samples);
for throw in 1..samples {
for num in 1..samples {
probabilities[throw][num] = (1..std::cmp::min(num + 1, distribution_size + 1))
.map(|dots| probabilities[throw - 1][num - dots].clone())
.sum::<F>()
/ F::from(distribution_size)
}
}
probabilities
}
fn create_uniform_probabilities_table(size: usize) -> Vec<Vec<F>> {
let mut table: Vec<Vec<F>> = (0..size)
.map(|_| (0..size).map(|_| F::from(0.0)).collect())
.collect();
table[0][0] = F::from(1.0);
table
}
}