use crate::context::Context;
use crate::error::{BotError, DiceRollingError};
use crate::parser::dice::{Amount, Element, Operator};
use itertools::Itertools;
use std::convert::TryFrom;
use std::fmt;

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DicePoolQuality {
    TenAgain,
    NineAgain,
    EightAgain,
    Rote,
    ChanceDie,
    NoExplode,
}

impl fmt::Display for DicePoolQuality {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DicePoolQuality::TenAgain => write!(f, "ten-again"),
            DicePoolQuality::NineAgain => write!(f, "nine-again"),
            DicePoolQuality::EightAgain => write!(f, "eight-again"),
            DicePoolQuality::Rote => write!(f, "rote quality"),
            DicePoolQuality::ChanceDie => write!(f, "chance die"),
            DicePoolQuality::NoExplode => write!(f, "no roll-agains"),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct DicePoolModifiers {
    pub(crate) success_on: i32,
    pub(crate) exceptional_on: i32,
    pub(crate) quality: DicePoolQuality,
}

impl DicePoolModifiers {
    pub fn default() -> DicePoolModifiers {
        DicePoolModifiers {
            success_on: 8,
            exceptional_on: 5,
            quality: DicePoolQuality::TenAgain,
        }
    }

    pub fn custom_quality(quality: DicePoolQuality) -> DicePoolModifiers {
        let success_on = if quality != DicePoolQuality::ChanceDie {
            8
        } else {
            10
        };
        DicePoolModifiers {
            success_on: success_on,
            exceptional_on: 5,
            quality: quality,
        }
    }

    pub fn custom_exceptional_on(exceptional_on: i32) -> DicePoolModifiers {
        DicePoolModifiers {
            success_on: 8,
            exceptional_on: exceptional_on,
            quality: DicePoolQuality::TenAgain,
        }
    }

    pub fn custom(quality: DicePoolQuality, exceptional_on: i32) -> DicePoolModifiers {
        DicePoolModifiers {
            success_on: 8,
            exceptional_on: exceptional_on,
            quality: quality,
        }
    }
}

#[derive(Debug, PartialEq)]
pub struct DicePool {
    pub(crate) amounts: Vec<Amount>,
    pub(crate) sides: i32,
    pub(crate) modifiers: DicePoolModifiers,
}

impl DicePool {
    pub fn easy_pool(dice_amount: i32, quality: DicePoolQuality) -> DicePool {
        DicePool {
            amounts: vec![Amount {
                operator: Operator::Plus,
                element: Element::Number(dice_amount),
            }],
            sides: 10,
            modifiers: DicePoolModifiers::custom_quality(quality),
        }
    }

    pub fn easy_with_modifiers(dice_amount: i32, modifiers: DicePoolModifiers) -> DicePool {
        DicePool {
            amounts: vec![Amount {
                operator: Operator::Plus,
                element: Element::Number(dice_amount),
            }],
            sides: 10,
            modifiers: modifiers,
        }
    }

    pub fn new(amounts: Vec<Amount>, modifiers: DicePoolModifiers) -> DicePool {
        DicePool {
            amounts: amounts,
            sides: 10, //TODO make configurable
            //TODO make configurable
            modifiers: modifiers,
        }
    }

    pub fn chance_die() -> DicePool {
        DicePool::easy_pool(1, DicePoolQuality::ChanceDie)
    }
}

///The result of a successfully executed roll of a dice pool. Does not
///contain the heavy information of the DicePool instance.
pub struct RolledDicePool {
    pub(crate) num_dice: i32,
    pub(crate) roll: DicePoolRoll,
    pub(crate) modifiers: DicePoolModifiers,
}

impl RolledDicePool {
    fn from(pool: &DicePool, num_dice: i32, rolls: Vec<i32>) -> RolledDicePool {
        RolledDicePool {
            modifiers: pool.modifiers,
            num_dice: num_dice,
            roll: DicePoolRoll {
                rolls: rolls,
                modifiers: pool.modifiers,
            },
        }
    }
}

impl fmt::Display for RolledDicePool {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let dice_plural = if self.num_dice == 1 { "die" } else { "dice" };
        write!(
            f,
            "{} {} ({}, exceptional on {} successes)",
            self.num_dice, dice_plural, self.modifiers.quality, self.modifiers.exceptional_on
        )
    }
}

