Houserules in my D&D Games, Part 2

Last updated:

A few years back I laid out some houserules I was planning to use for an upcoming D&D campaign. That campaign finished up, and I'm thinking about running another, so I've spent some time thinking over what I liked and disliked with what I did, and what I'd tweak further.

I'll start with my current plans, then go over my evaluations and reasoning later.

Carried Over From Last Time

First, Flexibility, Unearthed Arcana, and The Wagon are still in play from my previous post.

Justification

The first two are just good sense. We didn't actually use The Wagon much, but that's fine, it just means mundane items didn't end up mattering much, and we didn't need to worry about them. Overall low impact and good for players.

New Rest System

  • Short rests take 10 minutes, and are otherwise unchanged.
  • "Field rests" are an 8-hour rest (generally sleeping) when you're in a dangerous/stressful place (aka out on an adventure). They're identical to short rests, except you also regain HP equal to your level (without having to spend hit dice), and let you regain one spell slot or one use of a long-rest ability.
  • Long rests are as normal, but can only be taken in a safe place where you can properly relax, like a safe inn in a town where you don't have any reason to fear attack. "Magic camping" like Leomund's Tiny Hut or Rope Trick don't count; they give field rests.

Justification

Still trying to find the right balance of how people actually play (1-2 encounters a day, usually) versus how the rules assume people play (5-8 encounters a day), which is just nonsense; real-world play effectively gives long-rest classes (aka spellcasters) a massive buff since they can unload all their resources in most battles.

Making short rests shorter just makes them easier to squeeze in, narratively, without significantly impacting how often they can be used. It's actually totally fine for people to short rest between every battle, honestly, if they can swing it, and overusing a "shorter" short rest is better than not using short rests at all because pausing the action for an hour is hard to justify.

Previously my long rests were a downtime activity, aka several days of rest between adventures. This just proved narratively binding, bc I couldn't give my players a long rest where an adventure did expect them to receive one. Limiting to "safety and relaxation" gives a lot more flexibility while still avoiding the major problem of "fully rested between every encounter on the road" that I was trying to fix.

Field rest is me simplifying my previous rest and HP rules. First, using the Wizard's spell-recovery mechanic just turned out to be a little too complex for people who weren't actively playing wizards; switching to a short and sweet "regain one slot" should hopefully fix that up. It then chains nicely with "or one long-rest ability use". The power level of this regained thing can vary wildly, but I'm okay with that. Really I just want to give players some ability to recover from a harsh fight that drained more resources than expected, besides abandoning the dungeon or whatever. Hopefully this hits well.

Group Initiative + Init Rewards

When combat starts, all players roll initiative against the enemy's leader (or any appropriate enemy). If at least half win the opposed roll, the party gets first turn; otherwise enemies do. All party members (or all enemies) can act in any order during their turn.

Additionally, any player who wins their initiative roll gets to apply advantage or disadvantage to all rolls during their first round, whether the party won or lost the group roll.

Justification

Fast/slow initiative was okay, but ended up with a few problems. Always letting players go first had a larger impact than I'd hoped, and took a lot of tension out of the start of combat - you were always guaranteed you could get set up on that first turn (barring a legit Surprise). Also, the decision of fast vs slow caused a little more analysis-paralysis than I anticipated. Overall, it was just slightly too much downside for not quite enough benefit.

Group initiative is still a hit, tho. The flexibility is great in setting up combos, and it's just plain easier than the slog of calling out initiatives and figuring out order.

Also, fast/slow made the Init stat completely irrelevant, but there's just enough stuff that interacts with it in the rules that this was a little annoying. Bringing it back in approximately its normal role just means less edits.

But also, Init in RAW is pretty much irrelevant. All a high Init does is slightly predispose you to get your first action before an enemy's first action, but after that first round it has zero effect, and the variability of the d20 combined with how often you roll it (much less than attacks/saves) means in practice it had no real noticeable effect.

