aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2025-09-12 04:02:02 +0800
committer简律纯 <i@jyunko.cn>2025-09-12 04:02:02 +0800
commit0288d0956330d5ac8db48b752240f723e8703929 (patch)
tree0297dee9f8166af0a856dd3a1057ad5f25f14c6a /src
parent5135876b5e2a6c40232414ea0b7eb875fa225cf0 (diff)
downloadOneRoll-0288d0956330d5ac8db48b752240f723e8703929.tar.gz
OneRoll-0288d0956330d5ac8db48b752240f723e8703929.zip
feat: initial basic roll features
Diffstat (limited to 'src')
-rw-r--r--src/calculator.rs212
-rw-r--r--src/errors.rs17
-rw-r--r--src/lib.rs26
-rw-r--r--src/oneroll/__init__.py241
-rw-r--r--src/oneroll/__main__.py335
-rw-r--r--src/oneroll/_core.pyi247
-rw-r--r--src/oneroll/grammar.pest43
-rw-r--r--src/oneroll/tui.py251
-rw-r--r--src/parser.rs161
-rw-r--r--src/pyo3_template/__init__.py0
-rw-r--r--src/pyo3_template/__main__.py8
-rw-r--r--src/pyo3_template/_core.pyi3
-rw-r--r--src/python_bindings.rs154
-rw-r--r--src/types.rs65
14 files changed, 1744 insertions, 19 deletions
diff --git a/src/calculator.rs b/src/calculator.rs
new file mode 100644
index 0000000..9611a2b
--- /dev/null
+++ b/src/calculator.rs
@@ -0,0 +1,212 @@
+use crate::errors::DiceError;
+use crate::types::{DiceModifier, DiceResult, DiceRoll, Expression, VariableStore};
+use rand::Rng;
+
+pub struct DiceCalculator {
+ pub variables: VariableStore,
+}
+
+impl DiceCalculator {
+ pub fn new() -> Self {
+ Self {
+ variables: VariableStore::new(),
+ }
+ }
+
+ pub fn roll_dice(&mut self, dice: &DiceRoll) -> Result<Vec<Vec<i32>>, DiceError> {
+ if dice.count <= 0 || dice.sides <= 0 {
+ return Err(DiceError::InvalidExpression(
+ "骰子数量和面数必须大于0".to_string(),
+ ));
+ }
+
+ let mut rolls = Vec::new();
+
+ for _ in 0..dice.count {
+ let mut roll = rand::random::<u32>() % dice.sides as u32 + 1;
+ let mut final_rolls = vec![roll as i32];
+
+ // handle exploded throwing
+ for modifier in &dice.modifiers {
+ match modifier {
+ DiceModifier::Explode => {
+ while roll == dice.sides as u32 {
+ roll = rand::random::<u32>() % dice.sides as u32 + 1;
+ final_rolls.push(roll as i32);
+ }
+ }
+ _ => {}
+ }
+ }
+
+ // handle reroll
+ for modifier in &dice.modifiers {
+ match modifier {
+ DiceModifier::Reroll(threshold) => {
+ if final_rolls.iter().any(|&r| r <= *threshold) {
+ let new_roll = rand::random::<u32>() % dice.sides as u32 + 1;
+ final_rolls = vec![new_roll as i32];
+ }
+ }
+ DiceModifier::RerollOnce(threshold) => {
+ if let Some(pos) = final_rolls.iter().position(|&r| r <= *threshold) {
+ let new_roll = rand::random::<u32>() % dice.sides as u32 + 1;
+ final_rolls[pos] = new_roll as i32;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ rolls.push(final_rolls);
+ }
+
+ // handle high/low and discard high/low
+ let mut final_rolls = rolls;
+ for modifier in &dice.modifiers {
+ match modifier {
+ DiceModifier::KeepHigh(n) => {
+ let all_values: Vec<i32> = final_rolls.iter().flatten().cloned().collect();
+ let mut sorted = all_values;
+ sorted.sort_by(|a, b| b.cmp(a));
+ final_rolls = sorted.iter().take(*n as usize).map(|&v| vec![v]).collect();
+ }
+ DiceModifier::KeepLow(n) => {
+ let all_values: Vec<i32> = final_rolls.iter().flatten().cloned().collect();
+ let mut sorted = all_values;
+ sorted.sort();
+ final_rolls = sorted.iter().take(*n as usize).map(|&v| vec![v]).collect();
+ }
+ DiceModifier::DropHigh(n) => {
+ let all_values: Vec<i32> = final_rolls.iter().flatten().cloned().collect();
+ let mut sorted = all_values;
+ sorted.sort_by(|a, b| b.cmp(a));
+ final_rolls = sorted.iter().skip(*n as usize).map(|&v| vec![v]).collect();
+ }
+ DiceModifier::DropLow(n) => {
+ let all_values: Vec<i32> = final_rolls.iter().flatten().cloned().collect();
+ let mut sorted = all_values;
+ sorted.sort();
+ final_rolls = sorted.iter().skip(*n as usize).map(|&v| vec![v]).collect();
+ }
+ _ => {}
+ }
+ }
+
+ Ok(final_rolls)
+ }
+
+ pub fn evaluate_expression(&mut self, expr: &Expression) -> Result<DiceResult, DiceError> {
+ match expr {
+ Expression::Number(n) => Ok(DiceResult {
+ expression: n.to_string(),
+ total: *n,
+ rolls: vec![],
+ details: format!("{}", n),
+ comment: None,
+ }),
+ Expression::DiceRoll(dice) => {
+ let rolls = self.roll_dice(dice)?;
+ let total: i32 = rolls.iter().flatten().sum();
+ let details = format!(
+ "{}d{}{} = {} (详情: {:?})",
+ dice.count,
+ dice.sides,
+ self.modifiers_to_string(&dice.modifiers),
+ total,
+ rolls
+ );
+ Ok(DiceResult {
+ expression: format!("{}d{}", dice.count, dice.sides),
+ total,
+ rolls,
+ details,
+ comment: None,
+ })
+ }
+ Expression::Add(left, right) => {
+ let left_result = self.evaluate_expression(left)?;
+ let right_result = self.evaluate_expression(right)?;
+ Ok(DiceResult {
+ expression: format!("({}) + ({})", left_result.expression, right_result.expression),
+ total: left_result.total + right_result.total,
+ rolls: [left_result.rolls, right_result.rolls].concat(),
+ details: format!("{} + {} = {}", left_result.total, right_result.total, left_result.total + right_result.total),
+ comment: None,
+ })
+ }
+ Expression::Subtract(left, right) => {
+ let left_result = self.evaluate_expression(left)?;
+ let right_result = self.evaluate_expression(right)?;
+ Ok(DiceResult {
+ expression: format!("({}) - ({})", left_result.expression, right_result.expression),
+ total: left_result.total - right_result.total,
+ rolls: [left_result.rolls, right_result.rolls].concat(),
+ details: format!("{} - {} = {}", left_result.total, right_result.total, left_result.total - right_result.total),
+ comment: None,
+ })
+ }
+ Expression::Multiply(left, right) => {
+ let left_result = self.evaluate_expression(left)?;
+ let right_result = self.evaluate_expression(right)?;
+ Ok(DiceResult {
+ expression: format!("({}) * ({})", left_result.expression, right_result.expression),
+ total: left_result.total * right_result.total,
+ rolls: [left_result.rolls, right_result.rolls].concat(),
+ details: format!("{} * {} = {}", left_result.total, right_result.total, left_result.total * right_result.total),
+ comment: None,
+ })
+ }
+ Expression::Divide(left, right) => {
+ let left_result = self.evaluate_expression(left)?;
+ let right_result = self.evaluate_expression(right)?;
+ if right_result.total == 0 {
+ return Err(DiceError::CalculationError("除零错误".to_string()));
+ }
+ Ok(DiceResult {
+ expression: format!("({}) / ({})", left_result.expression, right_result.expression),
+ total: left_result.total / right_result.total,
+ rolls: [left_result.rolls, right_result.rolls].concat(),
+ details: format!("{} / {} = {}", left_result.total, right_result.total, left_result.total / right_result.total),
+ comment: None,
+ })
+ }
+ Expression::Power(left, right) => {
+ let left_result = self.evaluate_expression(left)?;
+ let right_result = self.evaluate_expression(right)?;
+ let result = left_result.total.pow(right_result.total as u32);
+ Ok(DiceResult {
+ expression: format!("({}) ^ ({})", left_result.expression, right_result.expression),
+ total: result,
+ rolls: [left_result.rolls, right_result.rolls].concat(),
+ details: format!("{} ^ {} = {}", left_result.total, right_result.total, result),
+ comment: None,
+ })
+ }
+ Expression::Paren(expr) => self.evaluate_expression(expr),
+ Expression::WithComment(expr, comment) => {
+ let mut result = self.evaluate_expression(expr)?;
+ result.comment = comment.clone();
+ Ok(result)
+ }
+ }
+ }
+
+
+ pub fn modifiers_to_string(&self, modifiers: &[DiceModifier]) -> String {
+ let mut result = String::new();
+ for modifier in modifiers {
+ match modifier {
+ DiceModifier::Explode => result.push('!'),
+ DiceModifier::Reroll(n) => result.push_str(&format!("r{}", n)),
+ DiceModifier::RerollOnce(n) => result.push_str(&format!("ro{}", n)),
+ DiceModifier::KeepHigh(n) => result.push_str(&format!("kh{}", n)),
+ DiceModifier::KeepLow(n) => result.push_str(&format!("kl{}", n)),
+ DiceModifier::DropHigh(n) => result.push_str(&format!("dh{}", n)),
+ DiceModifier::DropLow(n) => result.push_str(&format!("dl{}", n)),
+ }
+ }
+ result
+ }
+
+}
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..8a20863
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,17 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum DiceError {
+ #[error("解析错误: {0}")]
+ ParseError(String),
+ #[error("计算错误: {0}")]
+ CalculationError(String),
+ #[error("无效的骰子表达式: {0}")]
+ InvalidExpression(String),
+}
+
+impl std::convert::From<DiceError> for pyo3::PyErr {
+ fn from(err: DiceError) -> pyo3::PyErr {
+ pyo3::PyErr::new::<pyo3::exceptions::PyValueError, _>(err.to_string())
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 166cb09..68821bd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,16 +1,26 @@
+//! OneRoll - High-performance dice expression parser
+//!
+//! This is a dice expression parser implemented in Rust and bound to Python through PyO3.
+//! Supports complex dice expression parsing, various modifiers and mathematical operations.
+
use pyo3::prelude::*;
-#[pyfunction]
-fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
- Ok((a + b).to_string())
-}
+mod errors;
+mod types;
+mod calculator;
+mod parser;
+mod python_bindings;
-#[pyclass]
-pub struct Base {}
+pub use errors::DiceError;
+pub use types::{DiceResult, DiceRoll, DiceModifier, Expression};
+pub use calculator::DiceCalculator;
+pub use parser::DiceParser;
+pub use python_bindings::{OneRoll, roll_dice, roll_simple};
#[pymodule]
fn _core(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
- m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
- m.add_class::<Base>()?;
+ m.add_function(wrap_pyfunction!(roll_dice, m)?)?;
+ m.add_function(wrap_pyfunction!(roll_simple, m)?)?;
+ m.add_class::<OneRoll>()?;
Ok(())
} \ No newline at end of file
diff --git a/src/oneroll/__init__.py b/src/oneroll/__init__.py
new file mode 100644
index 0000000..a57cfee
--- /dev/null
+++ b/src/oneroll/__init__.py
@@ -0,0 +1,241 @@
+"""
+OneRoll - High Performance Dice Expression Parser
+
+A dice expression parser implemented in Rust and bound to Python through PyO3.
+Supports complex dice expression parsing, various modifiers and mathematical operations.
+
+Main functions:
+- Basic Dice Rolling (XdY)
+- Mathematical operations (+, -, *, /, ^)
+- Modifiers support (!, kh, kl, dh, dl, r, ro)
+- Bracket support
+- Complete error handling
+
+Example of usage:
+# Basic use
+import oneroll
+result = oneroll.roll("3d6 + 2")
+print(result.total) # output total points
+
+# Use the OneRoll class
+roller = oneroll.OneRoll()
+result = roller.roll("4d6kh3")
+
+# Simple throw
+total = oneroll.roll_simple(3, 6)
+"""
+
+from typing import Dict, List, Any, Union, Optional
+from ._core import (
+ OneRoll as _OneRoll,
+ roll_dice as _roll_dice,
+ roll_simple as _roll_simple,
+)
+
+__version__ = "1.0.1"
+__author__ = "HsiangNianian"
+__description__ = "高性能骰子表达式解析器"
+
+# 重新导出主要类和函数,提供更友好的接口
+class OneRoll:
+ """
+ OneRoll 骰子投掷器类
+
+ 提供面向对象的骰子投掷接口,支持复杂表达式和各种修饰符。
+
+ 示例:
+ roller = OneRoll()
+ result = roller.roll("3d6 + 2")
+ simple_result = roller.roll_simple(3, 6)
+ modifier_result = roller.roll_with_modifiers(4, 6, ["kh3"])
+ """
+
+ def __init__(self):
+ """初始化 OneRoll 实例"""
+ self._roller = _OneRoll()
+
+ def roll(self, expression: str) -> Dict[str, Any]:
+ """
+ 解析并计算骰子表达式
+
+ Args:
+ expression: 骰子表达式字符串,如 "3d6 + 2", "4d6kh3", "2d6! # 攻击投掷"
+
+ Returns:
+ 包含以下键的字典:
+ - expression: 表达式字符串
+ - total: 总点数
+ - rolls: 投掷结果列表
+ - details: 详细信息
+ - comment: 用户注释
+
+ Raises:
+ ValueError: 当表达式无效时
+
+ Example:
+ result = roller.roll("3d6 + 2")
+ print(f"总点数: {result['total']}")
+ print(f"详情: {result['details']}")
+ """
+ return self._roller.roll(expression)
+
+ def roll_simple(self, dice_count: int, dice_sides: int) -> int:
+ """
+ 简单骰子投掷
+
+ Args:
+ dice_count: 骰子数量
+ dice_sides: 骰子面数
+
+ Returns:
+ 总点数
+
+ Raises:
+ ValueError: 当参数无效时
+
+ Example:
+ total = roller.roll_simple(3, 6) # 投掷 3d6
+ """
+ return self._roller.roll_simple(dice_count, dice_sides)
+
+ def roll_with_modifiers(
+ self, dice_count: int, dice_sides: int, modifiers: List[str]
+ ) -> Dict[str, Any]:
+ """
+ 带修饰符的骰子投掷
+
+ Args:
+ dice_count: 骰子数量
+ dice_sides: 骰子面数
+ modifiers: 修饰符列表,如 ["kh3", "!"]
+
+ Returns:
+ 包含 total, rolls, details 的字典
+
+ Raises:
+ ValueError: 当参数或修饰符无效时
+
+ Example:
+ result = roller.roll_with_modifiers(4, 6, ["kh3"]) # 4d6kh3
+ """
+ return self._roller.roll_with_modifiers(dice_count, dice_sides, modifiers)
+
+
+# 便捷函数
+def roll(expression: str) -> Dict[str, Any]:
+ """
+ 解析并计算骰子表达式(便捷函数)
+
+ Args:
+ expression: 骰子表达式字符串,支持注释
+
+ Returns:
+ 投掷结果字典,包含 comment 字段
+
+ Example:
+ result = oneroll.roll("3d6 + 2 # 攻击投掷")
+ print(result["comment"]) # 输出: "攻击投掷"
+ """
+ return _roll_dice(expression)
+
+
+def roll_simple(dice_count: int, dice_sides: int) -> int:
+ """
+ 简单骰子投掷(便捷函数)
+
+ Args:
+ dice_count: 骰子数量
+ dice_sides: 骰子面数
+
+ Returns:
+ 总点数
+
+ Example:
+ total = oneroll.roll_simple(3, 6)
+ """
+ return _roll_simple(dice_count, dice_sides)
+
+
+def roll_multiple(expression: str, times: int) -> List[Dict[str, Any]]:
+ """
+ 多次投掷同一个表达式
+
+ Args:
+ expression: 骰子表达式字符串
+ times: 投掷次数
+
+ Returns:
+ 投掷结果列表
+
+ Example:
+ results = oneroll.roll_multiple("3d6", 10)
+ totals = [r['total'] for r in results]
+ """
+ return [_roll_dice(expression) for _ in range(times)]
+
+
+def roll_statistics(expression: str, times: int) -> Dict[str, Union[int, float]]:
+ """
+ 统计多次投掷的结果
+
+ Args:
+ expression: 骰子表达式字符串
+ times: 投掷次数
+
+ Returns:
+ 包含统计信息的字典
+
+ Example:
+ stats = oneroll.roll_statistics("3d6", 100)
+ print(f"平均值: {stats['mean']:.2f}")
+ """
+ results = roll_multiple(expression, times)
+ totals = [r["total"] for r in results]
+
+ return {
+ "min": min(totals),
+ "max": max(totals),
+ "mean": sum(totals) / len(totals),
+ "total": sum(totals),
+ "count": len(totals),
+ "results": totals,
+ }
+
+
+# 常用骰子表达式
+class CommonRolls:
+ """常用骰子表达式常量"""
+
+ # D&D 常用投掷
+ D20 = "1d20"
+ D20_ADVANTAGE = "2d20kh1"
+ D20_DISADVANTAGE = "2d20kl1"
+
+ # 属性投掷
+ ATTRIBUTE_ROLL = "4d6kh3"
+
+ # 伤害投掷
+ D6_DAMAGE = "1d6"
+ D8_DAMAGE = "1d8"
+ D10_DAMAGE = "1d10"
+ D12_DAMAGE = "1d12"
+
+ # 生命值
+ HIT_POINTS_D6 = "1d6"
+ HIT_POINTS_D8 = "1d8"
+ HIT_POINTS_D10 = "1d10"
+ HIT_POINTS_D12 = "1d12"
+
+
+# 导出公共接口
+__all__ = [
+ "OneRoll",
+ "roll",
+ "roll_simple",
+ "roll_multiple",
+ "roll_statistics",
+ "CommonRolls",
+ "__version__",
+ "__author__",
+ "__description__",
+]
diff --git a/src/oneroll/__main__.py b/src/oneroll/__main__.py
new file mode 100644
index 0000000..f454df8
--- /dev/null
+++ b/src/oneroll/__main__.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+"""
+OneRoll interactive dice roll program
+
+Supports command line parameters and interactive mode.
+
+Example of usage:
+# Direct throw
+python -m oneroll "3d6 + 2"
+
+# Interactive mode
+python -m oneroll
+
+# Statistical Mode
+python -m oneroll --stats "3d6" --times 100
+"""
+
+import sys
+import argparse
+from typing import List, Dict, Any
+from rich.console import Console
+from rich.table import Table
+from rich.panel import Panel
+from rich.text import Text
+from rich.prompt import Prompt, Confirm
+from rich.progress import Progress, SpinnerColumn, TextColumn
+
+from . import OneRoll, roll, roll_simple, roll_multiple, roll_statistics, CommonRolls
+
+console = Console()
+
+class OneRollCLI:
+ """OneRoll command line interface"""
+
+ def __init__(self):
+ self.roller = OneRoll()
+ self.history: List[Dict[str, Any]] = []
+
+ def print_result(self, result: Dict[str, Any], expression: str = None):
+ """Pretty print the dice roll result"""
+ if expression is None:
+ expression = result.get('expression', 'Unknown')
+
+ # Create result panel
+ total = result['total']
+ details = result['details']
+ rolls = result['rolls']
+
+ # Select color based on result value
+ if total >= 15:
+ color = "green"
+ elif total >= 10:
+ color = "yellow"
+ else:
+ color = "red"
+
+ # Build display text
+ text = Text()
+ text.append(f"🎲 {expression}\n", style="bold blue")
+ text.append(f"总点数: ", style="bold")
+ text.append(f"{total}", style=f"bold {color}")
+ text.append(f"\n详情: {details}", style="white")
+
+ if rolls:
+ text.append(f"\n投掷结果: ", style="bold")
+ text.append(f"{rolls}", style="cyan")
+
+ # Display comment
+ comment = result.get("comment", "")
+ if comment:
+ text.append(f"\n注释: ", style="bold")
+ text.append(f"{comment}", style="italic blue")
+
+ panel = Panel(text, title="Dice Roll Result", border_style=color)
+ console.print(panel)
+
+ def print_statistics(self, stats: Dict[str, Any], expression: str):
+ """Print statistics information"""
+ table = Table(title=f"统计结果: {expression} (投掷 {stats['count']} 次)")
+ table.add_column("统计项", style="cyan")
+ table.add_column("数值", style="green")
+
+ table.add_row("最小值", str(stats['min']))
+ table.add_row("最大值", str(stats['max']))
+ table.add_row("平均值", f"{stats['mean']:.2f}")
+ table.add_row("总和", str(stats['total']))
+ table.add_row("投掷次数", str(stats['count']))
+
+ console.print(table)
+
+ def print_history(self):
+ """Print dice roll history"""
+ if not self.history:
+ console.print("暂无投掷历史", style="yellow")
+ return
+
+ table = Table(title="投掷历史")
+ table.add_column("序号", style="cyan")
+ table.add_column("表达式", style="green")
+ table.add_column("总点数", style="yellow")
+ table.add_column("注释", style="blue")
+ table.add_column("详情", style="white")
+
+ for i, result in enumerate(self.history[-10:], 1): # only show last 30 times
+ table.add_row(
+ str(i),
+ result.get('expression', 'Unknown'),
+ str(result['total']),
+ result.get('comment', ''),
+ result['details']
+ )
+
+ console.print(table)
+
+ def show_help(self):
+ """show help information"""
+ help_text = """
+🎲 OneRoll 骰子表达式解析器
+
+支持的表达式格式:
+• 基本骰子: 3d6, 1d20, 2d10
+• 数学运算: 3d6 + 2, 2d6 * 3, (2d6 + 3) * 2
+• 修饰符:
+ - ! 爆炸骰子: 2d6!
+ - kh 取高: 4d6kh3
+ - kl 取低: 4d6kl2
+ - dh 丢弃高: 5d6dh1
+ - dl 丢弃低: 5d6dl1
+ - r 重投: 3d6r1
+ - ro 条件重投: 4d6ro1
+• 注释: 在表达式末尾使用 # 添加注释
+
+常用命令:
+• help - 显示帮助
+• history - 显示投掷历史
+• stats <表达式> <次数> - 统计投掷
+• clear - 清空历史
+• quit/exit - 退出程序
+
+常用表达式:
+• d20 - 1d20
+• advantage - 2d20kh1 (优势)
+• disadvantage - 2d20kl1 (劣势)
+• attribute - 4d6kh3 (属性投掷)
+ """
+
+ console.print(Panel(help_text, title="帮助信息", border_style="blue"))
+
+ def interactive_mode(self):
+ """Interactive Mode"""
+ console.print(Panel.fit("🎲 OneRoll 交互式掷骰程序", style="bold blue"))
+ console.print("输入 'help' 查看帮助,输入 'quit' 退出程序\n")
+
+ while True:
+ try:
+ user_input = Prompt.ask("🎲 请输入骰子表达式").strip()
+
+ if not user_input:
+ continue
+
+ # handle special command
+ if user_input.lower() in ['quit', 'exit', 'q']:
+ if Confirm.ask("确定要退出吗?"):
+ break
+ continue
+
+ if user_input.lower() == 'help':
+ self.show_help()
+ continue
+
+ if user_input.lower() == 'history':
+ self.print_history()
+ continue
+
+ if user_input.lower() == 'clear':
+ self.history.clear()
+ console.print("历史已清空", style="green")
+ continue
+
+ if user_input.lower().startswith('stats '):
+ parts = user_input.split()
+ if len(parts) >= 3:
+ expression = parts[1]
+ try:
+ times = int(parts[2])
+ if times > 1000:
+ console.print("统计次数不能超过1000次", style="red")
+ continue
+
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ console=console
+ ) as progress:
+ task = progress.add_task(f"正在统计 {expression}...", total=None)
+ stats = roll_statistics(expression, times)
+ progress.stop()
+
+ self.print_statistics(stats, expression)
+ except ValueError:
+ console.print("统计次数必须是数字", style="red")
+ except Exception as e:
+ console.print(f"统计错误: {e}", style="red")
+ else:
+ console.print("用法: stats <表达式> <次数>", style="red")
+ continue
+
+ # resolve regular expression's alias
+ expression = self._resolve_expression_alias(user_input)
+
+ # execute roll
+ try:
+ result = roll(expression)
+ self.history.append(result)
+ self.print_result(result, user_input)
+ except Exception as e:
+ console.print(f"错误: {e}", style="red")
+
+ except KeyboardInterrupt:
+ if Confirm.ask("\n确定要退出吗?"):
+ break
+ except EOFError:
+ break
+
+ def _resolve_expression_alias(self, user_input: str) -> str:
+ """resolve regular expression's alias"""
+ aliases = {
+ 'd20': CommonRolls.D20,
+ 'advantage': CommonRolls.D20_ADVANTAGE,
+ 'disadvantage': CommonRolls.D20_DISADVANTAGE,
+ 'attr': CommonRolls.ATTRIBUTE_ROLL,
+ 'attribute': CommonRolls.ATTRIBUTE_ROLL,
+ }
+
+ return aliases.get(user_input.lower(), user_input)
+
+ def run(self, args):
+ """run tui mode"""
+ if args.tui:
+ # start tui
+ try:
+ from .tui import run_tui
+ run_tui()
+ except ImportError:
+ console.print("TUI 模式需要安装 textual: pip install textual", style="red")
+ sys.exit(1)
+ except Exception as e:
+ console.print(f"TUI 启动失败: {e}", style="red")
+ sys.exit(1)
+
+ elif args.expression:
+ # single roll mode
+ try:
+ result = roll(args.expression)
+ self.print_result(result)
+ except Exception as e:
+ console.print(f"错误: {e}", style="red")
+ sys.exit(1)
+
+ elif args.stats:
+ # stats mode
+ try:
+ times = args.times or 100
+ if times > 10000:
+ console.print("统计次数不能超过10000次", style="red")
+ sys.exit(1)
+
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ console=console
+ ) as progress:
+ task = progress.add_task(f"正在统计 {args.stats}...", total=None)
+ stats = roll_statistics(args.stats, times)
+ progress.stop()
+
+ self.print_statistics(stats, args.stats)
+ except Exception as e:
+ console.print(f"错误: {e}", style="red")
+ sys.exit(1)
+
+ else:
+ # interactive mode
+ self.interactive_mode()
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="OneRoll 骰子表达式解析器",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+使用示例:
+ %(prog)s "3d6 + 2" # 单次投掷
+ %(prog)s --stats "3d6" --times 100 # 统计模式
+ %(prog)s --tui # 终端用户界面
+ %(prog)s # 交互式模式
+ """
+ )
+
+ parser.add_argument(
+ 'expression',
+ nargs='?',
+ help='骰子表达式,如 "3d6 + 2"'
+ )
+
+ parser.add_argument(
+ '--stats',
+ help='统计模式,指定要统计的表达式'
+ )
+
+ parser.add_argument(
+ '--times',
+ type=int,
+ default=100,
+ help='统计次数,默认100次'
+ )
+
+ parser.add_argument(
+ '--version',
+ action='version',
+ version='OneRoll 0.0.1'
+ )
+
+ parser.add_argument(
+ '--tui',
+ action='store_true',
+ help='启动终端用户界面 (TUI)'
+ )
+
+ args = parser.parse_args()
+
+ cli = OneRollCLI()
+ cli.run(args)
+
+if __name__ == '__main__':
+ main() \ No newline at end of file
diff --git a/src/oneroll/_core.pyi b/src/oneroll/_core.pyi
new file mode 100644
index 0000000..7916261
--- /dev/null
+++ b/src/oneroll/_core.pyi
@@ -0,0 +1,247 @@
+from typing import Dict, List, Any, Union
+
+DiceResult = Dict[str, Any]
+RollHistory = List[DiceResult]
+ModifierList = List[str]
+
+def roll_dice(expression: str) -> DiceResult:
+ """
+ Analyze and calculate dice expressions
+
+ Args:
+ expression: Dice expression string, supports the following formats:
+ - Basic dice: "3d6", "1d20", "2d10"
+ - Mathematical operations: "3d6 + 2", "2d6 * 3", "(2d6 + 3) * 2"
+ - Modifiers: "2d6!", "4d6kh3", "5d6dl1", "3d6r1", "4d6ro1"
+
+ Returns:
+ A dictionary containing the following keys:
+ - expression: str - expression string
+ - total: int - Total points
+ - rolls: List[List[int]] - List of throw results
+ - details: str - details
+ - comment: str - User comment
+
+ Raises:
+ ValueError: When the expression is invalid
+
+ Example:
+ result = roll_dice("3d6 + 2")
+ print(result["total"]) # output total points
+ """
+ ...
+
+def roll_simple(dice_count: int, dice_sides: int) -> int:
+ """
+ Simple dice throw
+
+ Args:
+ dice_count: The number of dice must be greater than 0
+ dice_sides: The number of dice faces must be greater than 0
+
+ Returns:
+ Total points
+
+ Raises:
+ ValueError: When the parameter is invalid
+
+ Example:
+ total = roll_simple(3, 6) # Roll 3 6-sided dice
+ """
+ ...
+
+class OneRoll:
+ """
+ OneRoll dice thrower category
+
+ Provides an object-oriented dice throwing interface, supporting complex expressions and various modifiers.
+ """
+
+ def __init__(self) -> None:
+ """
+ Initialize OneRoll Object
+
+ Example:
+ roller = OneRoll()
+ """
+ ...
+
+ def roll(self, expression: str) -> DiceResult:
+ """
+ Analyze and calculate dice expressions
+
+ Args:
+ expression: Dice expression string, supports the following formats:
+ - Basic dice: "3d6", "1d20", "2d10"
+ - Mathematical operations: "3d6 + 2", "2d6 * 3", "(2d6 + 3) * 2"
+ - Modifiers: "2d6!", "4d6kh3", "5d6dl1", "3d6r1", "4d6ro1"
+
+ Returns:
+ A dictionary containing the following keys:
+ - expression: str - expression string
+ - total: int - Total points
+ - rolls: List[List[int]] - List of throw results
+ - details: str - details
+ - comment: str - User comment
+
+ Raises:
+ ValueError: When the expression is invalid
+
+ Example:
+ roller = OneRoll()
+ result = roller.roll("3d6 + 2")
+ print(f"Total points: {result['total']}")
+ """
+ ...
+
+ def roll_simple(self, dice_count: int, dice_sides: int) -> int:
+ """
+ Simple dice throw
+
+ Args:
+ dice_count: The number of dice must be greater than 0
+ dice_sides: The number of dice faces must be greater than 0
+
+ Returns:
+ Total points
+
+ Raises:
+ ValueError: When the parameter is invalid
+
+ Example:
+ roller = OneRoll()
+ total = roller.roll_simple(3, 6) # Throw 3 6-sided dice
+ """
+ ...
+
+ def roll_with_modifiers(
+ self,
+ dice_count: int,
+ dice_sides: int,
+ modifiers: ModifierList
+ ) -> DiceResult:
+ """
+ Dice throw with modifier
+
+ Args:
+ dice_count: The number of dice must be greater than 0
+ dice_sides: The number of dice faces must be greater than 0
+ modifiers: modifier list, supports the following formats:
+ - "!" - Explosion dice
+ - "r<number>" - Re-submit, such as "r1"
+ - "ro<number>" - Conditional re-submission, such as "ro1"
+ - "kh<number>" - Take the height, such as "kh3"
+ - "kl<number>" - Take the low, such as "kl2"
+ - "dh<number>" - discard the height, such as "dh1"
+ - "dl<number>" - discard low, such as "dl1"
+
+ Returns:
+ A dictionary containing the following keys:
+ - total: int - Total points
+ - rolls: List[List[int]] - List of throw results
+ - details: str - details
+
+ Raises:
+ ValueError: When the parameter or modifier is invalid
+
+ Example:
+ roller = OneRoll()
+ result = roller.roll_with_modifiers(4, 6, ["kh3"]) # 4d6kh3
+ print(f"Total points: {result['total']}")
+ """
+ ...
+
+def is_valid_expression(expression: str) -> bool:
+ """
+ Check if the expression is valid
+
+ Args:
+ expression: The expression string to check
+
+ Returns:
+ Return True if the expression is valid, otherwise return False
+
+ Example:
+ if is_valid_expression("3d6 + 2"):
+ result = roll_dice("3d6 + 2")
+ """
+ ...
+
+def parse_expression(expression: str) -> Dict[str, Any]:
+ """
+ Parses expressions but not throwing
+
+ Args:
+ expression: The expression string to parse
+
+ Returns:
+ Analytical results dictionary
+
+ Raises:
+ ValueError: When the expression is invalid
+ """
+ ...
+
+class RollStatistics:
+ """Throw statistics"""
+ min: int
+ max: int
+ mean: float
+ total: int
+ count: int
+ results: List[int]
+
+def roll_multiple(expression: str, times: int) -> RollHistory:
+ """
+ Throw the same expression multiple times
+
+ Args:
+ expression: dice expression string
+ times: The number of throws must be greater than 0
+
+ Returns:
+ Throw result list
+
+ Raises:
+ ValueError: When the parameter is invalid
+
+ Example:
+ results = roll_multiple("3d6", 10)
+ totals = [r["total"] for r in results]
+ """
+ ...
+
+def roll_statistics(expression: str, times: int) -> RollStatistics:
+ """
+ Statistics of multiple throws
+
+ Args:
+ expression: dice expression string
+ times: The number of throws must be greater than 0
+
+ Returns:
+ Object containing statistics
+
+ Raises:
+ ValueError: When the parameter is invalid
+
+ Example:
+ stats = roll_statistics("3d6", 100)
+ print(f"Average: {stats.mean:.2f}")
+ """
+ ...
+
+class CommonRolls:
+ """Commonly used dice expression constants"""
+ D20: str
+ D20_ADVANTAGE: str
+ D20_DISADVANTAGE: str
+ ATTRIBUTE_ROLL: str
+ D6_DAMAGE: str
+ D8_DAMAGE: str
+ D10_DAMAGE: str
+ D12_DAMAGE: str
+ HIT_POINTS_D6: str
+ HIT_POINTS_D8: str
+ HIT_POINTS_D10: str
+ HIT_POINTS_D12: str \ No newline at end of file
diff --git a/src/oneroll/grammar.pest b/src/oneroll/grammar.pest
new file mode 100644
index 0000000..01b97e9
--- /dev/null
+++ b/src/oneroll/grammar.pest
@@ -0,0 +1,43 @@
+WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
+
+number = @{ "-"? ~ ("0" | ('1'..'9' ~ ('0'..'9')*)) }
+
+comment = { "#" ~ (!"\n" ~ ANY)* }
+
+dice_expr = { dice_term ~ (op ~ dice_term)* ~ comment? }
+dice_term = {
+ dice_roll
+ | paren_expr
+ | number
+}
+
+paren_expr = { "(" ~ dice_expr ~ ")" }
+
+dice_roll = {
+ number ~ "d" ~ dice_sides ~ modifiers?
+}
+
+dice_sides = @{ number }
+
+modifiers = { modifier+ }
+modifier = {
+ explode
+ | reroll
+ | reroll_once
+ | keep_high
+ | keep_low
+ | drop_high
+ | drop_low
+}
+
+explode = { "!" }
+reroll = { "r" ~ number }
+reroll_once = { "ro" ~ number }
+keep_high = { "kh" ~ number }
+keep_low = { "kl" ~ number }
+drop_high = { "dh" ~ number }
+drop_low = { "dl" ~ number }
+
+op = { "+" | "-" | "*" | "/" | "^" }
+
+main = { SOI ~ dice_expr ~ EOI } \ No newline at end of file
diff --git a/src/oneroll/tui.py b/src/oneroll/tui.py
new file mode 100644
index 0000000..9e4e4c9
--- /dev/null
+++ b/src/oneroll/tui.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+"""
+OneRoll Terminal User Interface (TUI)
+
+An interactive dice roll interface created using textual.
+"""
+
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.widgets import Header, Footer, Input, Button, Static, DataTable, Tabs, Tab
+from textual.reactive import reactive
+from textual.message import Message
+from typing import List, Dict, Any
+import json
+from datetime import datetime
+
+from . import OneRoll, roll, roll_statistics, CommonRolls
+
+class RollResult(Message):
+ """Throw result message"""
+
+ def __init__(self, result: Dict[str, Any], expression: str) -> None:
+ self.result = result
+ self.expression = expression
+ super().__init__()
+
+class ExpressionInput(Input):
+ """Expression input box"""
+
+ def __init__(self) -> None:
+ super().__init__(
+ placeholder="输入骰子表达式,如 3d6 + 2",
+ id="expression_input"
+ )
+
+ def on_key(self, event) -> None:
+ if event.key == "enter":
+ self.post_message(RollResult(roll(self.value), self.value))
+ self.value = ""
+
+class QuickRollButton(Button):
+ """Quick Throw Button"""
+
+ def __init__(self, label: str, expression: str) -> None:
+ self.expression = expression
+ super().__init__(label, id=f"quick_{expression}")
+
+ def on_button_pressed(self) -> None:
+ self.post_message(RollResult(roll(self.expression), self.expression))
+
+class RollHistory(DataTable):
+ """Throw History Table"""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.add_columns("时间", "表达式", "总点数", "详情")
+
+ def add_roll(self, result: Dict[str, Any], expression: str) -> None:
+ """Add throw record"""
+ time_str = datetime.now().strftime("%H:%M:%S")
+ self.add_row(
+ time_str,
+ expression,
+ str(result["total"]),
+ result["details"]
+ )
+ # keep table at bottom
+ self.scroll_end()
+
+class StatisticsPanel(Static):
+ """Statistics Panel"""
+
+ def __init__(self) -> None:
+ super().__init__("统计功能", id="stats_panel")
+
+ def show_statistics(self, expression: str, times: int = 100) -> None:
+ """Show statistics information"""
+ try:
+ stats = roll_statistics(expression, times)
+ stats_text = f"""
+统计结果: {expression} (投掷 {stats['count']} 次)
+
+最小值: {stats['min']}
+最大值: {stats['max']}
+平均值: {stats['mean']:.2f}
+总和: {stats['total']}
+ """
+ self.update(stats_text)
+ except Exception as e:
+ self.update(f"统计错误: {e}")
+
+class RollDisplay(Static):
+ """The throwing result shows """
+
+ def __init__(self) -> None:
+ super().__init__("等待投掷...", id="roll_display")
+
+ def show_result(self, result: Dict[str, Any], expression: str) -> None:
+ """Display throwing result"""
+ total = result["total"]
+ details = result["details"]
+ rolls = result["rolls"]
+
+ # a kind of color selection based on result
+ if total >= 15:
+ color = "green"
+ elif total >= 10:
+ color = "yellow"
+ else:
+ color = "red"
+
+ display_text = f"""[bold blue]🎲 {expression}[/bold blue]
+
+[bold]总点数:[/bold] [bold {color}]{total}[/bold {color}]
+[bold]详情:[/bold] {details}
+
+[bold]投掷结果:[/bold] {rolls}"""
+
+ # show the comment
+ comment = result.get("comment", "")
+ if comment:
+ display_text += f"\n\n[bold]注释:[/bold] [italic blue]{comment}[/italic blue]"
+
+ self.update(display_text)
+
+class OneRollTUI(App):
+ """OneRoll 终端用户界面"""
+
+ CSS = """
+ Screen {
+ layout: vertical;
+ }
+
+ #expression_input {
+ margin: 1;
+ }
+
+ #roll_display {
+ height: 8;
+ border: solid $primary;
+ margin: 1;
+ padding: 1;
+ }
+
+ #stats_panel {
+ height: 8;
+ border: solid $secondary;
+ margin: 1;
+ padding: 1;
+ }
+
+ #history_table {
+ height: 10;
+ margin: 1;
+ }
+
+ .quick_buttons {
+ height: 3;
+ margin: 1;
+ }
+
+ Button {
+ margin: 1;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ """UI components"""
+ yield Header()
+
+ with Container():
+ yield ExpressionInput()
+
+ with Horizontal(classes="quick_buttons"):
+ yield QuickRollButton("D20", CommonRolls.D20)
+ yield QuickRollButton("优势", CommonRolls.D20_ADVANTAGE)
+ yield QuickRollButton("劣势", CommonRolls.D20_DISADVANTAGE)
+ yield QuickRollButton("属性", CommonRolls.ATTRIBUTE_ROLL)
+ yield QuickRollButton("3D6", "3d6")
+ yield QuickRollButton("2D6", "2d6")
+
+ yield RollDisplay()
+
+ with Tabs():
+ with Tab("历史记录", id="history_tab"):
+ yield RollHistory(id="history_table")
+
+ with Tab("统计", id="stats_tab"):
+ yield StatisticsPanel()
+
+ yield Footer()
+
+ def on_mount(self) -> None:
+ """Initialization during interface mount"""
+ self.title = "OneRoll 骰子投掷器"
+ self.sub_title = "高性能骰子表达式解析器"
+
+ # Set focus to the input box
+ self.query_one(ExpressionInput).focus()
+
+ def on_roll_result(self, message: RollResult) -> None:
+ # display result
+ roll_display = self.query_one(RollDisplay)
+ roll_display.show_result(message.result, message.expression)
+
+ # add history
+ history_table = self.query_one(RollHistory)
+ history_table.add_roll(message.result, message.expression)
+
+ def on_key(self, event) -> None:
+ if event.key == "ctrl+q":
+ self.exit()
+ elif event.key == "ctrl+h":
+ self.show_help()
+ elif event.key == "ctrl+s":
+ self.show_statistics()
+
+ def show_help(self) -> None:
+ help_text = """
+OneRoll 骰子投掷器
+
+支持的表达式格式:
+• 基本骰子: 3d6, 1d20, 2d10
+• 数学运算: 3d6 + 2, 2d6 * 3, (2d6 + 3) * 2
+• 修饰符:
+ - ! 爆炸骰子: 2d6!
+ - kh 取高: 4d6kh3
+ - kl 取低: 4d6kl2
+ - dh 丢弃高: 5d6dh1
+ - dl 丢弃低: 5d6dl1
+ - r 重投: 3d6r1
+ - ro 条件重投: 4d6ro1
+
+快捷键:
+• Ctrl+Q - 退出程序
+• Ctrl+H - 显示帮助
+• Ctrl+S - 显示统计
+• Enter - 执行投掷
+ """
+
+ self.notify(help_text, title="帮助信息", timeout=10)
+
+ def show_statistics(self) -> None:
+ self.notify("统计功能开发中...", title="统计")
+
+def run_tui():
+ app = OneRollTUI()
+ app.run()
+
+if __name__ == "__main__":
+ run_tui()
diff --git a/src/parser.rs b/src/parser.rs
new file mode 100644
index 0000000..8f4633e
--- /dev/null
+++ b/src/parser.rs
@@ -0,0 +1,161 @@
+use crate::errors::DiceError;
+use crate::types::{DiceModifier, DiceRoll, Expression};
+
+mod oneroll {
+ include!(concat!(env!("OUT_DIR"), "/oneroll_grammar.rs"));
+}
+
+use oneroll::{Grammar, Rule};
+use pest::Parser;
+
+pub struct DiceParser;
+
+impl DiceParser {
+ pub fn parse_expression(input: &str) -> Result<Expression, DiceError> {
+ let pairs = Grammar::parse(Rule::main, input)
+ .map_err(|e| DiceError::ParseError(e.to_string()))?;
+
+ let pair = pairs.peek().unwrap();
+ Self::parse_dice_expr(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)
+ }
+ Rule::dice_expr => {
+ let mut pairs = pair.into_inner();
+ let mut expr = Self::parse_dice_term(pairs.next().unwrap())?;
+
+ while let Some(pair) = pairs.next() {
+ match pair.as_rule() {
+ Rule::op => {
+ let op = pair.as_str();
+ let right = Self::parse_dice_term(pairs.next().unwrap())?;
+
+ expr = match op {
+ "+" => Expression::Add(Box::new(expr), Box::new(right)),
+ "-" => Expression::Subtract(Box::new(expr), Box::new(right)),
+ "*" => Expression::Multiply(Box::new(expr), Box::new(right)),
+ "/" => Expression::Divide(Box::new(expr), Box::new(right)),
+ "^" => Expression::Power(Box::new(expr), Box::new(right)),
+ _ => return Err(DiceError::ParseError(format!("未知操作符: {}", op))),
+ };
+ }
+ Rule::comment => {
+ let comment = Self::parse_comment(pair)?;
+ if let Some(comment_text) = comment {
+ expr = Expression::WithComment(Box::new(expr), Some(comment_text));
+ }
+ }
+ _ => {}
+ }
+ }
+ Ok(expr)
+ }
+ _ => Err(DiceError::ParseError(format!("期望骰子表达式,得到: {:?}", pair.as_rule()))),
+ }
+ }
+
+ fn parse_dice_term(pair: pest::iterators::Pair<Rule>) -> Result<Expression, DiceError> {
+ match pair.as_rule() {
+ Rule::dice_term => {
+ let inner = pair.into_inner().next().unwrap();
+ match inner.as_rule() {
+ Rule::dice_roll => Self::parse_dice_roll(inner),
+ Rule::paren_expr => {
+ let expr = Self::parse_dice_expr(inner.into_inner().next().unwrap())?;
+ Ok(Expression::Paren(Box::new(expr)))
+ }
+ Rule::number => {
+ let num = inner.as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效数字".to_string()))?;
+ Ok(Expression::Number(num))
+ }
+ _ => Err(DiceError::ParseError("无效的骰子项".to_string())),
+ }
+ }
+ _ => Err(DiceError::ParseError("期望骰子项".to_string())),
+ }
+ }
+
+ fn parse_dice_roll(pair: pest::iterators::Pair<Rule>) -> Result<Expression, DiceError> {
+ let mut pairs = pair.into_inner();
+ let count = pairs.next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的骰子数量".to_string()))?;
+ let sides = pairs.next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的骰子面数".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);
+ }
+ }
+
+ Ok(Expression::DiceRoll(DiceRoll {
+ count,
+ sides,
+ modifiers,
+ }))
+ }
+
+ fn parse_modifier(pair: pest::iterators::Pair<Rule>) -> Result<DiceModifier, DiceError> {
+ match pair.as_rule() {
+ Rule::modifier => {
+ let inner = pair.into_inner().next().unwrap();
+ match inner.as_rule() {
+ Rule::explode => Ok(DiceModifier::Explode),
+ Rule::reroll => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的重投数值".to_string()))?;
+ Ok(DiceModifier::Reroll(num))
+ }
+ Rule::reroll_once => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的条件重投数值".to_string()))?;
+ Ok(DiceModifier::RerollOnce(num))
+ }
+ Rule::keep_high => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的取高数值".to_string()))?;
+ Ok(DiceModifier::KeepHigh(num))
+ }
+ Rule::keep_low => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的取低数值".to_string()))?;
+ Ok(DiceModifier::KeepLow(num))
+ }
+ Rule::drop_high => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的丢弃高数值".to_string()))?;
+ Ok(DiceModifier::DropHigh(num))
+ }
+ Rule::drop_low => {
+ let num = inner.into_inner().next().unwrap().as_str().parse::<i32>()
+ .map_err(|_| DiceError::ParseError("无效的丢弃低数值".to_string()))?;
+ Ok(DiceModifier::DropLow(num))
+ }
+ _ => Err(DiceError::ParseError("未知的修饰符".to_string())),
+ }
+ }
+ _ => Err(DiceError::ParseError("期望修饰符".to_string())),
+ }
+ }
+
+ fn parse_comment(pair: pest::iterators::Pair<Rule>) -> Result<Option<String>, DiceError> {
+ match pair.as_rule() {
+ Rule::comment => {
+ let comment = pair.as_str().trim_start_matches('#').trim();
+ Ok(if comment.is_empty() { None } else { Some(comment.to_string()) })
+ }
+ _ => Ok(None),
+ }
+ }
+
+
+}
diff --git a/src/pyo3_template/__init__.py b/src/pyo3_template/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/src/pyo3_template/__init__.py
+++ /dev/null
diff --git a/src/pyo3_template/__main__.py b/src/pyo3_template/__main__.py
deleted file mode 100644
index 0135c6d..0000000
--- a/src/pyo3_template/__main__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ._core import sum_as_string
-
-def main():
- print(sum_as_string(1, 2))
-
-
-if __name__ == "__main__":
- main() \ No newline at end of file
diff --git a/src/pyo3_template/_core.pyi b/src/pyo3_template/_core.pyi
deleted file mode 100644
index 4945a82..0000000
--- a/src/pyo3_template/_core.pyi
+++ /dev/null
@@ -1,3 +0,0 @@
-def sum_as_string(self, a: int, b: int) -> str: ...
-
-class Base: ... \ No newline at end of file
diff --git a/src/python_bindings.rs b/src/python_bindings.rs
new file mode 100644
index 0000000..4073238
--- /dev/null
+++ b/src/python_bindings.rs
@@ -0,0 +1,154 @@
+use pyo3::prelude::*;
+use pyo3::types::PyDict;
+
+use crate::calculator::DiceCalculator;
+use crate::parser::DiceParser;
+use crate::types::{DiceModifier, DiceRoll};
+
+#[pyclass]
+pub struct OneRoll;
+
+#[pymethods]
+impl OneRoll {
+ #[new]
+ fn new() -> Self {
+ Self
+ }
+
+ fn roll(&mut self, expression: &str) -> PyResult<PyObject> {
+ Python::with_gil(|py| {
+ let mut calculator = DiceCalculator::new();
+ let expr = DiceParser::parse_expression(expression)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ let result = calculator.evaluate_expression(&expr)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ let dict = PyDict::new(py);
+ dict.set_item("expression", &result.expression)?;
+ dict.set_item("total", result.total)?;
+ dict.set_item("rolls", result.rolls)?;
+ dict.set_item("details", &result.details)?;
+ dict.set_item("comment", result.comment.as_deref().unwrap_or(""))?;
+
+ Ok(dict.into())
+ })
+ }
+
+ fn roll_simple(&mut self, dice_count: i32, dice_sides: i32) -> PyResult<i32> {
+ let mut calculator = DiceCalculator::new();
+ let dice = DiceRoll {
+ count: dice_count,
+ sides: dice_sides,
+ modifiers: vec![],
+ };
+
+ let rolls = calculator.roll_dice(&dice)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ Ok(rolls.iter().flatten().sum())
+ }
+
+ fn roll_with_modifiers(&mut self, dice_count: i32, dice_sides: i32, modifiers: Vec<String>) -> PyResult<PyObject> {
+ Python::with_gil(|py| {
+ let mut calculator = DiceCalculator::new();
+ let mut dice_modifiers = Vec::new();
+
+ for modifier_str in modifiers {
+ let modifier = match modifier_str.as_str() {
+ "!" => DiceModifier::Explode,
+ s if s.starts_with("r") && !s.starts_with("ro") => {
+ let num = s[1..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的重投数值"))?;
+ DiceModifier::Reroll(num)
+ }
+ s if s.starts_with("ro") => {
+ let num = s[2..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的条件重投数值"))?;
+ DiceModifier::RerollOnce(num)
+ }
+ s if s.starts_with("kh") => {
+ let num = s[2..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的取高数值"))?;
+ DiceModifier::KeepHigh(num)
+ }
+ s if s.starts_with("kl") => {
+ let num = s[2..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的取低数值"))?;
+ DiceModifier::KeepLow(num)
+ }
+ s if s.starts_with("dh") => {
+ let num = s[2..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的丢弃高数值"))?;
+ DiceModifier::DropHigh(num)
+ }
+ s if s.starts_with("dl") => {
+ let num = s[2..].parse::<i32>()
+ .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>("无效的丢弃低数值"))?;
+ DiceModifier::DropLow(num)
+ }
+ _ => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>("未知的修饰符")),
+ };
+ dice_modifiers.push(modifier);
+ }
+
+ let dice = DiceRoll {
+ count: dice_count,
+ sides: dice_sides,
+ modifiers: dice_modifiers,
+ };
+
+ let rolls = calculator.roll_dice(&dice)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ let total: i32 = rolls.iter().flatten().sum();
+ let details = format!("{}d{}{} = {} (详情: {:?})",
+ dice_count, dice_sides,
+ calculator.modifiers_to_string(&dice.modifiers),
+ total, rolls);
+
+ let dict = PyDict::new(py);
+ dict.set_item("total", total)?;
+ dict.set_item("rolls", rolls)?;
+ dict.set_item("details", &details)?;
+
+ Ok(dict.into())
+ })
+ }
+}
+
+#[pyfunction]
+pub fn roll_dice(expression: &str) -> PyResult<PyObject> {
+ Python::with_gil(|py| {
+ let mut calculator = DiceCalculator::new();
+ let expr = DiceParser::parse_expression(expression)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ let result = calculator.evaluate_expression(&expr)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ let dict = PyDict::new(py);
+ dict.set_item("expression", &result.expression)?;
+ dict.set_item("total", result.total)?;
+ dict.set_item("rolls", result.rolls)?;
+ dict.set_item("details", &result.details)?;
+ dict.set_item("comment", result.comment.as_deref().unwrap_or(""))?;
+
+ Ok(dict.into())
+ })
+}
+
+#[pyfunction]
+pub fn roll_simple(dice_count: i32, dice_sides: i32) -> PyResult<i32> {
+ let mut calculator = DiceCalculator::new();
+ let dice = DiceRoll {
+ count: dice_count,
+ sides: dice_sides,
+ modifiers: vec![],
+ };
+
+ let rolls = calculator.roll_dice(&dice)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
+
+ Ok(rolls.iter().flatten().sum())
+}
diff --git a/src/types.rs b/src/types.rs
new file mode 100644
index 0000000..a88cbab
--- /dev/null
+++ b/src/types.rs
@@ -0,0 +1,65 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DiceResult {
+ pub expression: String,
+ pub total: i32,
+ pub rolls: Vec<Vec<i32>>,
+ pub details: String,
+ pub comment: Option<String>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct DiceRoll {
+ pub count: i32,
+ pub sides: i32,
+ pub modifiers: Vec<DiceModifier>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum DiceModifier {
+ Explode, // !
+ Reroll(i32), // rX
+ RerollOnce(i32), // roX
+ KeepHigh(i32), // khX
+ KeepLow(i32), // klX
+ DropHigh(i32), // dhX
+ DropLow(i32), // dlX
+}
+
+
+#[derive(Debug, Clone)]
+pub enum Expression {
+ Number(i32),
+ DiceRoll(DiceRoll),
+ Add(Box<Expression>, Box<Expression>),
+ Subtract(Box<Expression>, Box<Expression>),
+ Multiply(Box<Expression>, Box<Expression>),
+ Divide(Box<Expression>, Box<Expression>),
+ Power(Box<Expression>, Box<Expression>),
+ Paren(Box<Expression>),
+ WithComment(Box<Expression>, Option<String>),
+}
+
+// TODO: Variable storage
+#[derive(Debug, Clone, Default)]
+pub struct VariableStore {
+ pub variables: HashMap<String, i32>,
+}
+
+impl VariableStore {
+ pub fn new() -> Self {
+ Self {
+ variables: HashMap::new(),
+ }
+ }
+
+ pub fn set(&mut self, name: &str, value: i32) {
+ self.variables.insert(name.to_string(), value);
+ }
+
+ pub fn get(&self, name: &str) -> Option<i32> {
+ self.variables.get(name).copied()
+ }
+}