Hintergrund
Ich spiele regelmäßig D & D mit einigen Freunden. Während wir über die Komplexität einiger Systeme / Versionen beim Würfeln und Anwenden von Boni und Strafen sprachen, kamen wir scherzhaft auf eine zusätzliche Komplexität für das Würfeln von Ausdrücken. Einige von ihnen waren zu empörend (wie das Erweitern einfacher Würfelausdrücke wie 2d6
Matrixargumente 1 ), aber der Rest ergibt ein interessantes System.
Die Herausforderung
Bewerten Sie einen komplexen Würfelausdruck anhand der folgenden Regeln und geben Sie das Ergebnis aus.
Grundlegende Bewertungsregeln
- Wann immer ein Operator eine Ganzzahl erwartet, aber eine Liste für einen Operanden erhält, wird die Summe dieser Liste verwendet
- Wann immer ein Operator eine Liste erwartet, aber eine Ganzzahl für einen Operanden empfängt, wird die Ganzzahl als eine Liste mit einem Element behandelt, die diese Ganzzahl enthält
Betreiber
Alle Operatoren sind binäre Infixoperatoren. Zum Zwecke der Erklärung a
wird der linke Operand und b
der rechte Operand sein. Die Listennotation wird für Beispiele verwendet, in denen Operatoren Listen als Operanden verwenden können, tatsächliche Ausdrücke jedoch nur aus positiven ganzen Zahlen und Operatoren bestehen.
d
:a
Unabhängige, einheitliche Zufallszahlen im Bereich ausgeben[1, b]
- Vorrang: 3
- Beide Operanden sind ganze Zahlen
- Beispiele:
3d4 => [1, 4, 3]
,[1, 2]d6 => [3, 2, 6]
t
: nimm dieb
niedrigsten Werte vona
- Vorrang: 2
a
ist eine Liste,b
ist eine ganze Zahl- Wenn
b > len(a)
, werden alle Werte zurückgegeben - Beispiele:
[1, 5, 7]t1 => [1]
,[5, 18, 3, 9]t2 => [3, 5]
,3t5 => [3]
T
: nimm dieb
höchsten Werte vona
- Vorrang: 2
a
ist eine Liste,b
ist eine ganze Zahl- Wenn
b > len(a)
, werden alle Werte zurückgegeben - Beispiele:
[1, 5, 7]T1 => [7]
,[5, 18, 3, 9]T2 => [18, 9]
,3T5 => [3]
r
: Wenn Elemente in enthaltenb
sinda
, rollen Sie diese Elemente mit derd
generierten Anweisung erneut- Vorrang: 2
- Beide Operanden sind Listen
- Das erneute Rollen wird nur einmal durchgeführt, daher ist es möglich, noch Elemente
b
im Ergebnis zu haben - Beispiele:
3d6r1 => [1, 3, 4] => [6, 3, 4]
,2d4r2 => [2, 2] => [3, 2]
,3d8r[1,8] => [1, 8, 4] => [2, 2, 4]
R
: Wenn Elemente in enthaltenb
sinda
, wiederholen Sie diese Elemente, bis keine Elemente vonb
vorhanden sind, und verwenden Sie dabeid
die von ihnen generierte Anweisung- Vorrang: 2
- Beide Operanden sind Listen
- Beispiele:
3d6R1 => [1, 3, 4] => [6, 3, 4]
,2d4R2 => [2, 2] => [3, 2] => [3, 1]
,3d8R[1,8] => [1, 8, 4] => [2, 2, 4]
+
: addierena
undb
zusammen- Vorrang: 1
- Beide Operanden sind ganze Zahlen
- Beispiele:
2+2 => 4
,[2]+[2] => 4
,[3, 1]+2 => 6
-
: subtrahierenb
vona
- Vorrang: 1
- Beide Operanden sind ganze Zahlen
b
wird immer kleiner sein alsa
- Beispiele:
2-1 => 1
,5-[2] => 3
,[8, 3]-1 => 10
.
: verkettena
undb
zusammen- Vorrang: 1
- Beide Operanden sind Listen
- Beispiele:
2.2 => [2, 2]
,[1].[2] => [1, 2]
,3.[4] => [3, 4]
_
: Ausgabea
mit allen Elementen vonb
entfernt- Vorrang: 1
- Beide Operanden sind Listen
- Beispiele:
[3, 4]_[3] => [4]
,[2, 3, 3]_3 => [2]
,1_2 => [1]
Zusätzliche Regeln
- Wenn der Endwert eines Ausdrucks eine Liste ist, wird er vor der Ausgabe summiert
- Die Auswertung von Begriffen führt nur zu positiven Ganzzahlen oder Listen positiver Ganzzahlen. Bei jedem Ausdruck, der zu einer nicht positiven Ganzzahl oder einer Liste mit mindestens einer nicht positiven Ganzzahl führt, werden diese Werte durch
1
s ersetzt - Klammern können verwendet werden, um Begriffe zu gruppieren und die Reihenfolge der Auswertung festzulegen
- Operatoren werden in der Reihenfolge der höchsten bis zur niedrigsten Priorität ausgewertet, wobei die Auswertung bei gebundener Priorität von links nach rechts erfolgt (wird also
1d4d4
ausgewertet als(1d4)d4
). - Die Reihenfolge der Elemente in Listen spielt keine Rolle. Es ist durchaus akzeptabel, dass ein Operator, der eine Liste ändert, sie mit ihren Elementen in einer anderen relativen Reihenfolge zurückgibt
- Begriffe, die nicht bewertet werden können oder zu einer Endlosschleife führen würden (wie
1d1R1
oder3d6R[1, 2, 3, 4, 5, 6]
), sind ungültig
Testfälle
Format: input => possible output
1d20 => 13
2d6 => 8
4d6T3 => 11
2d20t1 => 13
5d8r1 => 34
5d6R1 => 20
2d6d6 => 23
3d2R1d2 => 3
(3d2R1)d2 => 11
1d8+3 => 10
1d8-3 => 4
1d6-1d2 => 2
2d6.2d6 => 12
3d6_1 => 8
1d((8d20t4T2)d(6d6R1r6)-2d4+1d2).(1d(4d6_3d3)) => 61
Alle außer dem letzten Testfall wurden mit der Referenzimplementierung generiert.
Gearbeitetes Beispiel
Ausdruck: 1d((8d20t4T2)d(6d6R1r6)-2d4+1d2).(1d(4d6_3d3))
8d20t4T2 => [19, 5, 11, 6, 19, 15, 4, 20]t4T2 => [4, 5, 6, 11]T2 => [11, 6]
(voll:1d(([11, 6])d(6d6R1r6)-2d4+1d2).(1d(4d6_3d3))
)6d6R1r6 => [2, 5, 1, 5, 2, 3]r1R6 => [2, 5, 3, 5, 2, 3]R6 => [2, 5, 3, 5, 2, 3]
(1d([11, 6]d[2, 5, 3, 5, 2, 3]-2d4+1d2).(1d(4d6_3d3))
)[11, 6]d[2, 5, 3, 5, 2, 3] => 17d20 => [1, 6, 11, 7, 2, 8, 15, 3, 4, 18, 11, 11, 1, 10, 8, 6, 11]
(1d([1, 6, 11, 7, 2, 8, 15, 3, 4, 18, 11, 11, 1, 10, 8, 6, 11]-2d4+1d2).(1d(4d6_3d3))
)2d4 => 7
(1d([1, 6, 11, 7, 2, 8, 15, 3, 4, 18, 11, 11, 1, 10, 8, 6, 11]-7+1d2).(1d(4d6_3d3))
)1d2 => 2
(1d([1, 6, 11, 7, 2, 8, 15, 3, 4, 18, 11, 11, 1, 10, 8, 6, 11]-7+2).(1d(4d6_3d3))
)[1, 6, 11, 7, 2, 8, 15, 3, 4, 18, 11, 11, 1, 10, 8, 6, 11]-7+2 => 133-7+2 => 128
(1d128).(1d(4d6_3d3))
)4d6_3d3 => [1, 3, 3, 6]_[3, 2, 2] => [1, 3, 3, 6, 3, 2, 2]
(1d128).(1d[1, 3, 3, 6, 3, 2, 2])
)1d[1, 3, 3, 6, 3, 2, 2] => 1d20 => 6
(1d128).(6)
)1d128 => 55
(55.6
)55.6 => [55, 6]
([55, 6]
)[55, 6] => 61
(getan)
Referenzimplementierung
Diese Referenzimplementierung verwendet dieselbe Konstante seed ( 0
), um jeden Ausdruck auf testbare, konsistente Ausgaben zu untersuchen. Es werden Eingaben in STDIN erwartet, wobei die einzelnen Ausdrücke durch Zeilenumbrüche voneinander getrennt sind.
#!/usr/bin/env python3
import re
from random import randint, seed
from collections import Iterable
from functools import total_ordering
def as_list(x):
if isinstance(x, Iterable):
return list(x)
else:
return [x]
def roll(num_sides):
return Die(randint(1, num_sides), num_sides)
def roll_many(num_dice, num_sides):
num_dice = sum(as_list(num_dice))
num_sides = sum(as_list(num_sides))
return [roll(num_sides) for _ in range(num_dice)]
def reroll(dice, values):
dice, values = as_list(dice), as_list(values)
return [die.reroll() if die in values else die for die in dice]
def reroll_all(dice, values):
dice, values = as_list(dice), as_list(values)
while any(die in values for die in dice):
dice = [die.reroll() if die in values else die for die in dice]
return dice
def take_low(dice, num_values):
dice = as_list(dice)
num_values = sum(as_list(num_values))
return sorted(dice)[:num_values]
def take_high(dice, num_values):
dice = as_list(dice)
num_values = sum(as_list(num_values))
return sorted(dice, reverse=True)[:num_values]
def add(a, b):
a = sum(as_list(a))
b = sum(as_list(b))
return a+b
def sub(a, b):
a = sum(as_list(a))
b = sum(as_list(b))
return max(a-b, 1)
def concat(a, b):
return as_list(a)+as_list(b)
def list_diff(a, b):
return [x for x in as_list(a) if x not in as_list(b)]
@total_ordering
class Die:
def __init__(self, value, sides):
self.value = value
self.sides = sides
def reroll(self):
self.value = roll(self.sides).value
return self
def __int__(self):
return self.value
__index__ = __int__
def __lt__(self, other):
return int(self) < int(other)
def __eq__(self, other):
return int(self) == int(other)
def __add__(self, other):
return int(self) + int(other)
def __sub__(self, other):
return int(self) - int(other)
__radd__ = __add__
__rsub__ = __sub__
def __str__(self):
return str(int(self))
def __repr__(self):
return "{} ({})".format(self.value, self.sides)
class Operator:
def __init__(self, str, precedence, func):
self.str = str
self.precedence = precedence
self.func = func
def __call__(self, *args):
return self.func(*args)
def __str__(self):
return self.str
__repr__ = __str__
ops = {
'd': Operator('d', 3, roll_many),
'r': Operator('r', 2, reroll),
'R': Operator('R', 2, reroll_all),
't': Operator('t', 2, take_low),
'T': Operator('T', 2, take_high),
'+': Operator('+', 1, add),
'-': Operator('-', 1, sub),
'.': Operator('.', 1, concat),
'_': Operator('_', 1, list_diff),
}
def evaluate_dice(expr):
return max(sum(as_list(evaluate_rpn(shunting_yard(tokenize(expr))))), 1)
def evaluate_rpn(expr):
stack = []
while expr:
tok = expr.pop()
if isinstance(tok, Operator):
a, b = stack.pop(), stack.pop()
stack.append(tok(b, a))
else:
stack.append(tok)
return stack[0]
def shunting_yard(tokens):
outqueue = []
opstack = []
for tok in tokens:
if isinstance(tok, int):
outqueue = [tok] + outqueue
elif tok == '(':
opstack.append(tok)
elif tok == ')':
while opstack[-1] != '(':
outqueue = [opstack.pop()] + outqueue
opstack.pop()
else:
while opstack and opstack[-1] != '(' and opstack[-1].precedence > tok.precedence:
outqueue = [opstack.pop()] + outqueue
opstack.append(tok)
while opstack:
outqueue = [opstack.pop()] + outqueue
return outqueue
def tokenize(expr):
while expr:
tok, expr = expr[0], expr[1:]
if tok in "0123456789":
while expr and expr[0] in "0123456789":
tok, expr = tok + expr[0], expr[1:]
tok = int(tok)
else:
tok = ops[tok] if tok in ops else tok
yield tok
if __name__ == '__main__':
import sys
while True:
try:
dice_str = input()
seed(0)
print("{} => {}".format(dice_str, evaluate_dice(dice_str)))
except EOFError:
exit()
[1]: Unsere Definition adb
für Matrixargumente war, AdX
für jedes X
in a * b
, wo zu rollen A = det(a * b)
. Das ist natürlich zu absurd für diese Herausforderung.
-
dass dies b
immer weniger ist, als a
ich sehe, gibt es keine Möglichkeit, nicht positive ganze Zahlen zu erhalten, daher scheint die zweite zusätzliche Regel sinnlos. OTOH, _
könnte zu einer leeren Liste führen, was in den gleichen Fällen nützlich erscheint. Was bedeutet es jedoch, wenn eine Ganzzahl benötigt wird? Normalerweise würde ich sagen, die Summe ist 0
...
0
. Nach der No-Non-Positives-Regel würde es als ausgewertet 1
.
[1,2]_([1]_[1])
ist [1,2]
?
[2]
, weil [1]_[1] -> [] -> 0 -> 1 -> [1]
.