Revisit shared pool rerolls
HighDiceRoller opened this issue · 5 comments
Possible signature:
Die.shared_reroll(rolls, condition, count, depth=None)
Should this return the sum, or some sort of OutcomeCountGenerator
?
If we go with some sort of distribution across pools, should the generator emit all possible counts at a time? Or do we treat each possible generator separately? It's possible that memoization works better in this case.
Using a Die
of generators has some problems:
- It requires that generators be sortable, which there are no meaningful semantics for; comparators may imply set-like relationships which are not appropriate for sorting.
- There's no way to tell by itself whether
evaluate(die_of_generators)
wants to evaluate using the generators themselves as outcomes, or to evaluate for each possible generator. So we'd still need a flag or separate method. - Very few
Die
methods seem applicable to generators. In fact, it's more generator methods that would apply.
Revisiting this considerably later:
Shared rerolls break indistinguishability---if you're trying to maximize the sum of d6s, if you roll a bunch of 4s, you will stick with 4s even if you would have rolled 6s on the rerolls. On the other hand if you rolled the 6s first, you get those 6s.
As an outside routine, we would want a split
function to divide a population into separate sub-populations. Then we could produce a probability distribution over the following groups:
- Dice that ended up on a reroll.
- Dice the player decided to keep.
- Dice that they player wanted to reroll, but ran out of shared rerolls.
The first could be decomposed into the last two if this is better for performance (probably).
Maybe we can express this as a MultisetGenerator
by splitting into concrete Pool
s on the first iteration? I don't know if I want to make generators valid values for dice though...
Extending to decks seems plausible though would take some extra care.
Another question is what the motivation is. 5e D&D has shared rerolls in Empowered Spell, but this is probably better solved by a multiset evaluator since the lowest dice get rerolled first.
After chatting with @alexrecarey I'm going to try to push this forward. Potential questions:
Allow for prioritizing outcomes to reroll?
With this we could handle cases like Empowered Spell with something like
d6.reroll_pool(8, [1, 2, 3], limit=4, depth=1, prioritize='lowest')
this example meaning 8d6, reroll up to 4 dice that rolled below average, prioritizing the lowest to be rerolled.
With regards to performance, the question would be how to avoid having to materialize the specific rerollable outcomes. Perhaps I can do the split first, then use highest/lowest
to determine which rerollable outcomes were not rerolled. But then I'd still have to additive union the two pools, which prevents using Pool
specializations on the result.
Interpret the limit on the number of rerolls as per-depth or total?
Limiting the total number of rerolls rather than per-depth seems probably a bad idea:
- It's rare.
- It's rare with good reason. If the allowed depth is >= the limit of rerolls, you're going to want to reroll just one die at a time so you have the maximum information before assigning each reroll. That's going to slow the game down, so I would hope no game designer actually does this.
- Apart from that, this can turn into a more difficult optimization problem. For example, if Empowered Spell allowed higher depths, then there are cases that you would want to reroll a 4 since with enough rerolls you would have a good chance of turning it into a 5 or 6. That's going to make it harder to come up with an efficient solution and for the user to understand exactly what it is that's being calculated.
What default depth to use?
Currently the single-die reroll
defaults depth = None
; in my experience depth = 1
and depth = None
appear pretty often in games. When it comes to pool rerolls I think depth = 1
is more common. But I don't want to have a different default for the single-die and the pool version.
Another option is to effectively only allow depth = 1
. Unlimited depth with rerolls-per-depth is effectively just unlimited rerolls period, at which point you might as well use the single-die version. Finite depths above 1 seem rare.
We could also make depth
a mandatory keyword argument. This might be a good idea anyways to force the user to make the depth unambiguous-at-a-glance.
MixtureMultisetGenerator
-> PoolMixture
The idea would be to only allow Pool
s in the mixture rather than general MultisetGenerator
s. This would allow to use the Pool
specialization of highest
etc.
It seems the easiest thing to do when selecting which dice to reroll is to pick at random. However, this seems less than intuitive for the user. Options:
- Just accept it.
- Only allow to reroll a single outcome.
- Allow to use a priority, but then we have the issue as above.
- The
keep
issue could potentially be improved if we track whether generators produce fixed-size multisets, and allow the specialization for general expressions if so. We may still want thePool
specialization since that one also has the benefit of skipping to the end if no kept dice remain. - Or maybe using
lowest(drop)
is enough?
- The
Seems good for now.