dragon.garden

a hoard of cultivated treasures

Legit Cone Algorithm

Several tabletop RPGs define a cone area effect as "as many spaces wide at the end as it is long", but not a single VTT I've used implements this definition correctly, usually overlaying a triangle onto the grid without regard for the corners and middle potentially ending up at wildly different true distances from the cone's origin. There's a much better way to approach this simply by taking the cone definition at its word - a cone should hit exactly five spaces that are five spaces away from its source, regardless of how it's oriented. An animation of a grid with a cone sweeping around in a circle. The cone is colored in alternating bands, showing that each segment includes one more square than the last, regardless of the cone's orientation. This can be accomplished by taking a user-given aiming angle and then stepping outward from the origin point in rings. Each ring that's N spaces away should include N spaces that lie within the cone's area, so just take that ring's set of spaces and choose the N of them that lie closest to the chosen angle. With this approach, a cone of any angle covers the same area and abides by the game's natural constraints, reaching all the spaces it ought to and none that it shouldn't. The Legit Cone Algorithm works equally well on square and hex grids. (In my experience, games that use square grids with even-odd diagonal costs use a different cone definition anyway, so there's no need for this algorithm to be applicable on that metric.)

"Gambler's Vindication" Loot Drop Algorithm

Gamers, in general, fail to grasp the nuances of probability. If a Giant Rat has a 10% chance to drop a Giant Rat Tail, and they need to collect 10 of them for a quest, they're going to angrily complain if they only have 5 rat tails after killing 100 rats, despite the fact that such an unfortunate outcome will happen more often than rolling a natural 1 on a d20. As angry gamers usually do not take well to explanations of why they're wrong, it's better to forestall such a circumstance in the first place. Rather than the crude solution of just counting the rats and making sure the tenth one drops a tail if none of the prior ones have, we can consider as inspiration a randomization method that innately carries such a guarantee while keeping the odds nice and smooth: make a deck of ten cards, with one of them marked as a success, and draw a card for each rat killed. This clearly works, but is hard to extend well - what if a Small Rat has a 3% tail drop chance and a Large Rat has a 15% chance? Fortunately, we can abstract the notion of drawing from a deck into a continuous "deck mass". Instead of drawing one card, we draw 3% or 15% of the deck. All we need to track is how much mass the deck currently has - shuffling an already-random deck has no effect on expected probabilities, so we don't need to know or care where the winning point "really" is within it. So, abstracting it out, we pick a random success point in the remaining deck, then grab an appropriately sized chunk out of it for each loot drop opportunity, reducing its remaining mass accordingly. If we get a success, we add another deck's worth of mass to what's left. Codewise, we can store for each player a dictionary of item IDs and "chaff" values, initialized to 1 on first draw if not yet defined. Then, a loot drop roll is just:
success = (rnd() * chaff <= drop_chance)
chaff -= drop_chance
if(success) chaff += 1
Notably, this still behaves perfectly well even if the chaff value goes negative due to some external circumstance, such as if you want each enemy to drop only one item at a time and so your Rat Tail gets preempted by a Rare Rat Figurine, so long as you don't count as a success items that are not actually delivered. Your overdue tail will simply be delivered at the next possible opportunity, and the deck mass will automatically account for the extra draw you've done in the meantime.

OK, but this is a bit too predictable.

Fortunately, we can cover our tracks a bit while still making the long run work out more reliably than it really should. As inspiration, what if we started off shuffling two decks together, with two successes amongst them? Then you're not guaranteed to get a hit after a deck's worth of cards, but still guaranteed to get at least one within two decks' worth (with the other waiting not far after it if the first was deep in the stack). To convert this to our continuous model, we're notionally drawing half as much of the deck as before, but there are two chances to win within it. We could find the topmost of the two cards by taking the minimum of two random numbers, but fortunately for the sake of generalizability there's a way to transform a single random roll into an equivalent distribution: 1 - (1 - roll) ^ (1 / 2). Since this generalizes to any number of random rolls, let's add a new variable to our mix, "bulk", and for this example of two decks set it to 2. Our code now is something like:
transformed = 1 - (1 - roll) ^ (1 / bulk)
prize_depth = transformed * chaff
draw_amount = drop_chance / bulk
chaff -= draw_amount
success = (prize_depth <= draw_amount)
if(success) chaff += 1/bulk
This probably breaks in baffling ways if bulk is less than 1, but I haven't been brave enough to try it. Obviously if bulk is 0 you lose the game due to trying to draw from an empty deck.

Treasure Chests and Unreasonably Fair Dice

What do we do when we need exactly one outcome, such as a die roll that gives a single number or a chest containing exactly one item? Basically, we normalize the result weights to get a nominal probability for each one, draw for each item based on the resulting probability, and instead of our boolean draw result we take success_depth = prize_depth / draw_amount. The item with the lowest success_depth is the result. Every item gets its chaff depleted as if a normal draw was done, and the winning item gets awarded to the player and has its chaff replenished accordingly. This has some interesting results when applied to dicelike rolls with a bulk parameter of 1, basically working very similarly to the classic "random bag" of Tetris fame, in which you're guaranteed one of each piece within a batch of 7. A higher bulk makes it rare-but-possible to get three L pieces in a row, which isn't possible with a traditional random bag. If applied to a die roll using a bulk of 2, this stabilizing factor can give you rolls that seem pretty legitimately random moment to moment but if you roll a d6 60 times you're suspiciously likely to get exactly 10 of each result.

Universal Luck Formula (aka Double Green Marbles)

A lot of video games have a mysterious Luck stat, with unclear effects. Sometimes it even fails to have any effect, due to a bug that's obviously hard to verify in testing. Intuitively, I personally want a +100% Luck bonus to make rare good things (like critical hits) happen about twice as often and rare bad things (like critical fumbles) happen about half as often. A naive "roll twice and take the better result" accomplishes the former, but basically obliterates the odds of the latter, which is not ideal. Fortunately, another physical analogy can come to our rescue here. Consider a jar of marbles with green marbles being a good outcome when drawn at random and red marbles being a bad outcome. If there's 1 in 10 green marbles, adding another green marble to the jar makes it 2 in 11, about twice as much. If there's 1 red marble out of 10, doubling the green marbles makes it 1 out of 19, about half as much. And if it's 5 and 5, that turns into 10 green and 5 red, making your coin flip into a 2/3rds chance, which also feels like a pretty good outcome for "+100% Luck". Generalizing, this looks like
(green + bonus_green) / (green + bonus_green + red)
...at least for positive luck values. However, things go wonky if we try to apply a luck penalty. Instead, let's define net_luck as
(1 + fortune_total) / (1 + misfortune_total)
so a 100% luck bonus gives a net_luck of 2 and a -100% luck penalty gives a net_luck of 1/2. This gives us
new_chance = base * net_luck / (base * net_luck + 1 - base)
which is perfectly usable for many purposes, but we can do something even better. If we're using the standard convention of rnd() < chance for our random rolls, we can reorganize things to equivalently transform the roll itself which can then be processed as desired, such as using it as a roll on a random table. This gives us the delightfully elegant formula of
luck = (1 + fortune) / (1 + misfortune)
roll = rnd()
lucky_roll = roll / (roll + luck - roll * luck)
Having both the base and modified rolls at hand, we can do things like flag "lucky" results where the base roll failed but the modified one succeeded, to make the effects of our luck bonus directly apparent to the player!

Maximally Dense Meaningful Damage Upgrades

I'm a big fan of upgrades being immediately noticeable and impactful for the player - in almost any game genre, I personally feel like gaining +5% to anything is a mistake. When it comes to damage, I feel like a good rule is "every upgrade should make every enemy easier to defeat" (at least, until you're one-shotting them). The Legend of Zelda: a Link to the Past found a solution (and in fact, the only minimal solution that works for arbitrary enemy health values): every level of sword upgrade doubles its damage from the previous level. (If it were anything less than double, there'd be a potential for an enemy to take two hits to defeat both before and after the upgrade, if for example its health is exactly twice your initial damage.) If you constrain potential enemy health values, however, it's possible to pack progress much more densely: You can arrange it so that an enemy that takes 5 hits to defeat with your starting weapon takes one less hit with each upgrade, and the same one-less-hit pattern holds for each weaker enemy. And furthermore, it's possible to accomplish this with the damage values being genuine honest integers that are invariant across enemies. If you're already sold on that premise, here's a list of magic numbers you can just drop into a health_and_damage_tiers array:

5, 7, 11, 16, 23, 34, 49, 70, 103, 148, 211, 310, 445, 634, 931, 1336, 1903, 2794, 4009, 5710, 8383, 12028, 17131, 25150

If you need more, a[n] = 3 * a[n-3] + 1 will let you generate as many more as you like. I just stopped arbitrarily at the largest one that will fit in a pico-8 fixed-point number.
If you like floats and formulas more than integers and tables, then I have a single neat little magic number for you: powers of e^(1/e), or ~1.44466786101, will exhibit the same behavior but even more cleanly, if used as damage and health tiers. Note, however, that the near-miss math that makes this trick work (with either integers or exponents) may be infuriating to players if you display healthbars: with the exponent version, a 1-damage weapon will take four hits to defeat an enemy with 3.015 health (and this proportion holds for every 4-hits enemy, when you're dealing e.g. 16-damage hits to an enemy with 49 health). And yeah, if you're not showing healthbars then you can fudge damage however you like anyway (looking at you Silksong), but it's the principle of the thing. Besides, if you are using genuine numbers, you can honestly tell the player that their sword damage went up from 11 to 16 with the upgrade they got and I think that's pretty neat.