更新目标:增强骰子系统,支持复杂数学运算(乘法、除法和括号表达式)
- 仅支持加法运算(通过
split("+")实现) - 不支持乘法、除法操作
- 不支持括号表达式
- 表达式解析逻辑简单,无法处理复杂优先级
# 现有实现的关键限制
terms = re.split(r"\+", result_expr) # 仅按加法分割目标:实现完整的四则运算和括号支持,同时保持安全性
技术方案:使用 Shunting-yard 算法(调度场算法)实现中缀表达式到后缀表达式(逆波兰表示法)的转换,然后计算后缀表达式的值。
实现步骤:
- 词法分析:将表达式分解为标记(tokens)
- 语法分析:使用 Shunting-yard 算法转换为后缀表达式
- 表达式求值:计算后缀表达式的最终结果
- ✅ 加法(已支持)
- ✅ 减法
- ✅ 乘法
- ✅ 除法(整除,向下取整)
- ✅ 括号优先级处理
- ✅ 基本骰子表达式(如
3d6,d20) - ✅ 百分骰
d% - ✅ 骰子表达式作为复杂表达式的一部分(如
(2d6+4)*2)
- ✅ 保留现有变量替换功能
- ✅ 允许变量在复杂表达式中使用(如
level*d8+con_mod)
- ✅ 禁止危险字符和代码执行
- ✅ 表达式长度限制
- ✅ 运算复杂度限制(防止资源耗尽)
核心函数重构:
def roll(expr: str, variables: Optional[Dict[str, int]] = None) -> int:
"""
解析并掷骰子表达式,支持四则运算和括号。
支持的表达式示例:
- 3d6+2
- (2d6-1)*4
- d20*(1+bonus/2)
- level*d8+con_mod
"""
# 安全性检查
# 变量替换
# 使用 Shunting-yard 算法解析表达式
# 计算并返回结果新增辅助函数:
def tokenize(expr: str) -> List[Token]:
"""将表达式字符串分解为标记列表"""
def to_postfix(tokens: List[Token]) -> List[Token]:
"""将中缀表达式转换为后缀表达式(Shunting-yard算法)"""
def evaluate_postfix(postfix_tokens: List[Token]) -> int:
"""计算后缀表达式的值"""
def roll_dice(count: int, sides: int) -> int:
"""掷指定数量和面数的骰子"""标记类型定义:
class TokenType(Enum):
NUMBER = "NUMBER"
DICE = "DICE"
PLUS = "PLUS"
MINUS = "MINUS"
MULTIPLY = "MULTIPLY"
DIVIDE = "DIVIDE"
LPAREN = "LPAREN"
RPAREN = "RPAREN"
class Token:
def __init__(self, type: TokenType, value: Any):
self.type = type
self.value = value-
表达式验证:
- 长度限制(最大 1000 字符)
- 字符白名单验证
- 括号匹配检查
-
运算限制:
- 最大骰子数量限制(单个表达式中不超过 100 个骰子)
- 最大计算步骤限制
# 基本运算测试
def test_basic_operations():
assert roll("2+3") == 5
assert roll("5-2") == 3
assert roll("3*4") == 12
assert roll("8/2") == 4
assert roll("7/2") == 3 # 整除,向下取整
# 骰子表达式测试
def test_dice_expressions():
result = roll("3d6")
assert 3 <= result <= 18
result = roll("d%") # 百分骰
assert 1 <= result <= 100# 混合运算测试
def test_complex_expressions():
# 使用种子确保可重复性
random.seed(42)
# 复杂表达式
result = roll("(2d6+4)*2") # (2d6=7+4=11)*2=22
assert result == 22
result = roll("d20+3*2") # d20=17+6=23
assert result == 23
# 嵌套括号
result = roll("((2+3)*4-6)/3") # ((5)*4-6)/3 = (20-6)/3 = 14/3 = 4
assert result == 4# 变量替换测试
def test_variable_substitution():
vars = {"level": 3, "bonus": 2, "con": 14}
result = roll("level*d8+bonus", vars)
assert isinstance(result, int)
# 复杂变量表达式
result = roll("(level+1)*bonus", vars)
assert result == (3+1)*2 == 8# 错误表达式测试
def test_error_handling():
with pytest.raises(ValueError):
roll("2d6+2d") # 无效的骰子表达式
with pytest.raises(ValueError):
roll("2d6/0") # 除零错误
with pytest.raises(ValueError):
roll("(2d6+3") # 括号不匹配
with pytest.raises(ValueError):
roll("2d6+eval('1+1')") # 安全性检查- 实现
Token类和TokenType枚举 - 开发
tokenize()函数,将表达式字符串转换为标记列表 - 测试词法分析正确性
- 开发
to_postfix()函数,处理操作符优先级和括号 - 确保正确处理四则运算的优先级规则
- 测试中缀到后缀表达式的转换
- 开发
evaluate_postfix()函数,计算后缀表达式 - 实现
roll_dice()函数处理骰子表达式 - 测试表达式求值的正确性
- 更新
roll()函数,集成新的解析和求值逻辑 - 保留并增强变量替换功能
- 强化安全性检查
- 实现所有测试用例
- 确保覆盖率 ≥ 95%
- 运行测试验证功能正确性
- 运行
black,ruff,mypy确保代码质量 - 优化解析和计算性能
- 检查潜在的安全问题
- 确保向后兼容:现有代码中使用的简单表达式(如
3d6,d20+5)应继续正常工作 - 变量替换逻辑保持一致
- 错误处理机制保持兼容
- 对于复杂表达式,Shunting-yard 算法的时间复杂度为 O(n)
- 骰子数量限制可以防止性能问题
- 表达式长度限制可以避免过长的处理时间
- 功能完整性:所有四则运算和括号表达式正确计算
- 向后兼容:现有代码不受影响
- 安全性:无代码注入风险
- 性能:表达式计算迅速完成
- 测试覆盖:测试覆盖率 ≥ 95%
- 代码质量:通过 Black/Ruff/MyPy 检查
预期成果:一个功能强大、安全可靠的骰子表达式解析器,支持复杂的数学运算,为 OSR 跑团引擎提供更灵活的规则实现能力。