Fondo
Juego D&D regularmente con algunos amigos. Mientras hablamos de la complejidad de algunos sistemas / versiones cuando se trata de lanzar dados y aplicar bonos y penalizaciones, bromeamos con cierta complejidad adicional para las expresiones de lanzamiento de dados. Algunos de ellos eran demasiado escandalosos (como extender expresiones simples de dados, como los 2d6
argumentos de matriz 1 ), pero el resto crea un sistema interesante.
El reto
Dada una expresión de dados compleja, evalúela de acuerdo con las siguientes reglas y genere el resultado.
Reglas básicas de evaluación
- Siempre que un operador espera un número entero pero recibe una lista para un operando, se utiliza la suma de esa lista
- Cada vez que un operador espera una lista pero recibe un entero para un operando, el entero se trata como una lista de un elemento que contiene ese entero
Operadores
Todos los operadores son operadores infix binarios. Para fines de explicación, a
será el operando izquierdo y b
será el operando derecho. La notación de lista se usará para ejemplos en los que los operadores pueden tomar listas como operandos, pero las expresiones reales consisten solo en enteros positivos y operadores.
d
: salida dea
enteros aleatorios uniformes independientes en el rango[1, b]
- Precedencia: 3
- Ambos operandos son enteros.
- Ejemplos:
3d4 => [1, 4, 3]
,[1, 2]d6 => [3, 2, 6]
t
: toma losb
valores más bajos dea
- Precedencia: 2
a
es una lista,b
es un entero- Si
b > len(a)
, se devuelven todos los valores - Ejemplos:
[1, 5, 7]t1 => [1]
,[5, 18, 3, 9]t2 => [3, 5]
,3t5 => [3]
T
: toma losb
valores más altos dea
- Precedencia: 2
a
es una lista,b
es un entero- Si
b > len(a)
, se devuelven todos los valores - Ejemplos:
[1, 5, 7]T1 => [7]
,[5, 18, 3, 9]T2 => [18, 9]
,3T5 => [3]
r
: Si cualquier elemento enb
están ena
, volver a tirar esos elementos, usando cualquierd
declaración que genera- Precedencia: 2
- Ambos operandos son listas
- El cambio de rol se realiza solo una vez, por lo que aún es posible tener elementos de
b
en el resultado - Ejemplos:
3d6r1 => [1, 3, 4] => [6, 3, 4]
,2d4r2 => [2, 2] => [3, 2]
,3d8r[1,8] => [1, 8, 4] => [2, 2, 4]
R
: Si cualquier elemento enb
están ena
, volver a tirar los elementos repetidamente hasta que no hay elementos deb
están presentes, usando cualquierd
declaración que genera- Precedencia: 2
- Ambos operandos son listas
- Ejemplos:
3d6R1 => [1, 3, 4] => [6, 3, 4]
,2d4R2 => [2, 2] => [3, 2] => [3, 1]
,3d8R[1,8] => [1, 8, 4] => [2, 2, 4]
+
: agregara
yb
juntos- Precedencia: 1
- Ambos operandos son enteros.
- Ejemplos:
2+2 => 4
,[2]+[2] => 4
,[3, 1]+2 => 6
-
: restarb
dea
- Precedencia: 1
- Ambos operandos son enteros.
b
siempre será menor quea
- Ejemplos:
2-1 => 1
,5-[2] => 3
,[8, 3]-1 => 10
.
: concatenara
yb
juntos- Precedencia: 1
- Ambos operandos son listas
- Ejemplos:
2.2 => [2, 2]
,[1].[2] => [1, 2]
,3.[4] => [3, 4]
_
: salidaa
con todos los elementos deb
eliminado- Precedencia: 1
- Ambos operandos son listas
- Ejemplos:
[3, 4]_[3] => [4]
,[2, 3, 3]_3 => [2]
,1_2 => [1]
Reglas Adicionales
- Si el valor final de una expresión es una lista, se suma antes de la salida
- La evaluación de los términos solo dará como resultado enteros positivos o listas de enteros positivos: cualquier expresión que dé como resultado un entero no positivo o una lista que contenga al menos un entero no positivo tendrá esos valores reemplazados por
1
s - Los paréntesis pueden usarse para agrupar términos y especificar el orden de evaluación
- Los operadores se evalúan en orden de precedencia más alta a precedencia más baja, y la evaluación se realiza de izquierda a derecha en el caso de precedencia vinculada (por
1d4d4
lo que se evaluaría como(1d4)d4
) - El orden de los elementos en las listas no importa: es perfectamente aceptable que un operador que modifica una lista la devuelva con sus elementos en un orden relativo diferente
- Los términos que no pueden evaluarse o darían lugar a un bucle infinito (como
1d1R1
o3d6R[1, 2, 3, 4, 5, 6]
) no son válidos
Casos de prueba
Formato: 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
Todos menos el último caso de prueba se generaron con la implementación de referencia.
Ejemplo trabajado
Expresión: 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]
(completo: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
(hecho)
Implementación de referencia
Esta implementación de referencia utiliza la misma semilla constante ( 0
) para evaluar cada expresión para resultados comprobables y consistentes. Espera entrada en STDIN, con nuevas líneas que separan cada expresión.
#!/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]: Nuestra definición de adb
argumentos de matriz era rodar AdX
para cada X
en a * b
, donde A = det(a * b)
. Claramente eso es demasiado absurdo para este desafío.
-
que b
siempre será menor de a
lo que no veo forma de obtener enteros no positivos, por lo que la segunda regla adicional parece inútil. OTOH, _
podría dar como resultado una lista vacía, que parece útil en los mismos casos, pero ¿qué significa cuando se necesita un número entero? Normalmente diría que la suma es 0
...
0
. Por la regla de no no positivos, se evaluaría como a 1
.
[1,2]_([1]_[1])
es [1,2]
?
[2]
, porque [1]_[1] -> [] -> 0 -> 1 -> [1]
.