Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revisit shared pool rerolls #78

Open
HighDiceRoller opened this issue Jul 30, 2022 · 4 comments
Open

Revisit shared pool rerolls #78

HighDiceRoller opened this issue Jul 30, 2022 · 4 comments

Comments

@HighDiceRoller
Copy link
Owner

HighDiceRoller commented Jul 30, 2022

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.
@HighDiceRoller
Copy link
Owner Author

HighDiceRoller commented Apr 3, 2023

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 Pools 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.

@HighDiceRoller
Copy link
Owner Author

HighDiceRoller commented Dec 27, 2023

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.

@HighDiceRoller
Copy link
Owner Author

HighDiceRoller commented May 26, 2024

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 Pools in the mixture rather than general MultisetGenerators. This would allow to use the Pool specialization of highest etc.

@HighDiceRoller
Copy link
Owner Author

HighDiceRoller commented May 27, 2024

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 the Pool 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant