From 0288d0956330d5ac8db48b752240f723e8703929 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 12 Sep 2025 04:02:02 +0800 Subject: feat: initial basic roll features --- src/calculator.rs | 212 ++++++++++++++++++++++++++ src/errors.rs | 17 +++ src/lib.rs | 26 +++- src/oneroll/__init__.py | 241 ++++++++++++++++++++++++++++++ src/oneroll/__main__.py | 335 ++++++++++++++++++++++++++++++++++++++++++ src/oneroll/_core.pyi | 247 +++++++++++++++++++++++++++++++ src/oneroll/grammar.pest | 43 ++++++ src/oneroll/tui.py | 251 +++++++++++++++++++++++++++++++ src/parser.rs | 161 ++++++++++++++++++++ src/pyo3_template/__init__.py | 0 src/pyo3_template/__main__.py | 8 - src/pyo3_template/_core.pyi | 3 - src/python_bindings.rs | 154 +++++++++++++++++++ src/types.rs | 65 ++++++++ 14 files changed, 1744 insertions(+), 19 deletions(-) create mode 100644 src/calculator.rs create mode 100644 src/errors.rs create mode 100644 src/oneroll/__init__.py create mode 100644 src/oneroll/__main__.py create mode 100644 src/oneroll/_core.pyi create mode 100644 src/oneroll/grammar.pest create mode 100644 src/oneroll/tui.py create mode 100644 src/parser.rs delete mode 100644 src/pyo3_template/__init__.py delete mode 100644 src/pyo3_template/__main__.py delete mode 100644 src/pyo3_template/_core.pyi create mode 100644 src/python_bindings.rs create mode 100644 src/types.rs (limited to 'src') 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>, 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::() % 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::() % 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::() % 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::() % 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 = 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 = 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 = 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 = 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 { + 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 for pyo3::PyErr { + fn from(err: DiceError) -> pyo3::PyErr { + pyo3::PyErr::new::(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 { - 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::()?; + m.add_function(wrap_pyfunction!(roll_dice, m)?)?; + m.add_function(wrap_pyfunction!(roll_simple, m)?)?; + m.add_class::()?; 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" - Re-submit, such as "r1" + - "ro" - Conditional re-submission, such as "ro1" + - "kh" - Take the height, such as "kh3" + - "kl" - Take the low, such as "kl2" + - "dh" - discard the height, such as "dh1" + - "dl" - 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 { + 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) -> Result { + 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) -> Result { + 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::() + .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) -> Result { + let mut pairs = pair.into_inner(); + let count = pairs.next().unwrap().as_str().parse::() + .map_err(|_| DiceError::ParseError("无效的骰子数量".to_string()))?; + let sides = pairs.next().unwrap().as_str().parse::() + .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) -> Result { + 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::() + .map_err(|_| DiceError::ParseError("无效的重投数值".to_string()))?; + Ok(DiceModifier::Reroll(num)) + } + Rule::reroll_once => { + let num = inner.into_inner().next().unwrap().as_str().parse::() + .map_err(|_| DiceError::ParseError("无效的条件重投数值".to_string()))?; + Ok(DiceModifier::RerollOnce(num)) + } + Rule::keep_high => { + let num = inner.into_inner().next().unwrap().as_str().parse::() + .map_err(|_| DiceError::ParseError("无效的取高数值".to_string()))?; + Ok(DiceModifier::KeepHigh(num)) + } + Rule::keep_low => { + let num = inner.into_inner().next().unwrap().as_str().parse::() + .map_err(|_| DiceError::ParseError("无效的取低数值".to_string()))?; + Ok(DiceModifier::KeepLow(num)) + } + Rule::drop_high => { + let num = inner.into_inner().next().unwrap().as_str().parse::() + .map_err(|_| DiceError::ParseError("无效的丢弃高数值".to_string()))?; + Ok(DiceModifier::DropHigh(num)) + } + Rule::drop_low => { + let num = inner.into_inner().next().unwrap().as_str().parse::() + .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) -> Result, 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 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 { + Python::with_gil(|py| { + let mut calculator = DiceCalculator::new(); + let expr = DiceParser::parse_expression(expression) + .map_err(|e| PyErr::new::(e.to_string()))?; + + let result = calculator.evaluate_expression(&expr) + .map_err(|e| PyErr::new::(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 { + 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::(e.to_string()))?; + + Ok(rolls.iter().flatten().sum()) + } + + fn roll_with_modifiers(&mut self, dice_count: i32, dice_sides: i32, modifiers: Vec) -> PyResult { + 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::() + .map_err(|_| PyErr::new::("无效的重投数值"))?; + DiceModifier::Reroll(num) + } + s if s.starts_with("ro") => { + let num = s[2..].parse::() + .map_err(|_| PyErr::new::("无效的条件重投数值"))?; + DiceModifier::RerollOnce(num) + } + s if s.starts_with("kh") => { + let num = s[2..].parse::() + .map_err(|_| PyErr::new::("无效的取高数值"))?; + DiceModifier::KeepHigh(num) + } + s if s.starts_with("kl") => { + let num = s[2..].parse::() + .map_err(|_| PyErr::new::("无效的取低数值"))?; + DiceModifier::KeepLow(num) + } + s if s.starts_with("dh") => { + let num = s[2..].parse::() + .map_err(|_| PyErr::new::("无效的丢弃高数值"))?; + DiceModifier::DropHigh(num) + } + s if s.starts_with("dl") => { + let num = s[2..].parse::() + .map_err(|_| PyErr::new::("无效的丢弃低数值"))?; + DiceModifier::DropLow(num) + } + _ => return Err(PyErr::new::("未知的修饰符")), + }; + 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::(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 { + Python::with_gil(|py| { + let mut calculator = DiceCalculator::new(); + let expr = DiceParser::parse_expression(expression) + .map_err(|e| PyErr::new::(e.to_string()))?; + + let result = calculator.evaluate_expression(&expr) + .map_err(|e| PyErr::new::(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 { + 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::(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>, + pub details: String, + pub comment: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DiceRoll { + pub count: i32, + pub sides: i32, + pub modifiers: Vec, +} + +#[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, Box), + Subtract(Box, Box), + Multiply(Box, Box), + Divide(Box, Box), + Power(Box, Box), + Paren(Box), + WithComment(Box, Option), +} + +// TODO: Variable storage +#[derive(Debug, Clone, Default)] +pub struct VariableStore { + pub variables: HashMap, +} + +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 { + self.variables.get(name).copied() + } +} -- cgit v1.2.3-70-g09d2