diff options
| author | 2025-09-12 04:02:02 +0800 | |
|---|---|---|
| committer | 2025-09-12 04:02:02 +0800 | |
| commit | 0288d0956330d5ac8db48b752240f723e8703929 (patch) | |
| tree | 0297dee9f8166af0a856dd3a1057ad5f25f14c6a /src/oneroll | |
| parent | 5135876b5e2a6c40232414ea0b7eb875fa225cf0 (diff) | |
| download | OneRoll-0288d0956330d5ac8db48b752240f723e8703929.tar.gz OneRoll-0288d0956330d5ac8db48b752240f723e8703929.zip | |
feat: initial basic roll features
Diffstat (limited to 'src/oneroll')
| -rw-r--r-- | src/oneroll/__init__.py | 241 | ||||
| -rw-r--r-- | src/oneroll/__main__.py | 335 | ||||
| -rw-r--r-- | src/oneroll/_core.pyi | 247 | ||||
| -rw-r--r-- | src/oneroll/grammar.pest | 43 | ||||
| -rw-r--r-- | src/oneroll/tui.py | 251 |
5 files changed, 1117 insertions, 0 deletions
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() |