///Store all rolls of the dice pool dice into one struct.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DicePoolRoll {
    modifiers: DicePoolModifiers,
    rolls: Vec<i32>,
}

/// Amount of dice to display before cutting off and showing "and X
/// more", so we don't spam the room with huge messages.
const MAX_DISPLAYED_ROLLS: usize = 15;

fn fmt_rolls(pool: &DicePoolRoll) -> String {
    let rolls = pool.rolls();
    if rolls.len() > MAX_DISPLAYED_ROLLS {
        let shown_amount = rolls.into_iter().take(MAX_DISPLAYED_ROLLS).join(", ");
        format!(
            "{}, and {} more",
            shown_amount,
            rolls.len() - MAX_DISPLAYED_ROLLS
        )
    } else {
        rolls.into_iter().join(", ")
    }
}

fn fmt_for_failure(pool: &DicePoolRoll) -> String {
    match pool.modifiers.quality {
        //There should only be 1 die in a chance die roll.
        DicePoolQuality::ChanceDie if pool.rolls().first() == Some(&1) => {
            String::from("dramatic failure!")
        }
        _ => String::from("failure!"),
    }
}

impl DicePoolRoll {
    pub fn rolls(&self) -> &[i32] {
        &self.rolls
    }

    pub fn successes(&self) -> i32 {
        let successes: usize = self
            .rolls
            .iter()
            .filter(|&roll| *roll >= self.modifiers.success_on)
            .count();

        i32::try_from(successes).unwrap_or(0)
    }

    pub fn is_exceptional(&self) -> bool {
        self.successes() >= self.modifiers.exceptional_on
    }
}

/// Attach a Context to a dice pool. Needed for database access.
pub struct DicePoolWithContext<'a>(pub &'a DicePool, pub &'a Context<'a>);

impl fmt::Display for DicePoolRoll {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let successes = self.successes();
        if successes > 0 {
            let success_msg = if self.is_exceptional() {
                format!("{} successes (exceptional!)", successes)
            } else {
                format!("{} successes", successes)
            };

            write!(f, "{} ({})", success_msg, fmt_rolls(&self))?;
        } else {
            write!(f, "{} ({})", fmt_for_failure(&self), fmt_rolls(&self))?;
        }

        Ok(())
    }
}

trait DieRoller {
    fn roll_number(&mut self, sides: i32) -> i32;
}

///A version of DieRoller that uses a rand::Rng to roll numbers.
struct RngDieRoller<R: rand::Rng>(R);

impl<R: rand::Rng> DieRoller for RngDieRoller<R> {
    fn roll_number(&mut self, sides: i32) -> i32 {
        self.0.gen_range(1..=sides)
    }
}

///Roll a die in the pool, that "explodes" on a given number or higher. Dice will keep
///being rolled until the result is lower than the explode number, which is normally 10.
///Statistically speaking, usually one result will be returned from this function.
fn roll_exploding_die<R: DieRoller>(
    roller: &mut R,
    sides: i32,
    explode_on_or_higher: i32,
) -> Vec<i32> {
    let mut results = vec![];
    loop {
        let roll = roller.roll_number(sides);
        results.push(roll);
        if roll < explode_on_or_higher {
            break;
        }
    }
    results
}

