Skip to content

Instantly share code, notes, and snippets.

@stickystyle
Last active December 1, 2021 17:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stickystyle/572b3d1ed35714cac8636ff79885acf1 to your computer and use it in GitHub Desktop.
Save stickystyle/572b3d1ed35714cac8636ff79885acf1 to your computer and use it in GitHub Desktop.
import sys
from random import randint
"""
*Snail Racing Rules*
Every snail needs a jockey, and any saddles not filled by characters are filled by other attendees
(use the commoner stat block to represent these NPCs). All the snails and their riders begin at the start line;
fireworks signal the start of the race, after which the riders race their snails to the finish line.
On a whiteboard or notepad, jot down the giant snails, their numbers, and their riders. Next to the names,
keep a running total of how far each snail travels in feet during the race. The course is 480 feet long.
* Racing Rules. *
The race is divided into 6-second rounds. Once the race begins, the snails move along the track at remarkable speed,
each one covering 80 feet per round by itself. Without riders, the snails are an even match, but a jockey can try to
improve a snail’s performance by patting its shell or speaking words of encouragement at it.
Initiative rolls are unnecessary. Once per round, each jockey can make a DC 12 Wisdom (Animal Handling) check.
On a successful check, the jockey’s snail moves an extra 10 feet that round, or 20 feet if the check succeeds by 5
or more. On a failed check, the jockey’s snail moves 10 feet slower that round, or 20 feet slower if the check fails
by 5 or more. The race ends when one or more snails travel the 480 feet needed to cross the finish line.
If two or more snails cross the finish line in the same round, determine the winner of the race by having those snails’
jockeys each roll a d20. The highest roll determines the winner, with one snail narrowly claiming victory. If the roll
for first place is tied, the race ends in a tie, and each winning jockey receives the first-place prize
(see “Prizes” below).
Signs posted in the stands and along the racetrack warn that neither jockeys nor spectators may influence the outcome
of a race by using magic or by harming other jockeys or snails. Any jockey who openly does so is disqualified,
as is the jockey’s snail. A spectator caught doing so is escorted from the carnival by 1d4 goblins (lawful good)
who are watching the crowd closely for troublemakers.
* Surprises. *
At the start of each round after the first, roll a d8 and consult the Snail Race Surprises table to
determine if something unexpected happens that round. When a surprise occurs, determine which snail is affected by
rolling a d8.
d8 Surprise
1–3 No surprise.
4 One random snail gets a stitch, reducing its speed by 40 feet this round.
5 A spectator throws a head of lettuce at one random snail. The snail stops to eat the lettuce and doesn’t move at all this round.
6–7 One random snail is bolstered by the cheering crowd and moves an extra 20 feet this round.
8 The saddle on one random snail comes loose and falls off. The snail’s jockey must succeed on a DC 15 Dexterity saving throw to remain atop the snail. On a failed save, the jockey falls off, and the snail continues the race on its own.
"""
FINISH = 480
INCREMENT = 25
SNAILS = {
1: {"name": "Shellymoo", "distance": 0},
2: {"name": "Nimblefoot", "distance": 0},
3: {"name": "High Road", "distance": 0},
4: {"name": "Quickleaf", "distance": 0},
5: {"name": "Flowerflash", "distance": 0},
6: {"name": "Whizzy", "distance": 0},
7: {"name": "Breakneck", "distance": 0},
8: {"name": "Queen's Majesty", "distance": 0},
}
def move_snail(roll):
distance_to_move = 80
if roll >= 17:
distance_to_move += 20
elif 12 <= roll <= 16:
distance_to_move += 10
elif 11 >= roll > 7:
distance_to_move += -10
elif roll <= 7:
distance_to_move += -20
else:
raise Exception(f"Invalid roll: {roll}")
return distance_to_move
def build_bar(distance):
# The ASCII block elements come in chunks of 8, so we work out how
# many fractions of 8 we need.
# https://en.wikipedia.org/wiki/Block_Elements
bar_chunks, remainder = divmod(int(distance * 8 / INCREMENT), 8)
# First draw the full width chunks
bar = "█" * bar_chunks
# Then add the fractional part. The Unicode code points for
# block elements are (8/8), (7/8), (6/8), ... , so we need to
# work backwards.
if remainder > 0:
bar += chr(ord("█") + (8 - remainder))
# If the bar is empty, add a left one-eighth block
bar = bar or "▏"
return bar
def print_snails(snails):
for s in snails:
bar = build_bar(snails[s]["distance"])
print(
f"{snails[s]['name'].rjust(longest_label_length)} ▏ {snails[s]['distance']:#4d} {bar}"
)
if __name__ == "__main__":
players = input("What numbers (1-8) will PC's be playing? (separate by commas) ")
players = [int(x) for x in players.split(",")]
for n, s in SNAILS.items():
if n in players:
name = f"{s['name']} ({n} PC)"
else:
name = f"{s['name']} ({n})"
s["name"] = name
longest_label_length = max(len(s["name"]) for _, s in SNAILS.items())
if max(players) > len(SNAILS):
print(f"Please choose a number between 1 and {len(SNAILS)}")
sys.exit()
play_round = 1
leader = 0
while leader < FINISH:
print("=====================")
print(f"Round {play_round}")
surprise_action = (None, 0)
if play_round > 1:
surprise_check = randint(1, 8)
if surprise_check == 4:
surprise = randint(1, 8)
print(
f"Snail {surprise} gets a stitch, reducing its speed by 40 feet this round."
)
surprise_action = (surprise, -40)
elif surprise_check == 5:
surprise = randint(1, 8)
print(
f"A spectator throws a head of lettuce at snail {surprise}. The snail stops to eat the lettuce and doesn’t move at all this round."
)
surprise_action = (surprise, None)
elif surprise_check >= 6 <= 7:
surprise = randint(1, 8)
print(
f"Snail {surprise} is bolstered by the cheering crowd and moves an extra 20 feet this round."
)
surprise_action = (surprise, 20)
elif surprise_check == 8:
surprise = randint(1, 8)
surprise_action = (surprise, 0)
if surprise in players:
print(
f"Player on snail {surprise} needs to make a DC 15 DEX saving throw to avoid being thrown off the snail"
)
do_they_pass = input("Do they pass? (y/n) ")
if do_they_pass.lower() == "n":
del SNAILS[surprise]
else:
dex_check = randint(1, 20)
if dex_check < 15:
print(
f"Snail {surprise} fails the DC 15 DEX saving throw and is thrown off the snail"
)
del SNAILS[surprise]
finishers = []
for snail in SNAILS:
if snail == surprise_action[0] and surprise_action[1] is None:
print(f"Snail {snail} not moving this round")
continue
if snail in players:
roll_result = input(f"What did player on {snail} roll? ")
else:
roll_result = randint(1, 20)
roll_result = int(roll_result)
assert roll_result in range(1, 21)
distance = move_snail(roll_result)
if snail == surprise_action[0] and surprise_action[1]:
distance += surprise_action[1]
SNAILS[snail]["distance"] += distance
if SNAILS[snail]["distance"] > leader:
leader = SNAILS[snail]["distance"]
if SNAILS[snail]["distance"] >= FINISH:
finishers.append(f"{SNAILS[snail]['name']} crosses the finish!")
if finishers:
print("\n".join(finishers))
play_round += 1
print_snails(SNAILS)
import unittest
class SnailTests(unittest.TestCase):
def test_move_snail(self):
from main import move_snail
self.assertEqual(move_snail(1), 60)
self.assertEqual(move_snail(2), 60)
self.assertEqual(move_snail(3), 60)
self.assertEqual(move_snail(4), 60)
self.assertEqual(move_snail(5), 60)
self.assertEqual(move_snail(6), 60)
self.assertEqual(move_snail(7), 60)
self.assertEqual(move_snail(8), 70)
self.assertEqual(move_snail(9), 70)
self.assertEqual(move_snail(10), 70)
self.assertEqual(move_snail(11), 70)
self.assertEqual(move_snail(12), 90)
self.assertEqual(move_snail(13), 90)
self.assertEqual(move_snail(14), 90)
self.assertEqual(move_snail(15), 90)
self.assertEqual(move_snail(16), 90)
self.assertEqual(move_snail(17), 100)
self.assertEqual(move_snail(18), 100)
self.assertEqual(move_snail(19), 100)
self.assertEqual(move_snail(20), 100)
@stickystyle
Copy link
Author

Example output...

=====================
Round 5
What did player on 1 roll? 20
Shellymoo (1 PC) crosses the finish!
   Shellymoo (1 PC) ▏  500 ████████████████████
     Nimblefoot (2) ▏  350 ██████████████
      High Road (3) ▏  440 █████████████████▌
      Quickleaf (4) ▏  410 ████████████████▍
    Flowerflash (5) ▏  380 ███████████████▏
         Whizzy (6) ▏  370 ██████████████▊
      Breakneck (7) ▏  370 ██████████████▊
Queen's Majesty (8) ▏  380 ███████████████▏

@stickystyle
Copy link
Author

The snail racing looked like a lot of fun, but all the work involved in rolling, checking, and writing down for 8 snails is going to make this feel less like a a race and more like accountant work. So to keep things going at a fast pace I wrote this little script this morning to handle all the math and keep things going quick so that it feels more like a race.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment