tyche 0.3.1

Dice rolling and dice expression (with a syntax similar to FoundryVTT) parsing library
Documentation
use std::borrow::Cow;

use crate::dice::{
	roller::{FastRand, Roller},
	Dice, DieRoll, Modifier, Rolled,
};

#[test]
fn single_d20() {
	let dice = construct_plain(1, 20);
	let rolled = rolls_successfully_and_in_range(&dice);
	assert_eq!(rolled.rolls.len(), 1);
	assert_eq!(*rolled.dice, dice);
}

#[test]
fn double_d8() {
	let dice = construct_plain(2, 8);
	let rolled = rolls_successfully_and_in_range(&dice);
	assert_eq!(rolled.rolls.len(), 2);
	assert_eq!(*rolled.dice, dice);
}

#[test]
fn hundred_d42s() {
	let dice = construct_plain(100, 42);
	let rolled = rolls_successfully_and_in_range(&dice);
	assert_eq!(rolled.rolls.len(), 100);
	assert_eq!(*rolled.dice, dice);
}

#[test]
fn max_dice() {
	let dice = construct_plain(u8::MAX, u8::MAX);
	let rolled = rolls_successfully_and_in_range(&dice);
	assert_eq!(rolled.rolls.len(), u8::MAX as usize);
	assert_eq!(*rolled.dice, dice);
}

#[test]
fn exploding_max_d4s() {
	let dice = construct_exploding(u8::MAX, 4);
	let rolled = rolls_successfully_and_in_range(&dice);
	assert!(rolled.rolls.len() > u8::MAX as usize);

	let explosions = rolled
		.rolls
		.into_iter()
		.filter(DieRoll::is_additional)
		.collect::<Vec<_>>();
	assert!(!explosions.is_empty());
	assert!(explosions.len() < u8::MAX as usize);
	rolls_in_range(&explosions, 4);
}

#[test]
fn triple_d0() {
	let dice = construct_plain(3, 0);
	let rolled = rolls_successfully(&dice);
	assert!(rolled.rolls.len() == 3);
	assert!(rolled.rolls.iter().all(|roll| roll.val == 0));
}

#[test]
fn all_dice_sides_occur() {
	let dice = Dice::new(u8::MAX, 20);
	let mut rng = FastRand::default();
	let mut rolls = Vec::new();

	for _ in 1..=1000 {
		let rolled = rng.roll(&dice, true);
		assert!(rolled.is_ok());
		rolls.append(&mut rolled.unwrap().rolls);
	}

	rolls_in_range(&rolls, 20);

	for side in 1..=20 {
		assert!(rolls.iter().filter(|roll| roll.val == side).count() > 0);
	}
}

#[test]
fn dice_equality() {
	let da = Dice::new(4, 8);
	let db = Dice::new(4, 8);
	assert_eq!(da, db);
}

#[test]
fn dice_inequality() {
	let da = Dice::new(4, 8);
	let db = Dice::new(4, 20);
	assert_ne!(da, db);

	let da = Dice::new(4, 8);
	let db = Dice::new(2, 8);
	assert_ne!(da, db);

	let da = Dice::new(4, 8);
	let db = Dice::builder().count(4).sides(8).explode(None, true).build();
	assert_ne!(da, db);
}

#[test]
fn roll_equality() {
	let da = Dice::builder().count(4).sides(8).explode(None, true).build();
	let db = Dice::builder().count(4).sides(8).explode(None, true).build();
	let ra = Rolled {
		rolls: vec![
			DieRoll::new(4),
			DieRoll::new(5),
			DieRoll::new(8),
			DieRoll::new(8),
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(4)
			},
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(7)
			},
		],
		dice: Cow::Owned(da),
	};
	let rb = Rolled {
		rolls: vec![
			DieRoll::new(4),
			DieRoll::new(5),
			DieRoll::new(8),
			DieRoll::new(8),
			DieRoll {
				added_by: Some(db.modifiers[0]),
				..DieRoll::new(4)
			},
			DieRoll {
				added_by: Some(db.modifiers[0]),
				..DieRoll::new(7)
			},
		],
		dice: Cow::Owned(db),
	};
	assert_eq!(ra, rb);
}

#[test]
fn roll_inequality() {
	let da = Dice::builder().count(4).sides(8).explode(None, true).build();
	let db = Dice::builder().count(4).sides(8).explode(None, true).build();
	let ra = Rolled {
		rolls: vec![
			DieRoll::new(4),
			DieRoll::new(5),
			DieRoll::new(8),
			DieRoll::new(8),
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(4)
			},
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(7)
			},
		],
		dice: Cow::Owned(da),
	};
	let rb = Rolled {
		rolls: vec![
			DieRoll::new(4),
			DieRoll::new(6),
			DieRoll::new(8),
			DieRoll::new(8),
			DieRoll {
				added_by: Some(db.modifiers[0]),
				..DieRoll::new(4)
			},
			DieRoll {
				added_by: Some(db.modifiers[0]),
				..DieRoll::new(7)
			},
		],
		dice: Cow::Owned(db),
	};
	assert_ne!(ra, rb);

	let da = Dice::builder().count(4).sides(8).explode(None, true).build();
	let db = Dice::new(4, 8);
	let ra = Rolled {
		rolls: vec![
			DieRoll::new(4),
			DieRoll::new(5),
			DieRoll::new(8),
			DieRoll::new(8),
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(4)
			},
			DieRoll {
				added_by: Some(da.modifiers[0]),
				..DieRoll::new(7)
			},
		],
		dice: Cow::Owned(da),
	};
	let rb = Rolled {
		rolls: vec![DieRoll::new(4), DieRoll::new(5), DieRoll::new(8), DieRoll::new(8)],
		dice: Cow::Owned(db),
	};
	assert_ne!(ra, rb);
}

fn construct_plain(count: u8, sides: u8) -> Dice {
	let dice = Dice::new(count, sides);
	assert_eq!(dice.count, count);
	assert_eq!(dice.sides, sides);
	assert_eq!(dice.modifiers.len(), 0);
	dice
}

fn construct_exploding(count: u8, sides: u8) -> Dice {
	let dice = Dice::builder().count(count).sides(sides).explode(None, true).build();
	assert_eq!(dice.count, count);
	assert_eq!(dice.sides, sides);
	assert_eq!(dice.modifiers.len(), 1);
	assert!(matches!(dice.modifiers[0], Modifier::Explode { .. }));
	dice
}

fn rolls_successfully(dice: &Dice) -> Rolled {
	let result = FastRand::default().roll(dice, true);
	assert!(result.is_ok());
	result.unwrap()
}

fn rolls_successfully_and_in_range(dice: &Dice) -> Rolled {
	let rolled = rolls_successfully(dice);
	rolls_in_range(&rolled.rolls, rolled.dice.sides);
	rolled
}

fn rolls_in_range(rolls: &[DieRoll], sides: u8) {
	assert!(rolls.iter().all(|roll| roll.val >= 1 && roll.val <= sides));
}