From f0b0356d8fe363e75b0b10e8712c93cfc1dc84b6 Mon Sep 17 00:00:00 2001 From: Sanne Bregman Date: Fri, 24 Feb 2023 20:06:20 +0100 Subject: [PATCH] Add Fate die support This is kinda rough, but should work fine. A physical Fate die is a d6 containing two - signs, two + signs, and two blank sides. This is emulated using -1 for the - sign, +1 for the + sign, and a 0 for the blank side. The result is that we can still add them up (so 10dF work), subtract them, keep highest etcetera. Also added a FateLiteral class that's optional, it's just there to make the end result (when looking at `result.expr`) a bit nicer. --- README.md | 2 +- d20/diceast.py | 2 ++ d20/expression.py | 16 +++++++++++++++- d20/grammar.lark | 2 +- tests/test_dice.py | 4 ++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 762a366..bec092c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These are the atoms used at the base of the syntax tree. | Name | Syntax | Description | Examples | |---------|-----------------------------------|-----------------------|--------------------------------| | literal | `INT`, `DECIMAL` | A literal number. | `1`, `0.5`, `3.14` | -| dice | `INT? "d" (INT \| "%")` | A set of die. | `d20`, `3d6` | +| dice | `INT? "d" (INT \| "%" \| "F" \| "f")` | A set of die. | `d20`, `3d6` | | set | `"(" (num ("," num)* ","?)? ")"` | A set of expressions. | `()`, `(2,)`, `(1, 3+3, 1d20)` | Note that `(3d6)` is equivalent to `3d6`, but `(3d6,)` is the set containing the one element `3d6`. diff --git a/d20/diceast.py b/d20/diceast.py index 8f8a707..cd649b7 100644 --- a/d20/diceast.py +++ b/d20/diceast.py @@ -428,6 +428,8 @@ def __init__(self, num, size): self.num = int(num) if str(size) == "%": self.size = str(size) + elif str(size) == "F" or str(size) == "f": + self.size = "F" else: self.size = int(size) diff --git a/d20/expression.py b/d20/expression.py index 4c16a7b..fda4949 100644 --- a/d20/expression.py +++ b/d20/expression.py @@ -175,6 +175,18 @@ def __repr__(self): return f"" +class FateLiteral(Literal): + """A literal for Fate dice""" + + def __repr__(self): + if self.number == -1: + return "" + elif self.number == 0: + return "" + else: + return "" + + class UnOp(Number): """Represents a unary operation.""" @@ -416,12 +428,14 @@ def children(self): return [] def _add_roll(self): - if self.size != "%" and self.size < 1: + if self.size != "%" and self.size != "F" and self.size < 1: raise errors.RollValueError("Cannot roll a 0-sided die.") if self._context: self._context.count_roll() if self.size == "%": n = Literal(random.randrange(10) * 10) + if self.size == "F": + n = FateLiteral(random.choice([-1, 0, 1])) # Technically it's -/0/+, but this also works with 4dF else: n = Literal(random.randrange(self.size) + 1) # 200ns faster than randint(1, self._size) self.values.append(n) diff --git a/d20/grammar.lark b/d20/grammar.lark index d45f212..7de1c10 100644 --- a/d20/grammar.lark +++ b/d20/grammar.lark @@ -46,7 +46,7 @@ DICE_OPERATOR: "rr" | "ro" | "ra" | "e" | "mi" | "ma" diceexpr: INTEGER? "d" DICE_VALUE -DICE_VALUE: INTEGER | "%" +DICE_VALUE: INTEGER | "%" | "F" | "f" selector: [SELTYPE] INTEGER diff --git a/tests/test_dice.py b/tests/test_dice.py index 5715821..403925a 100644 --- a/tests/test_dice.py +++ b/tests/test_dice.py @@ -5,6 +5,8 @@ STANDARD_EXPRESSIONS = [ "1d20", "1d%", + "1dF", + "1df" "1+1", "4d6kh3", "(1)", @@ -13,6 +15,7 @@ "4*(3d8kh2+9[fire]+(9d2e2+3[cold])/2)", "(1d4, 2+2, 3d6kl1)kh1", "((10d6kh5)kl2)kh1", + "17dFkh3" ] @@ -46,6 +49,7 @@ def test_sane_totals(): assert 1 <= r("(((1d6)))") <= 6 assert 4 <= r("(1d4, 2+2, 3d6kl1)kh1") <= 6 assert 1 <= r("((10d6kh5)kl2)kh1") <= 6 + assert -1 <= r("1dF") <= 1 def test_pemdas():