Changing it to a group roll with a pass/fail condition rather than individually graded should help a lot with this. You don't feel bad when the -1 Init heavy-armor fighter rolls higher than you; everyone who succeeds gets the same benefit, and helps the entire group. And when your +4 bonus does roll high (as it'ls do 25% more often than the -1), you'll get a noticeable benefit out of it rather than just a trivial scheduling benefit.

Attacks and Cantrips Auto-Hit, + Damage Crits

The biggest change so far: if you take the Attack action, or cast a cantrip, you hit automatically. (Or if a cantrip uses a saving throw, the enemy fails automatically.)

Additional effects on the attack (like a saving throw from a Battlemaster technique) are still rolled as normal. Non-Attack actions and levelled spells also roll as normal.

Instead of crits from rolling 20s, damage dice from an auto-hit explode: any die that rolls max gets rolled again, adding to the result, and this can happen multiple times. This applies to all dice from the attack, including things like Smite or Sneak Attack.

When something additional happens on a crit (orc ability, sword of sharpness, etc), it now triggers if the weapon's damage dice sum to 15 or more (after exploding). This does not include stat bonuses, additional dice, etc., just the core weapon's die. (This is subject to change.)

The Champion's ability to crit on 19-20 becomes that your dice now explode on one additional number - d4 crits on 3 or 4, d12 on 11 or 12, etc. (This is definitely subject to change, I don't think I'm happy with its results.)

Justification

A regular complaint from players, especially melee-ers, is that it feels very bad to have a "wasted" turn when your attacks all whiff - especially before 5th level when you've only got one attack, but it continues to happen a good amount of the time at higher levels too. Spellcasters are often hitting multiple people, and dealing half damage even when they fail, so they always feel like they're "contributing", but too often a melee class can feel like they did nothing in a round due to bad rolls.

So hey, let's fix that. The bread and butter of melee classes Just Works now. Not only can you always contribute at least some damage, but your crits feel a bit better too (always sucks to roll a crit and then, like, roll a 1 and a 2 on the damage dice). This also effectively buffs these classes' damage output by 50%-100% relative to spellcasters, which is just as much a good thing here as it is in the Rest rules.