///A die with the rote quality is re-rolled once if the roll fails. Otherwise, it obeys
///all normal rules (re-roll 10s). Re-rolled dice are appended to the result set, so we
///can keep track of the actual dice that were rolled.
fn roll_rote_die<R: DieRoller>(roller: &mut R, sides: i32, success_on: i32) -> Vec<i32> {
    let mut rolls = roll_exploding_die(roller, sides, 10);

    if rolls.len() == 1 && rolls[0] < success_on {
        rolls.append(&mut roll_exploding_die(roller, sides, 10));
    }

    rolls
}

///Roll a single die in the pool, potentially rolling additional dice depending on  pool
///behavior. The default ten-again will "explode" the die if the result is 10 (repeatedly, if
///there are multiple 10s). Nine- and eight-again will explode similarly if the result is
///at least that number. Rote quality will re-roll a failure once, while also exploding
///on 10. The function returns a Vec of all rolled dice (usually 1).
fn roll_die<R: DieRoller>(roller: &mut R, pool: &DicePool) -> Vec<i32> {
    let mut results = vec![];
    let sides = pool.sides;
    let success_on = pool.modifiers.success_on;

    match pool.modifiers.quality {
        DicePoolQuality::TenAgain => results.append(&mut roll_exploding_die(roller, sides, 10)),
        DicePoolQuality::NineAgain => results.append(&mut roll_exploding_die(roller, sides, 9)),
        DicePoolQuality::EightAgain => results.append(&mut roll_exploding_die(roller, sides, 8)),
        DicePoolQuality::Rote => results.append(&mut roll_rote_die(roller, sides, success_on)),
        DicePoolQuality::ChanceDie | DicePoolQuality::NoExplode => {
            results.push(roller.roll_number(sides))
        }
    }

    results
}

fn roll_dice<'a, R: DieRoller>(pool: &DicePool, num_dice: i32, roller: &mut R) -> Vec<i32> {
    (0..num_dice)
        .flat_map(|_| roll_die(roller, &pool))
        .collect()
}

