I enjoy a game called “Commands and Colors: Ancients”, which I’ve blogged about here before. It’s a simulation of tactical ancient warfare that uses special dice to resolve battles. In any given battle you can consider each six-sided die to have faces labeled with the symbols Miss, Miss, Hit, Sword, Helmet, Flag. To improve my play, I decided to generate and study a table of the odds of getting a specified number of hits when a specified number of dice is rolled. I set out to write a Python program to do this.
There are special circumstances under which flags and helmets convert to hits, so the program actually needs to print out several tables and is not entirely trivial. Still, it is computing on a mathematically simple model with strictly bounded computational cost – except for war elephants. These units have the special ability that when they roll a sword, the sword is counted as a hit and then rerolled. (This may make more sense if you think of a sword roll as representing impact damage.) You keep rolling and marking hits as long as the die keeps coming up swords.
To calculate the non-elephant probabilities I enumerated eight entire state spaces for each of 1 to 8 dice (the most you can ever roll under the rules – takes Alexander the Great or Julius Caesar leading a Heavy unit with the the Clash of Shields +2 bonus active), then counted up instances of each distinct outcome (so many misses, hits, swords, flags, banners) to assign a probability mass to each.
(Statisticians often think in terms of probability mass or probability weight, which has to be conserved as a distribution changes. It’s analogous to thinking of electricity as a fluid.)
I then had to write code to mutate a copy of each distribution according to the elephants’ sword-reroll rule. To do this, each outcome containing a sword hit needs its probability mass divided by six and reallocated to itself and five other outcomes with one fewer sword apiece; you stop reallocating when the probability mass on a sword-containing outcome drops below a very low noise level.
That particular piece of code gave me more trouble than the rest of the program put together. At one point I grumbled to a friend who had been following the project “I’m having persistent bugs in my probability mass reallocator.”
He looked right back at me and said, with a perfectly straight face, “Have you considered reversing the polarity of the neutron flow?”