(Cantrips fall into the same "feel bad" camp - when you're spending your turn being conservative with a cantrip rather than using a spell slot, it feels just as bad when you miss that and feel like you did nothing for the round.)

I'll compensate for the buffs on the DM side by putting a little extra HP on monsters, which is extremely easy for me to do on the fly.

The only real problem for me is that exploding-dice massively favor 2d4/2d6 weapons over their 1d8/1d12 equivalents. The math already favors them slightly in the normal rules (2d6 averages 7 damage, while 1d12 averages 6.5), but the gulf widens even more here. I'll see how I feel about this in practice; I know most players don't spend time writing a dice-simulation library to run numbers on these sorts of things, so probably it'll just not matter to them. ^_^

Best Potionomics Deck I've Found

Last updated:

Potionomics is a really fun game I've been following for a while, that just recently finally released. You play a potion-maker, crafting and selling potions, sending heroes out on adventures (buffed by your potions) to gather rare potion ingredients, and befriend and romance a bunch of adorable NPCs. I love it! The writing is so good!

Anyway, a big part of the mechanics is selling your potions, or promoting them to a judge during the potion competitions. This is done with a very fun little card-battling subsystem, where you deal "interest" to the customer to raise your potion's price, avoid using up all of their "patience", and they deal "stress" back to you in return.

Your starting deck sucks, and you get better cards from befriending NPCs. After some experimentation and reading, I think I have hit on the optimal deck, tho. It's based on Chorus, card draw, and cost reduction, and can almost always fully max a customer's interest by turn 2 or 3 at the latest, while keeping your stress minimal.

Basic Deck

You can swap some cards for better ones later, but these are required to get the deck working. It can be fully built on Day 21 at the earliest, just after you recruit Corsac and have his first card.

  • Xid Rank 6, for Jingle, Chorus, Improv, and Rhythm
  • Saffron Rank 5, for Meditate and Deep Connection
  • Quinn Rank 5, for Press The Attack and Pressure
  • Baptiste Rank 1, for Captivate
  • Corsac Rank 1, for Ferocity of the Squirrel
  • Owl Rank 2, for Scheme and Two Is Better Than One

The decklist is:

  • Jingle x2
  • Chorus x4
  • Improv x2
  • Rhythm x2
  • Meditate x2
  • Deep Connection x1
  • Press The Attack x1
  • Pressure x1
  • Captivate x1
  • Ferocity of the Squirrel x1
  • Scheme x2
  • Two Is Better Than One x1

Deck Strategy

The powerhouse of this Deck is Chorus - you'll play it 5-6 times per negotiation. All your card draw is to enable you to draw more Choruses. Improv and Scheme are your major card draw; Quinn's draw cards are last-ditch but can occasionally save your negotiation.

Ferocity is very important for the additional interest - you'll play 10-15 cards per negotiation, so this is worth 20-30 extra interest for free, as much as an additional Chorus! And remember that the effect lasts the entire haggle session, carrying over to later customers, so you don't need to cast it again (unless you have patience to spare or it's free, to get the 2 interest from playing a card).

Two Is Better Than One is used to clone Improv and Chorus two or three times each. Remember that the cloned cards also last the entire haggle session, so some early plays will improve your deck for all later customers. If you've filled up on Improv/Chorus or just don't have them in your hand, feel free to clone Jingle or Captivate, or Meditate if your stress is getting worrisome. Don't clone too much, tho - you don't want your deck hitting more than 27 or 28 cards total or it'll choke up your draws.

Jingle is a surprise key card - not only will it reduce Scheme or Rhythm to 0 cost, but it can actually reduce 1 or 0-cost cards to negative cost, making them add patience to the customer! (You'll see this in the UI as something like "--2", meaning it's adding 2 patience.) In other words, it's equivalent to a non-opener Captivate, or if Rhythm is active, to an opener Captivate!

Rhythm is important for kicking the deck into long-combo mode on turn 2 or 3, as it makes most of your deck free. If you have a Rhythm in your hand, it can be useful to play it and any Choruses you're holding, then just end your turn immediately - your next turn will be far more productive instead. Just make sure you'll have 2 or 3 patience at the start of your next turn, so you can play a Scheme.

Captivate's use is obvious, and Deep Connection is similar. You don't always need to cast Deep Connection, but burning thru a hand for the free Ferocity interest and making it all back from Deep Connection can be very useful.

Finally, managing stress is very important - you need all the cards you draw to keep this deck going, so having them replaced by Stress Cards can kill a negotiation. (Plus they prevent you from using Deep Connection!) Play Meditate whenever you can, and prefer ending a negotiation early if the customer is threatening to send you past 10% stress. That 10-20% stress interval is the Danger Zone, as even mildly unlucky RNG can suddenly put three or more stress cards in your hand and bump your stress even higher.

Perfected Deck

Making the deck perfect just requires spending more time with Saffron (you were gonna do that anyway for her coupons).

At rank 7, you earn Regulated Breathing, which should immediately replace Meditate. Like Ferocity, this buff lasts the entire haggle session; unlike Ferocity, it stacks every time you play it, so by the third or fourth customer you can bump the effect enough to be reducing your stress by 6+ each turn, keeping you at 0% no matter what the opponent does and ensuring you never draw a stress card unless the opponent specifically forces it.

At rank 9, you earn Serenity of Mind, which should immediately replace Scheme. You're always keeping your stress below 20% - at this point, it's almost always at 0% - so this is a strict upgrade, and a massive one at that.

Alternative Deck

You can get a reasonably-functioning version of this deck going during the second week, if you're quick to raise Xid and Saffron, and spend some time with Muktuk as well.

  • Use Build Rapport (Baptiste rank 2) instead of Two Is Better Than One - Sympathy can offset the lack of additional Choruses.
  • Use Bravado (Muktuk rank 5) instead of Ferocity - it's more powerful (3 interest per card) but only lasts two turns.
  • Swap one or both of your Meditates for Guided Thought - less stress reduction, but keeps the interest pressure going.
  • Use a second Captivate instead of Deep Connection, if you haven't gotten Saffron to rank 5 yet for the card.

Unfortunately Xid's Rhythm (rank 6) is pretty core to making the deck pop off, so boosting her relationship as quickly as possible with gifts and hang-outs should be a top priority.

Roll.js, an Exact Dice-Simulation Library

Last updated:

Are you the sort of person who likes to play around with RPG or boardgame mechanics? Have you ever looked at some dice-based mechanic and wondered just what the outcome chances really were, but instead just gave up, rolled a couple of times, and relied on vibes? Have you tried using AnyDice.com but gave up when you saw you'd have to learn a funky DSL to do anything non-trivial? Do you know JavaScript?

If you answered yes to some of those questions, I've got a new library for you! Roll.js is an exact-results dice-simulation library - it doesn't do simulations of the "repeat 10k times and report the average", it tracks every outcome with its precise chance of occurring (up to floating-point accuracy, at least).

The README explains the library itself, and there's a playground tool that'll let you write code against it immediately and even share the results with others! There are several bits of example code in the README, and several more in the playground (click the ? in the upper-right).

For example, a simple "d20 with advantage" roll is:

Roll.d20.advantage()

(playground link)

Want to know average damage of a greatsword after Great Weapon Master is applied (re-roll 1s and 2s, a single time)?

Roll.nd(2, 6).replace(x=> x <= 2, Roll.d6).sum();

(playground link)

Wanna build a d5 out of a d6 by rerolling 6s until they stop coming up, as long as it takes?

// "reroll()" calls map on its returned Roll 
// results again, until things stabilize.
Roll.d6.reroll({
  map: x=> (x == 6) ? Roll.d6 : x
});

(playground link)

Wanna do something complicated, like figure out what the chances are of dying/stabilizing/reviving from a D&D death save?

Roll.d20.reroll({
	summarize(roll, oldSummary={}) {
		return {
			successes:(roll>=10?1:0) + (oldSummary.successes || 0),
			failures:(roll<10?1:0) + (roll==1?1:0) + (oldSummary.failures || 0),
			nat20: (roll==20),
			toString() { return `${this.successes}/${this.failures}/${this.nat20}`; },
		}
	},
	map(summary) {
		if(summary.nat20) return "revive";
		if(summary.successes >= 3) return "stabilize";
		if(summary.failures >= 3) return "die";
		return Roll.d20;
	}
})

(playground link)


Point is, this library can do a lot of stuff, pretty easily, and you can use the JS you already know to do arbitrarily complicated stuff with it. I originally wrote it because I was fed up rewriting simulation code every time my brother and I were thinking about D&D homebrew, and in particular when I wrote some code to test out an alternate death-save mechanic it was getting too complicated; I figured I could just do it right, once, and (after several days of swearing at infinite-looping reroll code) I was correct!

At the moment the convenience functions (like .advantage()) are pretty biased towards D&D 5e usage, but I'm happy to add more for other dice systems. If you have any you'd like to see, let me know in a comment!

How To: Correctly Test for Python's Version

Last updated:

Python has four obvious built-in ways to retrieve its current version. If your software has a minimum version requirement, you'll use one of these methods to test whether the Python currently running your code is a high-enough version to do so successfully.

Three of these ways are worthless, actively-harmful traps. If you read nothing else, read this: use sys.version_info.

The four ways are:

  • sys.version_info is the good way, returning a tuple of numbers, one for each component of the version
  • sys.version is a terrible way, returning a string containing the full version identifier
  • platform.python_version() is another terrible way, also returning a string
  • platform.python_version_tuple() is a final and especially terrible way, returning A TUPLE OF GODDAM STRINGS FOR THE COMPONENTS

Deets

sys.version_info returns a tuple of numeric components. Well, it returns a namedtuple, but same deal. If I convert it to a plain tuple, on my system I get:

(3, 7, 12, 'final', 0)

Tuples in Python have nice sorting behavior; they'll compare against other tuples element-wise, with missing items considered as coming before present items. This means that if I write sys.version_info >= (3, 7), it'll be true - the 3 and 7 match, then the remaining components in sys.version mean it's slightly greater than the (3, 7) literal.

Importantly, because this uses element-wise comparison and the elements are (mostly) numbers, it won't fail for stupid reasons, like version 3.10 coming out. If you compare (3, 10) >= (3, 7) it correctly returns True, because 10 is greater than 7.


platform.python_version_tuple() also returns a tuple of components, but each component is a string. Currently on my system it returns ('3', '7', '12').

If you compare it with a literal tuple, like platform.python_version_tuple() >= ('3', '7') (note that you have to write the literal with strings, or else it'll error), it'll appear to work - you'll get back True. If the function returns ('3', '6') or ('3', '8') it also appears to do the right thing, returning False and True respectively. Bump it to ('3', '9') and it's still True, as expected. But advance to ('3', '10') and it suddenly returns False, indicating that version 3.10 is less than version 3.7.

This is because string-wise comparison is not the same as numerical comparison. Strings compare letter-by-letter, and "1" (the first letter of "10") is indeed less than "7", and so fails the >= check.

This is a very easy mistake for end-users to make. It's an unforgiveable mistake for Python, the language, to make. Returning a tuple, indicating it's been parsed, but filling that tuple with strings when they know the components are numeric and will be used as if they were numbers, is just absolute clown-shoes behavior.


sys.version and platform.python_version() just straight up return strings. Once again, naive comparison seems to work: on my system sys.version returns '3.7.12 (default, Nov 11 2021, 11:50:43) \n[GCC 10.3.0]', and if you run sys.version >= '3.7', it returns True as expected.

Both of these fail when the version is 3.10, in the exact same way as platform.python_version_tuple().

Conclusion

Always, always, ALWAYS use sys.version_info to get the current Python version, for any purpose. Anything else will break your code for stupid reasons.

Imposing Names On The Earth

Last updated:

I recently used What3Words to give a friend my house's location, since our entire neighborhood is only distinguished by the second address line, and Google Maps doesn't actually know any of them. W3W can redirect to Maps with a lat/long which works correctly for directions.

Afterwards, I spent part of the weekend thinking, as one does, about the tradeoffs of the two major "make every part of Earth addressable" efforts: What3Words and Google's PlusCodes.

The two made some very interesting tradeoff decisions differently, and I'm not sure which I like better!

What3Words

W3W divides the world up into ~3m squares, and assigns each a 3-word name. For example, I am at this very moment sitting at my office desk, located in the square named ///jukebox.slang.leaned. You can pop that triplet at the end of "what3words.com/" and find my precise location.

The assignment algorithm is proprietary (that's how they make money), but it's designed to (a) make more populated areas use shorter words, and (b) make nearby areas have no relationship to each other.

That is, there is absolutely no relationship between the name of one square and the name of the next square over; they're effectively random.

The intentional benefit of this is that if you get the name slightly wrong, you'll get the address WAY wrong, and will hopefully notice that the directions to your friends house shouldn't be directing you to another country. No "near misses" just putting you on a nearby street.

Obvious downside is the same, tho - errors will get the address way wrong, rather than nearly correct. It also means you can't generalize to larger areas; there's no concept of resolution larger (or smaller) than the 3m base square.

So you can't give an approximate location, only a fairly precise one. That said, on the scale that most ordinary people want, that's fine; we already have meaningful spatial demarcations that serve that purpose well (town/city names, plus maybe directional words or neighborhoods).

While the first version of W3W specifically used English words, they've since produced a large number of localized versions, so people can share addresses in their own language. They all use the same grid, so you can provide your location in multiple languages if you're expecting to serve a multilingual community.

PlusCodes

They're literally just a more usable way to express lat/long coordinates. Rather than base-10 (useful for math) they use base-20 (useful for memorization/communication), and interweave the digits so you get more precise in both dimensions as you go along.

That is, the first and second chars of a plus code are the high-order lat and long, naming a 20 degree square slice of the Earth. The third and fourth narrow that down to a 1 degree square (roughly 60 miles). Fifth and six cut it to a .05 degree square (roughly 3 miles). Etc. This goes on for 10 digits at which point they switch to a different scheme that goes digit-by-digit, but the details don't super matter.

Between the 8th and 9th digit you use a + to break it up visually, like 6PH57VP3+PR. You can also drop the first four digits if you have a city name or similar, like "7VP3+PR6 Singapore", giving you an easier-to-remember 7-digit name for an approximately 3m square area.

Benefits here are

  • easy to compute, obviously, since it's just lat/long expressed in base 20 and interwoven. The encoding/decoding algos can be done from scratch in a hundred lines or less of code. No need to contract out decoding to a 3rd-party company.
  • the precision can be scaled up or down trivially by adding or removing digits. (If you remove digits before the +, you replace them with 0, which isn't otherwise a valid digit in a pluscode.) So you can indicate large areas (so long as they're reasonably approximated by the slice boundaries), or even much smaller, more precise areas.
  • if you forget, or just leave off, the last bit of the code, you'll still end up close by. Like, a neighborhood can generally be located without any digits past the +.

Big downside is that getting anything but the very end wrong can make the address quite wrong without it necessarily looking wrong. In other words, errors in the pluscode translate to vastly different scales of errors in the final location, depending on where the error occurs. The most obvious errors (pointing to an entirely wrong part of the world) generally aren't even possible since those digits are usually omitted in favor of specifying a city instead, so your errors will often still give a location that looks reasonable at first glance, but might be next door, next neighborhood over, or next city over.

Another downside is the code spends the same amount of entropy on each location no matter how populated or middle-of-the-ocean it is. The one nod to this is being able to shorten the standard code from 11 digits to 7+city name (effectively giving human-friendly names to the 4-digit-prefix blocks), but past that there's no distinction between "populated city" and "wilderness", which W3W does distinguish between.

Both Are Cool

So there's several tradeoffs on either, several of which place the two on directly opposed sides. I dunno which I like best! Maybe both are great! I think that maybe they're best for different use-cases.

I think W3W does end up slightly better for human-relevant locations, such as in cities. The "any error is obviously way wrong" behavior seems like a benefit to me over the "error ranges from very slightly to very extremely wrong, depending on where it occurs" behavior of PlusCodes. And I like that populated areas are favored with shorter, more common, easier-to-spell words. (Compare my location I stated above, ///jukebox.slang.leaned, with an ocean square just a mile or so off the coast near me, ///virtuously.daylight.facsimiles.)

Also, when you are naming a larger area, like your house (rather than a very specific area like your front door) you have a goodly number of choices to pick from, and can choose one that's particularly memorable or appropriate. For example, I have about 20 squares covering my house, and chose a nice tech-related one (which I won't share, since my address isn't easy-accessible public knowledge and I'd prefer to keep it that way if I can) that happens to land roughly in the middle of my living room.

On the other hand, scientific use-cases seems to better served by PlusCodes. The variable slice sizes you can name means you can address many different use-cases, like naming ranges or geographic areas, as well as very precise locations, and can get more precise than the 3m square too, just by appending more digits. The names also all look generically similar, rather than having the potential for humor, which can be both good and bad in professional settings. (The 20-character set used by PlusCodes was chosen both to reduce the chance of mistyping/mishearing them, and to minimize the chance of addresses being real words, rude or otherwise. For example, it's missing all the vowels.)

Anyway I just wanted to get some of these thoughts out, and to interest more people in either/both of these systems if they weren't already aware. If you read to this point, success!