use crate::{Dice, Keep};
pub(crate) const ROLLS: usize = 252;
pub(crate) const KEEPS: usize = 462;
pub(crate) const SIZE5_OFFSET: usize = 210;
const FACTORIAL: [f64; 6] = [1.0, 1.0, 2.0, 6.0, 24.0, 120.0];
fn multinomial(counts: [u8; 6]) -> f64 {
let n: u8 = counts.iter().sum();
counts.iter().fold(FACTORIAL[usize::from(n)], |acc, &c| {
acc / FACTORIAL[usize::from(c)]
})
}
fn key(counts: [u8; 6]) -> usize {
counts
.iter()
.rev()
.fold(0, |acc, &c| acc * 6 + usize::from(c))
}
#[derive(Debug, Clone)]
pub(crate) struct DiceTables {
rolls: Vec<Dice>,
keeps: Vec<Keep>,
index_of: Vec<u16>,
roll_prob: [f64; ROLLS],
keep_succ_off: Vec<u32>,
keep_succ: Vec<(u16, f64)>,
roll_keeps_off: Vec<u32>,
roll_keeps: Vec<u16>,
}
fn multisets(size: u8) -> Vec<[u8; 6]> {
fn rec(counts: &mut [u8; 6], min_face: usize, left: u8, out: &mut Vec<[u8; 6]>) {
if left == 0 {
out.push(*counts);
return;
}
for face in min_face..6 {
counts[face] += 1;
rec(counts, face, left - 1, out);
counts[face] -= 1;
}
}
let mut out = Vec::new();
rec(&mut [0; 6], 0, size, &mut out);
out
}
impl DiceTables {
pub(crate) fn new() -> Self {
let counts_by_id: Vec<[u8; 6]> = (0..=5).flat_map(multisets).collect();
debug_assert_eq!(counts_by_id.len(), KEEPS);
let keeps: Vec<Keep> = counts_by_id
.iter()
.map(|&c| Keep::from_counts(c).expect("at most five dice"))
.collect();
let rolls: Vec<Dice> = counts_by_id[SIZE5_OFFSET..]
.iter()
.map(|&c| Dice::from_counts(c).expect("exactly five dice"))
.collect();
let mut index_of = vec![u16::MAX; 6usize.pow(6)];
for (id, &counts) in counts_by_id.iter().enumerate() {
index_of[key(counts)] = id as u16;
}
let mut roll_prob = [0.0; ROLLS];
for (r, dice) in rolls.iter().enumerate() {
roll_prob[r] = multinomial(dice.counts()) / 7776.0;
}
let mut keep_succ_off = vec![0u32; KEEPS + 1];
let mut keep_succ = Vec::new();
for (k, keep) in keeps.iter().enumerate() {
let rerolled = 5 - keep.len();
let outcomes = 6f64.powi(i32::from(rerolled));
for (r, roll) in rolls.iter().enumerate() {
if !roll.contains(*keep) {
continue;
}
let mut diff = roll.counts();
for (d, kept) in diff.iter_mut().zip(keep.counts()) {
*d -= kept;
}
keep_succ.push((r as u16, multinomial(diff) / outcomes));
}
keep_succ_off[k + 1] = keep_succ.len() as u32;
}
let mut roll_keeps_off = vec![0u32; ROLLS + 1];
let mut roll_keeps = Vec::new();
for (r, roll) in rolls.iter().enumerate() {
let mut ids: Vec<u16> = roll.keeps().map(|k| index_of[key(k.counts())]).collect();
ids.sort_unstable();
roll_keeps.extend_from_slice(&ids);
roll_keeps_off[r + 1] = roll_keeps.len() as u32;
}
Self {
rolls,
keeps,
index_of,
roll_prob,
keep_succ_off,
keep_succ,
roll_keeps_off,
roll_keeps,
}
}
pub(crate) fn roll(&self, roll_id: usize) -> Dice {
self.rolls[roll_id]
}
pub(crate) fn keep(&self, keep_id: usize) -> Keep {
self.keeps[keep_id]
}
pub(crate) fn keep_id(&self, keep: Keep) -> usize {
usize::from(self.index_of[key(keep.counts())])
}
pub(crate) fn roll_id(&self, dice: Dice) -> usize {
usize::from(self.index_of[key(dice.counts())]) - SIZE5_OFFSET
}
pub(crate) fn roll_prob(&self) -> &[f64; ROLLS] {
&self.roll_prob
}
pub(crate) fn keep_successors(&self, keep_id: usize) -> &[(u16, f64)] {
let (lo, hi) = (self.keep_succ_off[keep_id], self.keep_succ_off[keep_id + 1]);
&self.keep_succ[lo as usize..hi as usize]
}
pub(crate) fn roll_keeps(&self, roll_id: usize) -> &[u16] {
let (lo, hi) = (
self.roll_keeps_off[roll_id],
self.roll_keeps_off[roll_id + 1],
);
&self.roll_keeps[lo as usize..hi as usize]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_ids_line_up() {
let tables = DiceTables::new();
for keep_id in 0..KEEPS {
assert_eq!(tables.keep_id(tables.keep(keep_id)), keep_id);
}
for roll_id in 0..ROLLS {
let dice = tables.roll(roll_id);
assert_eq!(tables.roll_id(dice), roll_id);
assert_eq!(tables.keep_id(Keep::from(dice)), roll_id + SIZE5_OFFSET);
}
}
#[test]
fn probabilities_are_distributions() {
let tables = DiceTables::new();
let total: f64 = tables.roll_prob().iter().sum();
assert!((total - 1.0).abs() < 1e-12);
for keep_id in 0..KEEPS {
let successors = tables.keep_successors(keep_id);
let total: f64 = successors.iter().map(|&(_, p)| p).sum();
assert!(
(total - 1.0).abs() < 1e-12,
"keep {keep_id} sums to {total}"
);
}
for roll_id in 0..ROLLS {
let successors = tables.keep_successors(roll_id + SIZE5_OFFSET);
assert_eq!(successors.len(), 1);
assert_eq!(successors[0], (roll_id as u16, 1.0));
}
}
#[test]
fn transition_pairs_total_4368() {
let tables = DiceTables::new();
let succ: usize = (0..KEEPS).map(|k| tables.keep_successors(k).len()).sum();
let subs: usize = (0..ROLLS).map(|r| tables.roll_keeps(r).len()).sum();
assert_eq!(succ, 4368);
assert_eq!(subs, 4368);
}
}