diff options
| author | 2025-09-13 02:34:14 +0800 | |
|---|---|---|
| committer | 2025-09-13 02:34:14 +0800 | |
| commit | 00670619f3a9ac80683f1a1d751349e010c602dc (patch) | |
| tree | 242ccdbede9b38099ed773ea5ab725ded053843f | |
| parent | 93d462c41cf0ed359475f07578bac511a329c580 (diff) | |
| download | OneRoll-00670619f3a9ac80683f1a1d751349e010c602dc.tar.gz OneRoll-00670619f3a9ac80683f1a1d751349e010c602dc.zip | |
feat: introduce new dice modifiers for grouping and merging, enhance variable references and instruction sequences in parser
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | src/calculator.rs | 94 | ||||
| -rw-r--r-- | src/oneroll/grammar.pest | 14 | ||||
| -rw-r--r-- | src/parser.rs | 59 | ||||
| -rw-r--r-- | src/types.rs | 5 |
5 files changed, 183 insertions, 5 deletions
@@ -76,9 +76,11 @@ Dice Expression Syntax ---------------------- - `XdY`: Roll X dice with Y sides -- Modifiers: `kh`, `kl`, `dh`, `dl`, `!`, `r`, `ro` +- Modifiers: `kh`, `kl`, `dh`, `dl`, `!`, `e`, `r`, `ro`, `R`, `a`, `u`, `s`, `c`, `m`, `g`, `gs` - Mathematical operations: `+`, `-`, `*`, `/`, `^` - Comments: Add with `#`, e.g., `3d6 + 2 # Attack roll` +- Instruction sequences: Use `;` to separate multiple instructions +- Variable references: Use `$n` to reference the result of the nth instruction Examples -------- @@ -96,6 +98,18 @@ stats = oneroll.roll_statistics("3d6", 100) # Comment usage result = oneroll.roll("1d20 + 5 # Attack check") print(result["comment"]) + +# Instruction sequences +result = oneroll.roll("3d6; 1d20; 2d8") + +# Variable references +result = oneroll.roll("3d6; $1c6") # Count 6s in first roll + +# Group modifier +result = oneroll.roll("4d6g10") # Count groups >= 10 + +# Count modifier +result = oneroll.roll("5d6c6") # Count occurrences of 6 ``` Documentation diff --git a/src/calculator.rs b/src/calculator.rs index 07518b0..39934ad 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -154,6 +154,38 @@ impl DiceCalculator { let count = values.iter().filter(|&&v| v == *target).count() as i32; final_rolls = vec![vec![count]]; } + DiceModifier::Merge => { + // Merge with previous instruction rolls stored as $prev_rolls not yet available + // For MVP: no-op here; actual merge handled at sequence level + } + DiceModifier::Group { threshold, show_structure } => { + // Greedy grouping to reach >= threshold using sorted descending values + let mut values: Vec<i32> = final_rolls.iter().flatten().cloned().collect(); + values.sort_by(|a, b| b.cmp(a)); + let mut groups: Vec<Vec<i32>> = Vec::new(); + let mut current: Vec<i32> = Vec::new(); + let mut current_sum = 0; + for v in values { + current.push(v); + current_sum += v; + if current_sum >= *threshold { + groups.push(current.clone()); + current.clear(); + current_sum = 0; + } + } + let group_count = groups.len() as i32; + let mut details = format!("g{} -> {}", threshold, group_count); + if *show_structure { + details.push_str(": "); + details.push_str(&format!("{:?}", groups)); + if !current.is_empty() { + details.push_str(&format!(" - {:?}", current)); + } + } + // Represent as single result equal to number of groups + final_rolls = vec![vec![group_count]]; + } _ => {} } } @@ -170,6 +202,31 @@ impl DiceCalculator { details: format!("{}", n), comment: None, }), + Expression::VariableRef(index) => { + let key = format!("${}", index); + if let Some(value) = self.variables.get(&key) { + Ok(DiceResult { + expression: key.clone(), + total: value, + rolls: vec![vec![value]], + details: format!("var {} = {}", key, value), + comment: None, + }) + } else { + Err(DiceError::InvalidExpression(format!("未定义的变量引用: {}", key))) + } + } + Expression::Sequence(items) => { + let mut last: Option<DiceResult> = None; + for (i, item) in items.iter().enumerate() { + let res = self.evaluate_expression(item)?; + // 将每条指令的总和放入变量存储,键为 $1, $2... + let var_name = format!("${}", i + 1); + self.variables.set(&var_name, res.total); + last = Some(res); + } + Ok(last.unwrap_or(DiceResult { expression: String::new(), total: 0, rolls: vec![], details: String::new(), comment: None })) + } Expression::DiceRoll(dice) => { let rolls = self.roll_dice(dice)?; let total: i32 = rolls.iter().flatten().sum(); @@ -254,6 +311,35 @@ impl DiceCalculator { result.comment = comment.clone(); Ok(result) } + Expression::VariableRefWithModifiers(index, modifiers) => { + let key = format!("${}", index); + if let Some(value) = self.variables.get(&key) { + // Apply modifiers to the variable value + let mut rolls = vec![vec![value]]; + for modifier in modifiers { + match modifier { + DiceModifier::Count(target) => { + // For count modifier on variable, we count how many times the variable equals target + let count = if value == *target { 1 } else { 0 }; + rolls = vec![vec![count]]; + } + _ => { + // For other modifiers, we'll need to implement them + // For now, just keep the original value + } + } + } + Ok(DiceResult { + expression: format!("${}{}", index, self.modifiers_to_string(modifiers)), + total: rolls.iter().flatten().sum(), + rolls, + details: format!("var ${} = {} with modifiers", index, value), + comment: None, + }) + } else { + Err(DiceError::InvalidExpression(format!("未定义的变量引用: {}", key))) + } + } } } @@ -277,6 +363,14 @@ impl DiceCalculator { DiceModifier::Unique => result.push('u'), DiceModifier::Sort => result.push('s'), DiceModifier::Count(v) => result.push_str(&format!("c{}", v)), + DiceModifier::Merge => result.push('m'), + DiceModifier::Group { threshold, show_structure } => { + if *show_structure { + result.push_str(&format!("gs{}", threshold)); + } else { + result.push_str(&format!("g{}", threshold)); + } + } } } result diff --git a/src/oneroll/grammar.pest b/src/oneroll/grammar.pest index e5ce667..ac017b7 100644 --- a/src/oneroll/grammar.pest +++ b/src/oneroll/grammar.pest @@ -29,6 +29,8 @@ modifier = { | unique | sort | count + | merge + | group } explode = { "!" } explode_alias = { "e" } @@ -45,5 +47,15 @@ drop_low = { "dl" ~ number } unique = { "u" } sort = { "s" } count = { "c" ~ number } +merge = { "m" } +group = { "g" ~ ("s")? ~ number } + op = { "+" | "-" | "*" | "/" | "^" } -main = { SOI ~ dice_expr ~ EOI }
\ No newline at end of file + +// Instruction sequences and variable references +var_ref = { "$" ~ number ~ modifiers? } + +instruction = { dice_expr | var_ref } +instruction_list = { instruction ~ (";" ~ instruction)* } + +main = { SOI ~ instruction_list ~ EOI }
\ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs index 20cd992..73b5c49 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -16,15 +16,38 @@ impl DiceParser { .map_err(|e| DiceError::ParseError(e.to_string()))?; let pair = pairs.peek().unwrap(); - Self::parse_dice_expr(pair) + Self::parse_instruction_list(pair) } fn parse_dice_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expression, DiceError> { match pair.as_rule() { Rule::main => { - // main 规则包含 dice_expr let inner = pair.into_inner().next().unwrap(); - Self::parse_dice_expr(inner) + Self::parse_instruction_list(inner) + } + Rule::instruction_list => Self::parse_instruction_list(pair), + Rule::instruction => { + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::var_ref => { + let mut pairs = inner.into_inner(); + let n = pairs.next().unwrap().as_str().trim_start_matches('$').parse::<i32>() + .map_err(|_| DiceError::ParseError("无效的变量引用".to_string()))?; + if n <= 0 { return Err(DiceError::ParseError("变量引用必须>=1".to_string())); } + + let mut modifiers = Vec::new(); + if let Some(modifiers_pair) = pairs.next() { + for modifier_pair in modifiers_pair.into_inner() { + let modifier = Self::parse_modifier(modifier_pair)?; + modifiers.push(modifier); + } + } + + // Store variable reference with modifiers for special handling + Ok(Expression::VariableRefWithModifiers(n as usize, modifiers)) + } + _ => Self::parse_dice_expr(inner), + } } Rule::dice_expr => { let mut pairs = pair.into_inner(); @@ -168,6 +191,16 @@ impl DiceParser { .map_err(|_| DiceError::ParseError("无效的计数数值".to_string()))?; Ok(DiceModifier::Count(num)) } + Rule::merge => Ok(DiceModifier::Merge), + Rule::group => { + let s = inner.as_str(); + let show_structure = s.starts_with("gs"); + let inn = inner.into_inner(); + // last number + let num = inn.last().unwrap().as_str().parse::<i32>() + .map_err(|_| DiceError::ParseError("无效的分组阈值".to_string()))?; + Ok(DiceModifier::Group { threshold: num, show_structure }) + } _ => Err(DiceError::ParseError("未知的修饰符".to_string())), } } @@ -175,6 +208,26 @@ impl DiceParser { } } + fn parse_instruction_list(pair: pest::iterators::Pair<Rule>) -> Result<Expression, DiceError> { + match pair.as_rule() { + Rule::instruction_list => { + let mut exprs = Vec::new(); + for p in pair.into_inner() { + if p.as_rule() == Rule::instruction { + let e = Self::parse_dice_expr(p)?; + exprs.push(e); + } + } + if exprs.len() == 1 { + Ok(exprs.into_iter().next().unwrap()) + } else { + Ok(Expression::Sequence(exprs)) + } + } + _ => Self::parse_dice_expr(pair), + } + } + fn parse_comment(pair: pest::iterators::Pair<Rule>) -> Result<Option<String>, DiceError> { match pair.as_rule() { Rule::comment => { diff --git a/src/types.rs b/src/types.rs index 1d5d369..cb20908 100644 --- a/src/types.rs +++ b/src/types.rs @@ -34,6 +34,8 @@ pub enum DiceModifier { Unique, // u Sort, // s (sort results) Count(i32), // cV (count value V) + Merge, // m (merge with previous instruction) + Group { threshold: i32, show_structure: bool }, // gN / gsN } @@ -41,6 +43,9 @@ pub enum DiceModifier { pub enum Expression { Number(i32), DiceRoll(DiceRoll), + VariableRef(usize), // $n + VariableRefWithModifiers(usize, Vec<DiceModifier>), // $n with modifiers + Sequence(Vec<Expression>), // e1; e2; ... Add(Box<Expression>, Box<Expression>), Subtract(Box<Expression>, Box<Expression>), Multiply(Box<Expression>, Box<Expression>), |
