Contexte
Je joue régulièrement à D&D avec des amis. Tout en parlant de la complexité de certains systèmes / versions lorsqu'il s'agit de lancer des dés et d'appliquer des bonus et des pénalités, nous avons proposé en plaisantant une certaine complexité supplémentaire pour les expressions de roulement de dés. Certains d'entre eux étaient trop scandaleux (comme l'extension d'expressions de dés simples comme 2d6
aux arguments de matrice 1 ), mais les autres constituent un système intéressant.
Le défi
Étant donné une expression de dés complexe, évaluez-la selon les règles suivantes et sortez le résultat.
Règles d'évaluation de base
- Chaque fois qu'un opérateur attend un entier mais reçoit une liste pour un opérande, la somme de cette liste est utilisée
- Chaque fois qu'un opérateur attend une liste mais reçoit un entier pour un opérande, l'entier est traité comme une liste à un élément contenant cet entier
Les opérateurs
Tous les opérateurs sont des opérateurs d'infixes binaires. Aux fins d'explication, a
sera l'opérande gauche et b
sera l'opérande droit. La notation de liste sera utilisée pour des exemples où les opérateurs peuvent prendre des listes comme opérandes, mais les expressions réelles ne sont constituées que d'entiers positifs et d'opérateurs.
d
: sortie d'a
entiers aléatoires uniformes indépendants dans la plage[1, b]
- Priorité: 3
- Les deux opérandes sont des entiers
- Exemples:
3d4 => [1, 4, 3]
,[1, 2]d6 => [3, 2, 6]
t
: prendre lesb
valeurs les plus faibles dea
- Priorité: 2
a
est une liste,b
est un entier- Si
b > len(a)
, toutes les valeurs sont retournées - Exemples:
[1, 5, 7]t1 => [1]
,[5, 18, 3, 9]t2 => [3, 5]
,3t5 => [3]
T
: prendre lesb
valeurs les plus élevées dea
- Priorité: 2
a
est une liste,b
est un entier- Si
b > len(a)
, toutes les valeurs sont retournées - Exemples:
[1, 5, 7]T1 => [7]
,[5, 18, 3, 9]T2 => [18, 9]
,3T5 => [3]
r
: si des élémentsb
sonta
dedans, relancez ces éléments, en utilisant lad
déclaration qui les a générés- Priorité: 2
- Les deux opérandes sont des listes
- Le relancement n'est effectué qu'une seule fois, il est donc possible d'avoir encore des éléments
b
dans le résultat - Exemples:
3d6r1 => [1, 3, 4] => [6, 3, 4]
,2d4r2 => [2, 2] => [3, 2]
,3d8r[1,8] => [1, 8, 4] => [2, 2, 4]
R
: si des élémentsb
sont dansa
, relancez ces éléments à plusieurs reprises jusqu'à ce qu'aucun élément deb
ne soit présent, en utilisant lad
déclaration qui les a générés- Priorité: 2
- Les deux opérandes sont des listes
- Exemples:
3d6R1 => [1, 3, 4] => [6, 3, 4]
,2d4R2 => [2, 2] => [3, 2] => [3, 1]
,3d8R[1,8] => [1, 8, 4] => [2, 2, 4]
+
: ajoutera
etb
ensemble- Priorité: 1
- Les deux opérandes sont des entiers
- Exemples:
2+2 => 4
,[2]+[2] => 4
,[3, 1]+2 => 6
-
: soustraireb
dea
- Priorité: 1
- Les deux opérandes sont des entiers
b
sera toujours inférieur àa
- Exemples:
2-1 => 1
,5-[2] => 3
,[8, 3]-1 => 10
.
: concaténera
etb
ensemble- Priorité: 1
- Les deux opérandes sont des listes
- Exemples:
2.2 => [2, 2]
,[1].[2] => [1, 2]
,3.[4] => [3, 4]
_
: sortiea
avec tous les élémentsb
supprimés- Priorité: 1
- Les deux opérandes sont des listes
- Exemples:
[3, 4]_[3] => [4]
,[2, 3, 3]_3 => [2]
,1_2 => [1]
Règles supplémentaires
- Si la valeur finale d'une expression est une liste, elle est additionnée avant la sortie
- L'évaluation des termes ne produira que des entiers positifs ou des listes d'entiers positifs - toute expression qui aboutit à un entier non positif ou une liste contenant au moins un entier non positif verra ces valeurs remplacées par
1
s - Les parenthèses peuvent être utilisées pour regrouper les termes et spécifier l'ordre d'évaluation
- Les opérateurs sont évalués par ordre de priorité la plus élevée à la priorité la plus basse, l'évaluation se déroulant de gauche à droite dans le cas d'une priorité liée (ainsi
1d4d4
serait évalué comme(1d4)d4
) - L'ordre des éléments dans les listes n'a pas d'importance - il est parfaitement acceptable pour un opérateur qui modifie une liste de la renvoyer avec ses éléments dans un ordre relatif différent
- Les termes qui ne peuvent pas être évalués ou qui entraîneraient une boucle infinie (comme
1d1R1
ou3d6R[1, 2, 3, 4, 5, 6]
) ne sont pas valides
Cas de test
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
Tous, sauf le dernier cas de test, ont été générés avec l'implémentation de référence.
Exemple travaillé
Expression: 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]
(plein: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
(terminé)
Implémentation de référence
Cette implémentation de référence utilise la même graine constante ( 0
) pour évaluer chaque expression pour des sorties cohérentes testables. Il attend une entrée sur STDIN, avec des retours à la ligne séparant chaque expression.
#!/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]: Notre définition des adb
arguments pour la matrice était de rouler AdX
pour chacun X
dans a * b
, où A = det(a * b)
. De toute évidence, c'est trop absurde pour ce défi.
-
ce b
sera toujours moins que a
je ne vois aucun moyen d'obtenir des entiers non positifs, donc la deuxième règle supplémentaire semble inutile. OTOH, _
pourrait entraîner une liste vide, ce qui semble utile dans les mêmes cas, mais qu'est-ce que cela signifie lorsqu'un entier est nécessaire? Normalement, je dirais que la somme est 0
...
0
. Selon la règle du non-positif, il serait évalué comme a 1
.
[1,2]_([1]_[1])
est [1,2]
?
[2]
, car [1]_[1] -> [] -> 0 -> 1 -> [1]
.