rdice-cli 0.1.1

Command-line dice roller powered by rdice-core
use rdice_core::DiceEngine;
use rdice_core::die::{DieKind, FaceValue};
use rdice_core::engine::{DieRoll, RollAnalysis, RollBatchResult};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RollOutputMode {
    Folded,
    Expanded,
}

pub fn print_dice(engine: &DiceEngine) {
    for die in engine.list_dice() {
        let kind = match die.kind {
            DieKind::Builtin => "builtin",
            DieKind::Custom => "custom",
        };
        let faces = format_die_faces(die.kind, &die.faces);
        println!("{} ({kind}): [{faces}]", die.name);
    }
}

fn format_die_faces(kind: DieKind, faces: &[FaceValue]) -> String {
    if kind == DieKind::Builtin
        && faces.len() > 20
        && faces.iter().enumerate().all(
            |(index, face)| matches!(face, FaceValue::Integer(value) if *value == index as i64 + 1),
        )
    {
        return format!("1..{}", faces.len());
    }

    faces
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}

pub fn print_roll_result(result: &RollBatchResult, modifiers: &[i64], mode: RollOutputMode) {
    match mode {
        RollOutputMode::Folded => print_folded_rolls(&result.rolls),
        RollOutputMode::Expanded => print_expanded_rolls(&result.rolls),
    }

    for modifier in modifiers {
        println!("modifier: {modifier:+}");
    }

    let modifier_sum: i64 = modifiers.iter().sum();
    if should_print_total(result, modifiers, mode) {
        match result.integer_sum {
            Some(sum) => println!("total: {}", sum + modifier_sum),
            None if !modifiers.is_empty() => println!("total: {modifier_sum}"),
            None => {}
        }
    }
}

pub fn print_analysis(analysis: &RollAnalysis, show_expected: bool, show_range: bool) {
    if show_expected {
        println!("expected: {}", analysis.expected_value);
    }
    if show_range {
        println!(
            "range: {}..{}",
            analysis.point_range.min, analysis.point_range.max
        );
    }
}

fn print_expanded_rolls(rolls: &[DieRoll]) {
    for roll in rolls {
        println!("#{} {}: {}", roll.roll_id, roll.die_name, roll.value);
    }
}

fn print_folded_rolls(rolls: &[DieRoll]) {
    let mut groups: Vec<(&str, Vec<&FaceValue>)> = Vec::new();
    for roll in rolls {
        if let Some((_, values)) = groups
            .iter_mut()
            .find(|(die_name, _)| *die_name == roll.die_name)
        {
            values.push(&roll.value);
        } else {
            groups.push((&roll.die_name, vec![&roll.value]));
        }
    }

    for (die_name, values) in groups {
        let shown_values = values
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(", ");
        let mut sum = 0_i64;
        let mut has_integer = false;
        for value in values {
            if let FaceValue::Integer(integer) = value {
                sum += integer;
                has_integer = true;
            }
        }
        if has_integer {
            println!("{die_name} x{}: {sum}", shown_values.split(", ").count());
        } else {
            println!("{die_name}: [{shown_values}]");
        }
    }
}

fn should_print_total(result: &RollBatchResult, modifiers: &[i64], mode: RollOutputMode) -> bool {
    let roll_count = result.rolls.len();
    let modifier_count = modifiers.len();
    match mode {
        RollOutputMode::Expanded => roll_count + modifier_count > 1,
        RollOutputMode::Folded => {
            let mut group_names: Vec<&str> = Vec::new();
            for roll in &result.rolls {
                if !group_names.contains(&roll.die_name.as_str()) {
                    group_names.push(&roll.die_name);
                }
            }
            let group_count = group_names.len();
            group_count + modifier_count > 1
        }
    }
}