From cbc653ffd0ea9abf4360623dc7a7651e1a49cc61 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Sat, 25 Oct 2025 00:30:48 +0800 Subject: feat: Implement plugin system with combat tracker and dice analyzer - Added `plugin_system_demo.py` to demonstrate basic plugin usage, processing, and analysis. - Created `CombatTrackerPlugin` for tracking combat statistics including damage and healing. - Developed `DiceAnalyzerPlugin` for analyzing dice rolls and calculating success rates. - Introduced `renderer_demo.py` for rendering output in HTML, Markdown, and JSON formats. - Implemented `rule_system_demo.py` to showcase rule engine capabilities with various examples. - Established core rule engine functionality in `rules.py` with support for conditions and actions. - Enhanced base plugin structure in `base.py` to support different plugin types (Processor, Renderer, Analyzer). - Added custom exception handling in `exceptions.py` for better error management. - Configured logging setup in `logging_config.py` for improved logging capabilities. - Created unit tests in `test_rust_core.py` to validate core functionalities and performance. --- examples/README.md | 190 --------------------------- examples/basic_usage.py | 58 ++------- examples/custom_plugin.py | 70 ++-------- examples/plugin_system_demo.py | 173 ++++++++++++++++++++++++ examples/plugins/combat_tracker_plugin.py | 96 ++++++++++++++ examples/plugins/dice_analyzer_plugin.py | 90 +++++++++++++ examples/renderer_demo.py | 147 +++++++++++++++++++++ examples/rule_system_demo.py | 210 ++++++++++++++++++++++++++++++ 8 files changed, 737 insertions(+), 297 deletions(-) delete mode 100644 examples/README.md create mode 100644 examples/plugin_system_demo.py create mode 100644 examples/plugins/combat_tracker_plugin.py create mode 100644 examples/plugins/dice_analyzer_plugin.py create mode 100644 examples/renderer_demo.py create mode 100644 examples/rule_system_demo.py (limited to 'examples') diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index d823f5b..0000000 --- a/examples/README.md +++ /dev/null @@ -1,190 +0,0 @@ -# ConventionalRP 示例 - -本目录包含 ConventionalRP SDK 的使用示例。 - -## 目录结构 - -``` -examples/ -├── basic_usage.py # 基础使用示例 -├── custom_plugin.py # 自定义插件示例 -├── rules/ # 规则文件 -│ └── dnd5e_rules.json5 # D&D 5E 解析规则 -├── logs/ # 示例日志文件 -│ ├── sample_session.txt # 完整会话日志 -│ └── combat_log.txt # 战斗日志 -└── output/ # 输出文件目录(自动生成) -``` - -## 快速开始 - -### 1. 基础使用示例 - -演示如何解析 TRPG 日志并以多种格式输出: - -```bash -cd examples -python basic_usage.py -``` - -**输出:** -- `output/session_output.json` - JSON 格式 -- `output/session_output.html` - HTML 格式 -- `output/session_output.md` - Markdown 格式 - -### 2. 自定义插件示例 - -演示如何创建自定义插件进行数据分析: - -```bash -python custom_plugin.py -``` - -**功能:** -- 骰子统计分析 -- 对话提取 -- 角色行为分析 - -## 规则文件格式 - -规则文件使用 JSON5 格式(支持注释和尾随逗号): - -```json5 -{ - metadata: [{ - type: "metadata", - patterns: ["正则表达式"], - groups: ["字段名"], - priority: 100 - }], - - content: [{ - type: "内容类型", - match_type: "匹配模式", // enclosed, prefix, suffix - patterns: ["正则表达式"], - groups: ["提取字段"], - priority: 90 - }] -} -``` - -### 匹配模式说明 - -- **enclosed**: 封闭匹配(如 `**动作**`、`「对话」`) -- **prefix**: 前缀匹配(如 `[系统]消息`) -- **suffix**: 后缀匹配(文本结尾) - -### 优先级 - -- 数字越大优先级越高 -- 建议范围:1-100 -- 元数据通常设置为最高优先级(100) - -## 日志文件格式 - -标准 TRPG 日志格式: - -``` -[时间戳] <角色名> 内容 -``` - -**示例:** - -``` -[2025-10-24 14:30:01] <艾莉娅> 「我要检查这扇门」 -[2025-10-24 14:30:05] 检定结果: [d20 = 18] -[2025-10-24 14:30:10] 你发现了陷阱 -``` - -## 自定义规则 - -你可以为不同的游戏系统创建自定义规则: - -### D&D 5E - -已提供 `rules/dnd5e_rules.json5` - -### 其他系统 - -创建新的规则文件,参考 D&D 5E 规则的结构: - -```bash -cp rules/dnd5e_rules.json5 rules/my_system_rules.json5 -# 然后编辑 my_system_rules.json5 -``` - -## 创建自定义插件 - -插件是用于扩展功能的 Python 类: - -```python -class MyPlugin: - def __init__(self): - self.name = "My Plugin" - - def process(self, parsed_data): - # 你的处理逻辑 - return result -``` - -查看 `custom_plugin.py` 了解完整示例。 - -## 常见模式 - -### 1. 骰子投掷 - -- `[d20 = 18]` - 简单投掷结果 -- `.r1d20+5` - 投掷命令 -- `(1d20+5 = 18)` - 完整投掷信息 - -### 2. 角色动作 - -- `*动作描述*` - 单星号 -- `**重要动作**` - 双星号 - -### 3. 对话 - -- `「对话内容」` - 中文引号 -- `"对话内容"` - 英文引号 -- `"对话内容"` - 弯引号 - -### 4. OOC(脱戏) - -- `((OOC内容))` - 双括号 -- `//OOC注释` - 双斜杠 - -### 5. 系统消息 - -- `[系统]消息内容` -- `[System]Message` - -## 疑难解答 - -### 问题:规则文件加载失败 - -**解决方案:** -1. 确保文件是有效的 JSON5 格式 -2. 检查正则表达式是否转义正确(使用 `\\` 而不是 `\`) -3. 验证文件编码为 UTF-8 - -### 问题:解析结果不正确 - -**解决方案:** -1. 调整规则的优先级 -2. 测试正则表达式(使用 https://regex101.com/) -3. 检查 match_type 是否正确 - -### 问题:中文字符显示异常 - -**解决方案:** -- 确保所有文件使用 UTF-8 编码 -- 在打开文件时指定 `encoding='utf-8'` - -## 更多示例 - -访问项目文档查看更多示例: -https://crp.hydroroll.team/ - -## 贡献 - -欢迎提交新的示例和规则文件!请参考 CONTRIBUTING.md diff --git a/examples/basic_usage.py b/examples/basic_usage.py index c327cb0..d1ab724 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -1,13 +1,6 @@ -#!/usr/bin/env python3 -""" -基础使用示例 -演示如何使用 ConventionalRP 解析和处理 TRPG 日志 -""" - import sys from pathlib import Path -# 添加 src 目录到 Python 路径 project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root / "src")) @@ -18,37 +11,22 @@ from conventionalrp.renderers.html_renderer import HTMLRenderer from conventionalrp.renderers.json_renderer import JSONRenderer from conventionalrp.renderers.markdown_renderer import MarkdownRenderer - -def main(): - # 获取示例文件路径 +def main(): example_dir = Path(__file__).parent rules_file = example_dir / "rules" / "dnd5e_rules.json5" log_file = example_dir / "logs" / "sample_session.txt" - - print("=" * 60) - print("ConventionalRP 基础使用示例") - print("=" * 60) - - # 步骤 1: 加载规则 - print("\n[步骤 1] 加载解析规则...") + parser = Parser() parser.load_rules(str(rules_file)) - print(f"✓ 规则加载成功: {rules_file.name}") + print(f"Rule loaded: {rules_file.name}") - # 步骤 2: 解析日志 - print("\n[步骤 2] 解析 TRPG 日志...") parsed_data = parser.parse_log(str(log_file)) - print(f"✓ 日志解析完成,共 {len(parsed_data)} 条记录") + print(f"Log parsed successfully, {len(parsed_data)} entries found.") - # 步骤 3: 处理解析结果 - print("\n[步骤 3] 处理解析后的数据...") processor = Processor() processed_data = processor.process_tokens(parsed_data) - print(f"✓ 数据处理完成") - - # 步骤 4: 渲染输出 - print("\n[步骤 4] 渲染输出...") - + print(f"Done processing data") + # JSON 格式 json_renderer = JSONRenderer() json_output = json_renderer.render(processed_data) @@ -56,7 +34,7 @@ def main(): json_file.parent.mkdir(exist_ok=True) with open(json_file, "w", encoding="utf-8") as f: f.write(json_output) - print(f"✓ JSON 输出已保存: {json_file}") + print(f"Json: {json_file}") # HTML 格式 html_renderer = HTMLRenderer() @@ -64,32 +42,14 @@ def main(): html_file = example_dir / "output" / "session_output.html" with open(html_file, "w", encoding="utf-8") as f: f.write(html_output) - print(f"✓ HTML 输出已保存: {html_file}") + print(f"HTML: {html_file}") - # Markdown 格式 md_renderer = MarkdownRenderer() md_output = md_renderer.render(processed_data) md_file = example_dir / "output" / "session_output.md" with open(md_file, "w", encoding="utf-8") as f: f.write(md_output) - print(f"✓ Markdown 输出已保存: {md_file}") - - # 预览前几条记录 - print("\n" + "=" * 60) - print("解析结果预览(前3条):") - print("=" * 60) - for i, entry in enumerate(parsed_data[:3], 1): - print(f"\n[记录 {i}]") - print(f" 时间: {entry.get('timestamp', 'N/A')}") - print(f" 发言者: {entry.get('speaker', 'N/A')}") - print(f" 内容类型数: {len(entry.get('content', []))}") - for content in entry.get('content', [])[:2]: # 只显示前2个内容 - print(f" - {content.get('type', 'unknown')}: {content.get('content', '')[:50]}...") - - print("\n" + "=" * 60) - print("✓ 所有步骤完成!") - print("=" * 60) - + print(f"Markdown: {md_file}") if __name__ == "__main__": main() diff --git a/examples/custom_plugin.py b/examples/custom_plugin.py index dd96311..5f77c41 100644 --- a/examples/custom_plugin.py +++ b/examples/custom_plugin.py @@ -1,14 +1,7 @@ -#!/usr/bin/env python3 -""" -自定义插件示例 -演示如何创建和使用自定义插件来扩展 ConventionalRP 的功能 -""" - import sys from typing import List, Dict, Any from pathlib import Path -# 添加 src 目录到 Python 路径 project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root / "src")) @@ -17,21 +10,12 @@ from conventionalrp.core.processor import Processor class DiceRollAnalyzer: - """骰子统计分析插件""" + """骰点统计分析插件""" def __init__(self): self.name = "Dice Roll Analyzer" def analyze(self, parsed_data: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - 分析日志中的所有骰子投掷 - - Args: - parsed_data: 解析后的日志数据 - - Returns: - 统计结果 - """ stats = { "total_rolls": 0, "by_character": {}, @@ -65,15 +49,6 @@ class DialogueExtractor: self.name = "Dialogue Extractor" def extract(self, parsed_data: List[Dict[str, Any]]) -> List[Dict[str, str]]: - """ - 提取所有角色对话 - - Args: - parsed_data: 解析后的日志数据 - - Returns: - 对话列表 - """ dialogues = [] for entry in parsed_data: @@ -92,59 +67,38 @@ class DialogueExtractor: def main(): - print("=" * 60) - print("ConventionalRP 自定义插件示例") - print("=" * 60) - - # 准备数据 example_dir = Path(__file__).parent rules_file = example_dir / "rules" / "dnd5e_rules.json5" log_file = example_dir / "logs" / "combat_log.txt" - - print("\n[1] 解析日志...") + + print("\nLoading log...") parser = Parser() parser.load_rules(str(rules_file)) parsed_data = parser.parse_log(str(log_file)) - print(f"✓ 解析完成,共 {len(parsed_data)} 条记录") + print(f"Done, {len(parsed_data)} in total") - # 使用骰子分析插件 - print("\n[2] 运行骰子统计分析插件...") dice_analyzer = DiceRollAnalyzer() dice_stats = dice_analyzer.analyze(parsed_data) - - print(f"\n骰子统计结果:") + + print(f"\nStatistics:") print(f" 总投掷次数: {dice_stats['total_rolls']}") print(f"\n 按角色统计:") for character, count in dice_stats['by_character'].items(): print(f" {character}: {count} 次") - print(f"\n 按骰子类型统计:") + print(f"\n Statistics in Dice Types:") for dice_type, count in dice_stats['dice_types'].items(): - print(f" d{dice_type}: {count} 次") + print(f" d{dice_type}: {count} times") - # 使用对话提取插件 - print("\n[3] 运行对话提取插件...") dialogue_extractor = DialogueExtractor() dialogues = dialogue_extractor.extract(parsed_data) - - print(f"\n提取到 {len(dialogues)} 条对话:") - for i, dialogue in enumerate(dialogues[:5], 1): # 只显示前5条 + + print(f"\nExtracted {len(dialogues)} dialogues:") + for i, dialogue in enumerate(dialogues[:5], 1): # Only show the first 5 print(f"\n [{i}] {dialogue['speaker']} ({dialogue['timestamp']})") print(f" {dialogue['dialogue']}") if len(dialogues) > 5: - print(f"\n ... 还有 {len(dialogues) - 5} 条对话") - - print("\n" + "=" * 60) - print("✓ 插件演示完成!") - print("=" * 60) - print("\n提示: 你可以创建自己的插件来实现:") - print(" - 战斗统计分析") - print(" - 角色行为分析") - print(" - 关键词提取") - print(" - 情感分析") - print(" - 自动摘要生成") - print(" - ... 以及更多!") - + print(f"\n ... and {len(dialogues) - 5} more dialogues") if __name__ == "__main__": main() diff --git a/examples/plugin_system_demo.py b/examples/plugin_system_demo.py new file mode 100644 index 0000000..1899b34 --- /dev/null +++ b/examples/plugin_system_demo.py @@ -0,0 +1,173 @@ +import sys +import os +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from conventionalrp.plugins import PluginManager +from conventionalrp.core.parser import Parser + +sys.path.insert(0, str(Path(__file__).parent / "plugins")) +from dice_analyzer_plugin import DiceAnalyzerPlugin +from combat_tracker_plugin import CombatTrackerPlugin + + +def demo_basic_plugin_usage(): + manager = PluginManager() + + dice_analyzer = DiceAnalyzerPlugin() + dice_analyzer.initialize() + manager.register_plugin(dice_analyzer) + + combat_tracker = CombatTrackerPlugin() + combat_tracker.initialize() + manager.register_plugin(combat_tracker) + + print("\nRegistered Plugins:") + for plugin_info in manager.list_plugins(): + print(f" - {plugin_info['name']} v{plugin_info['version']} (type: {plugin_info['type']})") + + stats = manager.get_statistics() + print(f"\nPlugin Statistics: {stats}") + + +def demo_processor_plugin(): + manager = PluginManager() + + combat_tracker = CombatTrackerPlugin() + combat_tracker.initialize() + manager.register_plugin(combat_tracker) + + combat_log = [ + {"type": "dialogue", "speaker": "战士", "content": "我攻击兽人,造成12点伤害!"}, + {"type": "dialogue", "speaker": "法师", "content": "火球术!造成28点火焰伤害"}, + {"type": "dialogue", "speaker": "牧师", "content": "治疗之光,恢复15点生命值"}, + {"type": "dialogue", "speaker": "战士", "content": "重击!造成18点伤害"}, + {"type": "dialogue", "speaker": "牧师", "content": "群体治疗,恢复10点生命值"}, + ] + + print("\nProcessing Combat Log:") + from conventionalrp.plugins.base import ProcessorPlugin + processed_log = manager.execute_plugins(combat_log, plugin_type=ProcessorPlugin) + + for token in processed_log: + if "combat_data" in token: + print(f" {token['speaker']}: {token['content']}") + print(f" -> {token['combat_data']}") + + print("\nCombat Summary:") + summary = combat_tracker.get_combat_summary() + print(f" 总伤害: {summary['total_damage']}") + print(f" 总治疗: {summary['total_healing']}") + print(f" 净伤害: {summary['net_damage']}") + print("\n 角色统计:") + for character, stats in summary['character_stats'].items(): + print(f" {character}: 造成伤害={stats['damage_dealt']}, 治疗={stats['healing_done']}") + + +def demo_analyzer_plugin(): + manager = PluginManager() + + dice_analyzer = DiceAnalyzerPlugin() + dice_analyzer.initialize() + manager.register_plugin(dice_analyzer) + + dice_rolls = [ + {"type": "dice", "content": "d20=15", "result": 15}, + {"type": "success", "content": "检定成功"}, + {"type": "dice", "content": "d6=4", "result": 4}, + {"type": "dice", "content": "d20=20", "result": 20}, + {"type": "success", "content": "大成功!Critical hit!"}, + {"type": "dice", "content": "d20=1", "result": 1}, + {"type": "failure", "content": "大失败..."}, + {"type": "dice", "content": "d20=12", "result": 12}, + {"type": "dice", "content": "d6=3", "result": 3}, + {"type": "success", "content": "检定成功"}, + ] + + print("\nDice Roll Data:") + for roll in dice_rolls: + if roll["type"] == "dice": + print(f" {roll['content']}") + + from conventionalrp.plugins.base import AnalyzerPlugin + analysis = manager.execute_plugins(dice_rolls, plugin_type=AnalyzerPlugin) + + print("\nAnalyze result:") + print(f" 总投掷次数: {analysis['total_rolls']}") + print(f" 骰子类型分布: {analysis['dice_types']}") + print(f" 成功次数: {analysis['success_count']}") + print(f" 失败次数: {analysis['failure_count']}") + print(f" 大成功次数: {analysis['critical_hits']}") + print(f" 大失败次数: {analysis['critical_fails']}") + print(f" 成功率: {analysis['success_rate']:.1%}") + print(f" 出现极值比率: {analysis['critical_rate']:.1%}") + + +def demo_plugin_enable_disable(): + manager = PluginManager() + dice_analyzer = DiceAnalyzerPlugin() + dice_analyzer.initialize() + manager.register_plugin(dice_analyzer) + + combat_tracker = CombatTrackerPlugin() + combat_tracker.initialize() + manager.register_plugin(combat_tracker) + + print("\nInitial State:") + for plugin_info in manager.list_plugins(): + print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}") + + # Disable DiceAnalyzer + print("\nDisabling DiceAnalyzer...") + manager.disable_plugin("DiceAnalyzer") + + print("\nCurrent State:") + for plugin_info in manager.list_plugins(): + print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}") + + print("\nRe-enabling DiceAnalyzer...") + manager.enable_plugin("DiceAnalyzer") + + print("\nFinal State:") + for plugin_info in manager.list_plugins(): + print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}") + + +def demo_plugin_discovery(): + plugin_dir = Path(__file__).parent / "plugins" + manager = PluginManager(plugin_dirs=[str(plugin_dir)]) + + print(f"\nSearching for plugins in directory: {plugin_dir}") + discovered = manager.discover_plugins() + + print(f"\nFound {len(discovered)} plugin modules:") + for module in discovered: + print(f" - {module}") + + print("\nLoading plugins...") + for py_file in plugin_dir.glob("*.py"): + if py_file.name.startswith("_"): + continue + + plugin_class = manager.load_plugin_from_file(str(py_file)) + if plugin_class: + print(f" ✓ Successfully loaded: {py_file.name}") + + +def main(): + try: + demo_basic_plugin_usage() + demo_processor_plugin() + demo_analyzer_plugin() + demo_plugin_enable_disable() + demo_plugin_discovery() + + except Exception as e: + print(f"\n{e!r}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/examples/plugins/combat_tracker_plugin.py b/examples/plugins/combat_tracker_plugin.py new file mode 100644 index 0000000..a12bc9e --- /dev/null +++ b/examples/plugins/combat_tracker_plugin.py @@ -0,0 +1,96 @@ +import sys +import os +from pathlib import Path +import re +from typing import Any, Dict, List + +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) + +from conventionalrp.plugins.base import ProcessorPlugin + + +class CombatTrackerPlugin(ProcessorPlugin): + """战斗数据追踪插件""" + + def __init__(self): + super().__init__("CombatTracker", "1.0.0") + self.damage_pattern = re.compile(r'(\d+)\s*点?(伤害|damage|dmg)', re.IGNORECASE) + self.heal_pattern = re.compile(r'(\d+)\s*点?(治疗|healing|heal)', re.IGNORECASE) + + def initialize(self, config: Dict[str, Any] | None = None): + self.config = config or {} + self.total_damage = 0 + self.total_healing = 0 + self.character_stats = {} + self.logger.info("CombatTrackerPlugin initialized") + + def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]: + content = token.get("content", "") + speaker = token.get("speaker", "Unknown") + + damage_match = self.damage_pattern.search(content) + if damage_match: + damage = int(damage_match.group(1)) + self.total_damage += damage + + if speaker not in self.character_stats: + self.character_stats[speaker] = {"damage_dealt": 0, "healing_done": 0} + self.character_stats[speaker]["damage_dealt"] += damage + + token["combat_data"] = { + "type": "damage", + "amount": damage, + "total_damage": self.total_damage, + } + + heal_match = self.heal_pattern.search(content) + if heal_match: + healing = int(heal_match.group(1)) + self.total_healing += healing + + if speaker not in self.character_stats: + self.character_stats[speaker] = {"damage_dealt": 0, "healing_done": 0} + self.character_stats[speaker]["healing_done"] += healing + + token["combat_data"] = { + "type": "healing", + "amount": healing, + "total_healing": self.total_healing, + } + + return token + + def get_combat_summary(self) -> Dict[str, Any]: + return { + "total_damage": self.total_damage, + "total_healing": self.total_healing, + "net_damage": self.total_damage - self.total_healing, + "character_stats": self.character_stats, + } + + def reset_stats(self): + self.total_damage = 0 + self.total_healing = 0 + self.character_stats.clear() + self.logger.info("Combat stats reset") + + +if __name__ == "__main__": + plugin = CombatTrackerPlugin() + plugin.initialize() + + test_tokens = [ + {"type": "dialogue", "speaker": "战士", "content": "我攻击兽人,造成12点伤害"}, + {"type": "dialogue", "speaker": "法师", "content": "火球术命中,造成28点伤害"}, + {"type": "dialogue", "speaker": "牧师", "content": "治疗术,恢复15点生命值"}, + {"type": "dialogue", "speaker": "战士", "content": "再次攻击,造成8点伤害"}, + ] + + for token in test_tokens: + processed = plugin.process_token(token) + if "combat_data" in processed: + print(f" {processed['speaker']}: {processed['combat_data']}") + + summary = plugin.get_combat_summary() + for key, value in summary.items(): + print(f" {key}: {value}") diff --git a/examples/plugins/dice_analyzer_plugin.py b/examples/plugins/dice_analyzer_plugin.py new file mode 100644 index 0000000..3ab2fb9 --- /dev/null +++ b/examples/plugins/dice_analyzer_plugin.py @@ -0,0 +1,90 @@ +import sys +import os +from pathlib import Path +import re +from typing import Any, Dict, List + +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) + +from conventionalrp.plugins.base import AnalyzerPlugin + + +class DiceAnalyzerPlugin(AnalyzerPlugin): + """骰子投掷数据分析插件""" + def __init__(self): + super().__init__("DiceAnalyzer", "1.0.0") + self.dice_pattern = re.compile(r'd(\d+)') + + def initialize(self, config: Dict[str, Any] | None = None): + self.config = config or {} + self.logger.info("DiceAnalyzerPlugin initialized") + + def analyze(self, data: Any) -> Dict[str, Any]: + if not isinstance(data, list): + return {"error": "Input must be a list of tokens"} + + total_rolls = 0 + dice_types = {} + success_count = 0 + failure_count = 0 + critical_hits = 0 + critical_fails = 0 + + for token in data: + if not isinstance(token, dict): + continue + + token_type = token.get("type", "") + content = token.get("content", "") + + if token_type == "dice": + total_rolls += 1 + + match = self.dice_pattern.search(content) + if match: + dice_type = f"d{match.group(1)}" + dice_types[dice_type] = dice_types.get(dice_type, 0) + 1 + + if token_type == "success": + success_count += 1 + elif token_type == "failure": + failure_count += 1 + + if "critical" in content.lower(): + if "success" in token_type or "成功" in content: + critical_hits += 1 + elif "failure" in token_type or "失败" in content: + critical_fails += 1 + + result = { + "total_rolls": total_rolls, + "dice_types": dice_types, + "success_count": success_count, + "failure_count": failure_count, + "critical_hits": critical_hits, + "critical_fails": critical_fails, + "success_rate": success_count / total_rolls if total_rolls > 0 else 0, + "critical_rate": (critical_hits + critical_fails) / total_rolls if total_rolls > 0 else 0, + } + + self.logger.info(f"Analyzed {total_rolls} dice rolls") + return result + + +if __name__ == "__main__": + plugin = DiceAnalyzerPlugin() + plugin.initialize() + + test_data = [ + {"type": "dice", "content": "d20=15"}, + {"type": "success", "content": "检定成功"}, + {"type": "dice", "content": "d6=4"}, + {"type": "dice", "content": "d20=20"}, + {"type": "success", "content": "大成功!Critical hit!"}, + {"type": "dice", "content": "d20=1"}, + {"type": "failure", "content": "大失败..."}, + ] + + result = plugin.analyze(test_data) + for key, value in result.items(): + print(f" {key}: {value}") diff --git a/examples/renderer_demo.py b/examples/renderer_demo.py new file mode 100644 index 0000000..fa89b42 --- /dev/null +++ b/examples/renderer_demo.py @@ -0,0 +1,147 @@ +import sys +import os +from pathlib import Path + +# 添加父目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from conventionalrp.renderers.html_renderer import HTMLRenderer +from conventionalrp.renderers.markdown_renderer import MarkdownRenderer +from conventionalrp.renderers.json_renderer import JSONRenderer + + +TEST_DATA = [ + { + "type": "dialogue", + "speaker": "战士", + "content": "我们需要更小心地前进,前面可能有陷阱。", + "timestamp": "2024-01-15 20:30:15", + "tags": ["对话", "警告"], + }, + { + "type": "dice", + "speaker": "战士", + "content": "d20+5", + "result": 18, + "timestamp": "2024-01-15 20:30:30", + }, + { + "type": "success", + "content": "战士成功发现了隐藏的陷阱!", + "timestamp": "2024-01-15 20:30:35", + }, + { + "type": "narration", + "content": "昏暗的走廊中,石板地面上隐约可见一些不寻常的纹路。", + "timestamp": "2024-01-15 20:31:00", + "tags": ["环境描述"], + }, + { + "type": "dialogue", + "speaker": "法师", + "content": "让我施放侦测魔法,看看这里是否有魔法陷阱。", + "timestamp": "2024-01-15 20:31:15", + }, + { + "type": "dice", + "speaker": "法师", + "content": "d20+8", + "result": 23, + "timestamp": "2024-01-15 20:31:20", + "combat_data": {"type": "damage", "amount": 12, "total_damage": 12}, + }, + { + "type": "system", + "content": "法师侦测到前方10英尺处有一个魔法陷阱(火球术触发)。", + "timestamp": "2024-01-15 20:31:25", + }, +] + + +def demo_html_themes(): + themes = ["light", "dark", "fantasy"] + + for theme in themes: + print(f"\nGenerate {theme} theme...") + renderer = HTMLRenderer(theme=theme) + html_output = renderer.render(TEST_DATA) + + output_file = f"output_html_{theme}.html" + with open(output_file, "w", encoding="utf-8") as f: + f.write(html_output) + +def demo_html_custom_css(): + custom_css = """ + body { + background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%); + } + + .token { + animation: fadeIn 0.5s; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + """ + + renderer = HTMLRenderer(theme="light", custom_css=custom_css) + html_output = renderer.render(TEST_DATA) + + output_file = "output_html_custom.html" + with open(output_file, "w", encoding="utf-8") as f: + f.write(html_output) + + +def demo_markdown_styles(): + renderer_full = MarkdownRenderer(enable_syntax_hints=True, enable_emoji=True) + md_output_full = renderer_full.render(TEST_DATA) + + with open("output_markdown_full.md", "w", encoding="utf-8") as f: + f.write(md_output_full) + + renderer_simple = MarkdownRenderer(enable_syntax_hints=False, enable_emoji=False) + md_output_simple = renderer_simple.render(TEST_DATA) + + with open("output_markdown_simple.md", "w", encoding="utf-8") as f: + f.write(md_output_simple) + + print(md_output_full[:100] + "...") + + +def demo_json_formats(): + renderer_pretty = JSONRenderer(pretty=True, indent=2) + json_output_pretty = renderer_pretty.render(TEST_DATA) + + with open("output_json_pretty.json", "w", encoding="utf-8") as f: + f.write(json_output_pretty) + + renderer_compact = JSONRenderer(pretty=False) + json_output_compact = renderer_compact.render(TEST_DATA) + + with open("output_json_compact.json", "w", encoding="utf-8") as f: + f.write(json_output_compact) + + renderer_sorted = JSONRenderer(pretty=True, indent=4, sort_keys=True) + json_output_sorted = renderer_sorted.render(TEST_DATA) + + with open("output_json_sorted.json", "w", encoding="utf-8") as f: + f.write(json_output_sorted) + +def main(): + + try: + demo_html_themes() + demo_html_custom_css() + demo_markdown_styles() + demo_json_formats() + + except Exception as e: + print(f"\n{e!r}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/examples/rule_system_demo.py b/examples/rule_system_demo.py new file mode 100644 index 0000000..d38056b --- /dev/null +++ b/examples/rule_system_demo.py @@ -0,0 +1,210 @@ +import sys +from pathlib import Path + +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "src")) + +from conventionalrp.core.rules import Rule, RuleEngine +from conventionalrp.core.processor import Processor + + +def example_1_simple_rules(): + rule = Rule( + name="tag_dice_rolls", + condition={"type": "dice_roll"}, + action={"type": "add_tag", "tag": "game_mechanics"}, + priority=100 + ) + + engine = RuleEngine() + engine.add_rule(rule) + + data = {"type": "dice_roll", "content": "[d20 = 18]"} + + print(f"Original data: {data}") + result = engine.process(data) + print(f"Processed result: {result}") + print() + + +def example_2_conditional_rules(): + engine = RuleEngine() + + # Rule1: High rolls (>=15) get "success" tag + engine.add_rule_dict( + name="high_roll", + condition={ + "type": "dice_roll", + "result": {"type": "greater_than", "value": 15} + }, + action={"type": "add_tag", "tag": "success"}, + priority=100 + ) + + # Rule2: Low rolls (<10) get "failure" tag + engine.add_rule_dict( + name="low_roll", + condition={ + "type": "dice_roll", + "result": {"type": "less_than", "value": 10} + }, + action={"type": "add_tag", "tag": "failure"}, + priority=100 + ) + + test_cases = [ + {"type": "dice_roll", "result": 18, "content": "[d20 = 18]"}, + {"type": "dice_roll", "result": 5, "content": "[d20 = 5]"}, + {"type": "dice_roll", "result": 12, "content": "[d20 = 12]"}, + ] + + for data in test_cases: + result = engine.process(data) + print(f"结果: {data['result']} -> 标签: {result.get('tags', [])}") + print() + + +def example_3_field_transformation(): + engine = RuleEngine() + + # Rule: Normalize speaker names + engine.add_rule_dict( + name="normalize_speaker", + condition={"type": "metadata"}, + action={ + "type": "transform", + "field": "speaker", + "function": "upper" + }, + priority=90 + ) + + data = { + "type": "metadata", + "speaker": "艾莉娅", + "timestamp": "2025-10-24 14:30:01" + } + + print(f"Original data: {data}") + result = engine.process(data) + print(f"Processed result: {result}") + print() + + +def example_4_processor_with_rules(): + processor = Processor() + + # Add rule: Highlight important dialogues + processor.add_rule(Rule( + name="highlight_important_dialogue", + condition={ + "type": "dialogue", + "content": {"type": "contains", "value": "重要"} + }, + action={"type": "add_tag", "tag": "important"}, + priority=100 + )) + + # Add rule: Mark all metadata as processed + processor.add_rule(Rule( + name="mark_metadata", + condition={"type": "metadata"}, + action={"type": "set_field", "field": "processed_by", "value": "rule_engine"}, + priority=90 + )) + + tokens = [ + {"type": "metadata", "speaker": "DM", "timestamp": "2025-10-24"}, + {"type": "dialogue", "content": "这是重要的线索"}, + {"type": "dialogue", "content": "普通对话"}, + {"type": "dice_roll", "result": 20}, + ] + + print(f"Processing {len(tokens)} tokens...") + results = processor.process_tokens(tokens) + + for i, result in enumerate(results): + print(f" [{i+1}] {result.get('type')}: " + f"Tags={result.get('tags', [])} " + f"Processed by={result.get('processed_by', 'N/A')}") + print() + + +def example_5_custom_processor(): + processor = Processor() + + # Custom processing function: Count characters + def add_char_count(data): + if "content" in data: + data["char_count"] = len(data["content"]) + return data + + # Custom processing function: Ensure timestamp exists + def ensure_timestamp(data): + if "timestamp" not in data: + from datetime import datetime + data["timestamp"] = datetime.now().isoformat() + return data + + processor.add_processor(add_char_count) + processor.add_processor(ensure_timestamp) + + test_data = [ + {"type": "dialogue", "content": "你好世界"}, + {"type": "text", "content": "这是一段很长的文本内容"}, + ] + + results = processor.process_tokens(test_data) + + for result in results: + print(f" {result.get('type')}: " + f"Character count={result.get('char_count')} " + f"Timestamp={result.get('timestamp', 'N/A')[:19]}") + print() + + +def example_6_priority_and_order(): + engine = RuleEngine() + + engine.add_rule_dict( + name="low_priority", + condition={"type": "test"}, + action={"type": "set_field", "field": "processed_by", "value": "low"}, + priority=10 + ) + + engine.add_rule_dict( + name="high_priority", + condition={"type": "test"}, + action={"type": "set_field", "field": "processed_by", "value": "high"}, + priority=100 + ) + + engine.add_rule_dict( + name="medium_priority", + condition={"type": "test"}, + action={"type": "set_field", "field": "processed_by", "value": "medium"}, + priority=50 + ) + + data = {"type": "test"} + + result1 = engine.process(data, apply_all=False) + print(f"Only apply the highest priority matching rule: {result1}") + + result2 = engine.process(data, apply_all=True) + print(f"Apply all matching rules: {result2}") + print() + + +def main(): + example_1_simple_rules() + example_2_conditional_rules() + example_3_field_transformation() + example_4_processor_with_rules() + example_5_custom_processor() + example_6_priority_and_order() + + +if __name__ == "__main__": + main() -- cgit v1.2.3-70-g09d2