///Roll the dice in a dice pool, according to behavior documented in the various rolling
///methods.
pub async fn roll_pool(pool: &DicePoolWithContext<'_>) -> Result<RolledDicePool, BotError> {
    if pool.0.amounts.len() > 100 {
        return Err(DiceRollingError::ExpressionTooLarge.into());
    }

    let num_dice = crate::logic::calculate_dice_amount(&pool.0.amounts, &pool.1).await?;
    let mut roller = RngDieRoller(rand::thread_rng());

    if num_dice > 0 {
        let rolls = roll_dice(&pool.0, num_dice, &mut roller);
        Ok(RolledDicePool::from(&pool.0, num_dice, rolls))
    } else {
        let chance_die = DicePool::chance_die();
        let pool = DicePoolWithContext(&chance_die, &pool.1);
        let rolls = roll_dice(&pool.0, 1, &mut roller);
        Ok(RolledDicePool::from(&pool.0, 1, rolls))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db::sqlite::Database;
    use crate::db::Variables;
    use url::Url;

    macro_rules! dummy_room {
        () => {
            crate::context::RoomContext {
                id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
                display_name: "displayname".to_owned(),
                secure: false,
            }
        };
    }

    ///Instead of being random, generate a series of numbers we have complete
    ///control over.
    struct SequentialDieRoller {
        results: Vec<i32>,
        position: usize,
    }

    impl SequentialDieRoller {
        fn new(results: Vec<i32>) -> SequentialDieRoller {
            SequentialDieRoller {
                results: results,
                position: 0,
            }
        }
    }

    impl DieRoller for SequentialDieRoller {
        fn roll_number(&mut self, _sides: i32) -> i32 {
            let roll = self.results[self.position];
            self.position += 1;
            roll
        }
    }

    //Sanity checks
    #[test]
    pub fn chance_die_has_success_on_10_test() {
        assert_eq!(10, DicePool::chance_die().modifiers.success_on);
    }

    #[test]
    pub fn non_chance_die_has_success_on_8_test() {
        fn check_success_on(quality: DicePoolQuality) {
            let modifiers = DicePoolModifiers::custom_quality(quality);
            let amount = vec![Amount {
                operator: Operator::Plus,
                element: Element::Number(1),
            }];
            assert_eq!(8, DicePool::new(amount, modifiers).modifiers.success_on);
        }

        check_success_on(DicePoolQuality::TenAgain);
        check_success_on(DicePoolQuality::NineAgain);
        check_success_on(DicePoolQuality::EightAgain);
        check_success_on(DicePoolQuality::Rote);
        check_success_on(DicePoolQuality::NoExplode);
    }

    //Dice rolling tests.
    #[test]
    pub fn ten_again_test() {
        let mut roller = SequentialDieRoller::new(vec![10, 8, 1]);
        let rolls = roll_exploding_die(&mut roller, 10, 10);
        assert_eq!(vec![10, 8], rolls);
    }

    #[test]
    pub fn nine_again_test() {
        let mut roller = SequentialDieRoller::new(vec![10, 9, 8, 1]);
        let rolls = roll_exploding_die(&mut roller, 10, 9);
        assert_eq!(vec![10, 9, 8], rolls);
    }

    #[test]
    pub fn eight_again_test() {
        let mut roller = SequentialDieRoller::new(vec![10, 9, 8, 8, 1]);
        let rolls = roll_exploding_die(&mut roller, 10, 8);
        assert_eq!(vec![10, 9, 8, 8, 1], rolls);
    }

    #[test]
    pub fn rote_quality_fail_then_succeed_test() {
        let mut roller = SequentialDieRoller::new(vec![5, 8, 1]);
        let rolls = roll_rote_die(&mut roller, 10, 8);
        assert_eq!(vec![5, 8], rolls);
    }

    #[test]
    pub fn rote_quality_fail_twice_test() {
        let mut roller = SequentialDieRoller::new(vec![5, 6, 10]);
        let rolls = roll_rote_die(&mut roller, 10, 8);
        assert_eq!(vec![5, 6], rolls);
    }

    #[test]
    pub fn rote_quality_fail_then_explode_test() {
        let mut roller = SequentialDieRoller::new(vec![5, 10, 8, 1]);
        let rolls = roll_rote_die(&mut roller, 10, 8);
        assert_eq!(vec![5, 10, 8], rolls);
    }

    #[test]
    pub fn rote_quality_obeys_success_on_test() {
        //With success_on = 8, should only roll once.
        let mut roller = SequentialDieRoller::new(vec![8, 7]);
        let rolls = roll_rote_die(&mut roller, 10, 8);
        assert_eq!(vec![8], rolls);

        //With success_on = 9, we should re-roll if it's an 8.
        roller = SequentialDieRoller::new(vec![8, 7]);
        let rolls = roll_rote_die(&mut roller, 10, 9);
        assert_eq!(vec![8, 7], rolls);
    }

    #[test]
    fn dice_pool_modifiers_chance_die_test() {
        let modifiers = DicePoolModifiers::custom_quality(DicePoolQuality::ChanceDie);
        assert_eq!(10, modifiers.success_on);
    }

    #[test]
    fn dice_pool_modifiers_default_sanity_check() {
        let modifiers = DicePoolModifiers::default();
        assert_eq!(8, modifiers.success_on);
        assert_eq!(5, modifiers.exceptional_on);
        assert_eq!(DicePoolQuality::TenAgain, modifiers.quality);
    }

    #[test]
    pub fn no_explode_roll_test() {
        let pool = DicePool::easy_pool(1, DicePoolQuality::NoExplode);
        let mut roller = SequentialDieRoller::new(vec![10, 8]);
        let roll = roll_dice(&pool, 1, &mut roller);
        assert_eq!(vec![10], roll);
    }

    #[test]
    fn number_of_dice_equality_test() {
        let num_dice = 5;
        let rolls = vec![1, 2, 3, 4, 5];
        let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
        let rolled_pool = RolledDicePool::from(&pool, num_dice, rolls);
        assert_eq!(5, rolled_pool.num_dice);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
    async fn rejects_large_expression_test() {
        let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
        let homeserver = Url::parse("http://example.com").unwrap();
        let db = Database::new(db_path.path().to_str().unwrap())
            .await
            .unwrap();

        let ctx = Context {
            user: crate::models::User::default(),
            db: db,
            matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
            room: dummy_room!(),
            username: "username",
            message_body: "message",
        };

        let mut amounts = vec![];

        for _ in 0..500 {
            amounts.push(Amount {
                operator: Operator::Plus,
                element: Element::Number(1),
            });
        }

        let pool = DicePool::new(amounts, DicePoolModifiers::default());
        let pool_with_ctx = DicePoolWithContext(&pool, &ctx);
        let result = roll_pool(&pool_with_ctx).await;
        assert!(matches!(
            result,
            Err(BotError::DiceRollingError(
                DiceRollingError::ExpressionTooLarge
            ))
        ));
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
    async fn converts_to_chance_die_test() {
        let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
        crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
            .await
            .unwrap();

        let db = Database::new(db_path.path().to_str().unwrap())
            .await
            .unwrap();

        let homeserver = Url::parse("http://example.com").unwrap();
        let ctx = Context {
            user: crate::models::User::default(),
            db: db,
            matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
            room: dummy_room!(),
            username: "username",
            message_body: "message",
        };

        let mut amounts = vec![];

        amounts.push(Amount {
            operator: Operator::Plus,
            element: Element::Number(-1),
        });

        let pool = DicePool::new(amounts, DicePoolModifiers::default());
        let pool_with_ctx = DicePoolWithContext(&pool, &ctx);
        let result = roll_pool(&pool_with_ctx).await;
        assert!(result.is_ok());

        let roll = result.unwrap();
        assert_eq!(DicePoolQuality::ChanceDie, roll.modifiers.quality);
        assert_eq!(1, roll.num_dice);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
    async fn can_resolve_variables_test() {
        let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
        crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
            .await
            .unwrap();

        let db = Database::new(db_path.path().to_str().unwrap())
            .await
            .unwrap();

        let homeserver = Url::parse("http://example.com").unwrap();
        let ctx = Context {
            user: crate::models::User::default(),
            db: db.clone(),
            matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
            room: dummy_room!(),
            username: "username",
            message_body: "message",
        };

        db.set_user_variable(&ctx.username, &ctx.room.id.as_str(), "myvariable", 10)
            .await
            .expect("could not set myvariable to 10");

        let amounts = vec![Amount {
            operator: Operator::Plus,
            element: Element::Variable("myvariable".to_owned()),
        }];

        let pool = DicePool::new(amounts, DicePoolModifiers::default());

        assert_eq!(
            crate::logic::calculate_dice_amount(&pool.amounts, &ctx)
                .await
                .unwrap(),
            10
        );
    }

    //DicePool tests
    #[test]
    fn easy_pool_chance_die_test() {
        let pool = DicePool::easy_pool(1, DicePoolQuality::ChanceDie);
        assert_eq!(10, pool.modifiers.success_on);
    }

    #[test]
    fn easy_pool_quality_test() {
        fn check_quality(quality: DicePoolQuality) {
            let pool = DicePool::easy_pool(1, quality);
            assert_eq!(quality, pool.modifiers.quality);
        }

        check_quality(DicePoolQuality::TenAgain);
        check_quality(DicePoolQuality::NineAgain);
        check_quality(DicePoolQuality::EightAgain);
        check_quality(DicePoolQuality::Rote);
        check_quality(DicePoolQuality::ChanceDie);
        check_quality(DicePoolQuality::NoExplode);
    }

    #[test]
    fn is_successful_on_equal_test() {
        let result = DicePoolRoll {
            rolls: vec![8],
            modifiers: DicePoolModifiers {
                exceptional_on: 5,
                success_on: 8,
                quality: DicePoolQuality::TenAgain,
            },
        };

        assert_eq!(1, result.successes());
    }

    #[test]
    fn chance_die_success_test() {
        let result = DicePoolRoll {
            rolls: vec![10],
            modifiers: DicePoolModifiers {
                exceptional_on: 5,
                success_on: 10,
                quality: DicePoolQuality::ChanceDie,
            },
        };

        assert_eq!(1, result.successes());
    }

    #[test]
    fn chance_die_fail_test() {
        let result = DicePoolRoll {
            rolls: vec![9],
            modifiers: DicePoolModifiers {
                exceptional_on: 5,
                success_on: 10,
                quality: DicePoolQuality::ChanceDie,
            },
        };

        assert_eq!(0, result.successes());
    }

    #[test]
    fn is_exceptional_test() {
        let result = DicePoolRoll {
            rolls: vec![8, 8, 9, 10, 8],
            modifiers: DicePoolModifiers {
                exceptional_on: 5,
                success_on: 8,
                quality: DicePoolQuality::TenAgain,
            },
        };

        assert_eq!(5, result.successes());
        assert_eq!(true, result.is_exceptional());
    }

    #[test]
    fn is_not_exceptional_test() {
        let result = DicePoolRoll {
            rolls: vec![8, 8, 9, 10],
            modifiers: DicePoolModifiers::default(),
        };

        assert_eq!(4, result.successes());
        assert_eq!(false, result.is_exceptional());
    }

    //Format tests
    #[test]
    fn formats_rolled_dice_pool_single_die() {
        let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
        let rolled_pool = RolledDicePool::from(&pool, 1, vec![1]);
        let message = format!("{}", rolled_pool);
        assert!(message.starts_with("1 die"));
    }

    #[test]
    fn formats_rolled_dice_pool_multiple_dice() {
        let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
        let rolled_pool = RolledDicePool::from(&pool, 2, vec![1, 2]);
        let message = format!("{}", rolled_pool);
        assert!(message.starts_with("2 dice"));
    }

    #[test]
    fn formats_rolled_dice_pool_zero_dice() {
        let pool = DicePool::easy_pool(5, DicePoolQuality::TenAgain);
        let rolled_pool = RolledDicePool::from(&pool, 0, vec![]);
        let message = format!("{}", rolled_pool);
        assert!(message.starts_with("0 dice"));
    }

    #[test]
    fn formats_dramatic_failure_test() {
        let result = DicePoolRoll {
            rolls: vec![1],
            modifiers: DicePoolModifiers::custom_quality(DicePoolQuality::ChanceDie),
        };

        assert_eq!("dramatic failure!", fmt_for_failure(&result));
    }

    #[test]
    fn formats_regular_failure_when_not_chance_die_test() {
        let result = DicePoolRoll {
            rolls: vec![1],
            modifiers: DicePoolModifiers {
                quality: DicePoolQuality::TenAgain,
                exceptional_on: 5,
                success_on: 10,
            },
        };

        assert_eq!("failure!", fmt_for_failure(&result));
    }

    #[test]
    fn formats_lots_of_dice_test() {
        let result = DicePoolRoll {
            modifiers: DicePoolModifiers::default(),
            rolls: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        };

        assert_eq!(
            "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, and 4 more",
            fmt_rolls(&result)
        );
    }

    #[test]
    fn shows_more_than_10_dice_test() {
        //Make sure we display more than 10 dice when below the display limit (15).
        let result = DicePoolRoll {
            modifiers: DicePoolModifiers::default(),
            rolls: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
        };

        assert_eq!(
            "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14",
            fmt_rolls(&result)
        );
    }

    #[test]
    fn shows_exactly_15_dice_test() {
        //If we are at format limit (15), make sure all are shown
        let result = DicePoolRoll {
            modifiers: DicePoolModifiers::default(),
            rolls: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
        };

        assert_eq!(
            "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15",
            fmt_rolls(&result)
        );
    }
}