sfondo
Gioco a D&D regolarmente con alcuni amici. Mentre parliamo della complessità di alcuni sistemi / versioni quando si tratta di lanciare dadi e applicare bonus e penalità, scherzosamente abbiamo trovato un'ulteriore complessità per le espressioni di lancio dei dadi. Alcuni di loro erano troppo scandalosi (come estendere semplici espressioni di dadi come 2d6
matrici di argomenti 1 ), ma il resto crea un sistema interessante.
La sfida
Data un'espressione complessa dei dadi, valutala secondo le seguenti regole e genera il risultato.
Regole di valutazione di base
- Ogni volta che un operatore si aspetta un numero intero ma riceve un elenco per un operando, viene utilizzata la somma di tale elenco
- Ogni volta che un operatore si aspetta un elenco ma riceve un numero intero per un operando, l'intero viene trattato come un elenco di un elemento contenente tale numero intero
operatori
Tutti gli operatori sono operatori di infissione binaria. Ai fini della spiegazione, a
sarà l'operando di sinistra e b
sarà l'operando di destra. La notazione elenco verrà utilizzata per esempi in cui gli operatori possono prendere elenchi come operandi, ma le espressioni effettive sono costituite solo da numeri interi e operatori positivi.
d
: outputa
interi casuali uniformi indipendenti nell'intervallo[1, b]
- Precedenza: 3
- Entrambi gli operandi sono numeri interi
- Esempi:
3d4 => [1, 4, 3]
,[1, 2]d6 => [3, 2, 6]
t
: prende ib
valori più bassi daa
- Precedenza: 2
a
è un elenco,b
è un numero intero- Se
b > len(a)
vengono restituiti tutti i valori - Esempi:
[1, 5, 7]t1 => [1]
,[5, 18, 3, 9]t2 => [3, 5]
,3t5 => [3]
T
: prende ib
valori più alti daa
- Precedenza: 2
a
è un elenco,b
è un numero intero- Se
b > len(a)
vengono restituiti tutti i valori - Esempi:
[1, 5, 7]T1 => [7]
,[5, 18, 3, 9]T2 => [18, 9]
,3T5 => [3]
r
: Se qualche elemento inb
sonoa
, ritirare quegli elementi, usando qualsiasid
dichiarazione li ha generati- Precedenza: 2
- Entrambi gli operandi sono elenchi
- Il rilancio viene eseguito una sola volta, quindi è possibile avere ancora elementi
b
nel risultato - Esempi:
3d6r1 => [1, 3, 4] => [6, 3, 4]
,2d4r2 => [2, 2] => [3, 2]
,3d8r[1,8] => [1, 8, 4] => [2, 2, 4]
R
: Se qualche elemento inb
sonoa
, ritirare quegli elementi più volte fino a quando non gli elementi dib
sono presenti, utilizzando qualsiasid
dichiarazione li ha generati- Precedenza: 2
- Entrambi gli operandi sono elenchi
- Esempi:
3d6R1 => [1, 3, 4] => [6, 3, 4]
,2d4R2 => [2, 2] => [3, 2] => [3, 1]
,3d8R[1,8] => [1, 8, 4] => [2, 2, 4]
+
: aggiungia
eb
insieme- Precedenza: 1
- Entrambi gli operandi sono numeri interi
- Esempi:
2+2 => 4
,[2]+[2] => 4
,[3, 1]+2 => 6
-
: sottrarreb
daa
- Precedenza: 1
- Entrambi gli operandi sono numeri interi
b
sarà sempre inferiore aa
- Esempi:
2-1 => 1
,5-[2] => 3
,[8, 3]-1 => 10
.
: concatenatia
eb
insieme- Precedenza: 1
- Entrambi gli operandi sono elenchi
- Esempi:
2.2 => [2, 2]
,[1].[2] => [1, 2]
,3.[4] => [3, 4]
_
: outputa
con tutti gli elementib
rimossi- Precedenza: 1
- Entrambi gli operandi sono elenchi
- Esempi:
[3, 4]_[3] => [4]
,[2, 3, 3]_3 => [2]
,1_2 => [1]
Regole aggiuntive
- Se il valore finale di un'espressione è un elenco, viene sommato prima dell'output
- La valutazione dei termini comporterà solo numeri interi positivi o elenchi di numeri interi positivi - qualsiasi espressione che dia luogo a un numero intero non positivo o a un elenco contenente almeno un numero intero non positivo avrà quei valori sostituiti da
1
s - Le parentesi possono essere utilizzate per raggruppare i termini e specificare l'ordine di valutazione
- Gli operatori vengono valutati in ordine di precedenza dal più basso al precedente, con la valutazione che procede da sinistra a destra in caso di precedenza legata (quindi
1d4d4
sarebbe valutata come(1d4)d4
) - L'ordine degli elementi negli elenchi non ha importanza: è perfettamente accettabile per un operatore che modifica un elenco restituirlo con i suoi elementi in un diverso ordine relativo
- I termini che non possono essere valutati o risulterebbero in un ciclo infinito (come
1d1R1
o3d6R[1, 2, 3, 4, 5, 6]
) non sono validi
Casi test
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
Tutti tranne l'ultimo caso di test sono stati generati con l'implementazione di riferimento.
Esempio lavorato
Espressione: 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]
(completo1d(([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
(fatto)
Implementazione di riferimento
Questa implementazione di riferimento utilizza lo stesso seed costante ( 0
) per valutare ogni espressione per output verificabili e coerenti. Si aspetta input su STDIN, con le nuove righe che separano ogni espressione.
#!/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]: La nostra definizione di adb
per gli argomenti di matrice era di rollare AdX
per ciascuno X
in a * b
, dove A = det(a * b)
. Chiaramente è troppo assurdo per questa sfida.
-
che b
sarà sempre inferiore a a
non vedo alcun modo per ottenere numeri interi non positivi, quindi la seconda regola aggiuntiva sembra inutile. OTOH, _
potrebbe risultare in un elenco vuoto, che sembra utile negli stessi casi, ma cosa significa quando è necessario un numero intero? Normalmente direi che la somma è 0
...
0
. In base alla regola dei non positivi, sarebbe valutata come a 1
.
[1,2]_([1]_[1])
è [1,2]
?
[2]
, perché [1]_[1] -> [] -> 0 -> 1 -> [1]
.