A character/one-shot generator for KOBOLDS IN SPACE!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

koboldgen.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import random as r
  2. import argparse
  3. beg = ["a","e","i","o","u","ba","be","bi","bo","bu","by","y","da","de","di","do","du","dy","fa","fi","fo","fe","fu","ga","ge","gi","go","gu","ka","ke","ki","ko","ku","ky","ma","me","mi","mo","mu","na","ne","ni","no","nu","pa","pe","pi","po","pu","ra","re","ri","ro","ru","ry","sa","se","si","so","su","ta","te","ti","to","tu","ty","wa","we","wi","wo","wy","za","ze","zi","zo","zu","zy"]
  4. mid = beg + ["l","x","n","r"]
  5. def gen_name(length=None, minimum=3, maximum=9):
  6. if length == None:
  7. lgt = r.randint(minimum,maximum)
  8. else:
  9. lgt = length
  10. morae = []
  11. while len("".join(morae)) < lgt:
  12. if len(morae) == 0:
  13. mora = r.choice(beg)
  14. morae.append(mora[0].upper() + mora[1:])
  15. else:
  16. mora = r.choice(mid)
  17. if morae[-1] == mora:
  18. mora = r.choice(mid)
  19. morae.append(mora)
  20. return "".join(morae)
  21. class Plot:
  22. loc1 = ["friendly","hostile","derelict","airless","poison-filled/covered","overgrown","looted","burning","frozen","haunted","infested"]
  23. loc2 = ["asteroid","moon","space station","spaceship","ringworld","Dyson sphere","planet","Space Whale","pocket of folded space","time vortex","Reroll"]
  24. miss = ["to explore","to loot everything not bolted down too securely","to find the last group of kobolds who came here","to find a rumored secret weapon","to find a way to break someone else's secret weapon","to claim this place in the name of the Kobold Empire","to make friends","to rediscover lost technology","to find lost magical items","to find and defeat a powerful enemy"]
  25. prob = [
  26. {
  27. "id": 0,
  28. "name": "an Undead Sample Pack (swarm of zombies and skeletons)",
  29. "shortname": "undead",
  30. "stats": [0,5,2,6],
  31. },
  32. {
  33. "id": 1,
  34. "name": "a rival band of kobolds",
  35. "shortname": "kobold rivals",
  36. "stats": [3,3,4,4],
  37. },
  38. {
  39. "id": 2,
  40. "name": "a detachment from the Elf Armada",
  41. "shortname": "elves",
  42. "stats": [4,3,5,4],
  43. },
  44. {
  45. "id": 3,
  46. "name": "refugees with parasites. Big parasites",
  47. "shortname": "refugees",
  48. "stats": [2,4,0,0],
  49. },
  50. {
  51. "id": 4,
  52. "name": "an artificial intelligence bent on multi-world domination",
  53. "shortname": "AI",
  54. "stats": [4,1,6,3],
  55. },
  56. {
  57. "id": 5,
  58. "name": "robot spiders",
  59. "shortname": "spiders",
  60. "stats": [3,3,2,4],
  61. },
  62. {
  63. "id": 6,
  64. "name": "semi-intelligent metal eating slime",
  65. "shortname": "slime",
  66. "stats": [0,2,1,5],
  67. },
  68. {
  69. "id": 7,
  70. "name": "a living asteroid that intends to follow the kobolds home like the largest puppy",
  71. "shortname": "asteroid",
  72. "stats": [2,3,1,6],
  73. },
  74. {
  75. "id": 8,
  76. "name": "an old lich that wants everyone to stay off of their 'lawn'",
  77. "shortname": "lich",
  78. "stats": [5,2,6,3],
  79. },
  80. {
  81. "id": 9,
  82. "name": "elder gods hailing from the dark spaces between the stars",
  83. "shortname": "gods",
  84. "stats": [0,6,6,6],
  85. },
  86. {
  87. "id": 10,
  88. "name": "a Flying Brain Monster",
  89. "shortname": "Flying Brain Monster",
  90. "stats": [2,3,6,1],
  91. },
  92. ]
  93. def __init__(self):
  94. self.loc_desc = Plot.loc1[r.randint(0, len(Plot.loc1)-1)]
  95. self.locIndex = r.randint(0, len(Plot.loc2)-1)
  96. if self.locIndex == len(Plot.loc2) - 1:
  97. self.location = "battlefield on "
  98. self.locIndex = r.randint(0, len(Plot.loc2)-2)
  99. self.mainLocation = Plot.loc2[self.locIndex]
  100. if self.mainLocation[0].lower() in ["a","e","i","o","u"]:
  101. self.location += "an "
  102. else:
  103. self.location += "a "
  104. self.location += self.mainLocation
  105. else:
  106. self.location = Plot.loc2[self.locIndex]
  107. self.missIndex = r.randint(0, len(Plot.miss)-1)
  108. self.oops = r.randint(1,12)
  109. self.mission = ""
  110. if self.oops == 12:
  111. self.mission += "...well, they weren't paying attention, so don't tell them, but they're supposed "
  112. self.mission += Plot.miss[r.randint(0, len(Plot.miss)-1)] + "!"
  113. self.probIndex = r.randint(0, len(Plot.prob)-1)
  114. self.problem = Plot.prob[self.probIndex]
  115. self.secProblem = None
  116. self.thirdProblem = None
  117. if self.problem["id"] in [1,2]:
  118. self.problem["name"] += " led by " + gen_name()
  119. if self.problem["id"] in [4,7,8,10]:
  120. self.problem["name"] += " named " + gen_name(minimum=5, maximum=12)
  121. if self.problem["id"] == 3:
  122. self.secProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  123. if self.problem["id"] == 10:
  124. self.secProbIndex = r.randint(0, len(Plot.prob)-2)
  125. self.secProblem = Plot.prob[self.secProbIndex]
  126. if self.secProbIndex == 3:
  127. self.thirdProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  128. self.fullProblem = self.problem["name"]
  129. if self.secProblem and self.secProblem["name"] != "Parasites":
  130. self.fullProblem += " and its minion, " + self.secProblem["name"]
  131. class Character:
  132. GADGETS = [ {"name": "Awesome Dagger of Sneak(?) Attacks", "description": "Yell 'Sneak Attack!' to count a mixed-success Body attack as a success.", "reusable": True},
  133. {"name": "Button of Uselessness", "description": f"This large, red button can be stuck onto any flat surface, horizontal, vertical, or otherwise. A short time after it has been placed, any character (friend or foe) nearby must succeed a Brains roll to avoid pressing the button. Pressing the button does nothing, but this action takes the place of anything that might otherwise be done during an Event if the Brains roll is failed. The button can be pressed {r.randint(1,6)} times before it breaks and no longer compels others to press it.", "reusable": False},
  134. {"name": "Encyclopedia of Stuff I Totally Knew", "description": "Add 2 points to the target number of any uncontested Brains roll, or 1 point to the target number of a contested Brains roll.", "reusable": True},
  135. {"name": "Medkit", "description": "Make a Brains/Order roll to restore 2 lost Body points, or 1 on a Mixed Success. If the character has medical training, restored Body points may be doubled.", "reusable": False},
  136. {"name": "Potion of Healing", "description": "Restore 1 lost Body point. Using counts as an Event but no roll is needed unless it’s contested.", "reusable": False},
  137. {"name": "Tinfoil Helm of Shielding", "description": "Count any Brains damage as a mixed success. If you take Body damage, roll 1d6; on a 6, the Helm is useless until you take a nap.", "reusable": True},
  138. {"name": "Handy Toothbrush", "description": "Scrub the Space Gunk off whatever object you're interacting with to turn a mixed Order success to make that object work into a full success.", "reusable": True},
  139. {"name": "Jar of ... Something", "description": "Throw it (Body/Chaos) at another character to make them spend an Event cleaning it off, or throw it at the floor to make a ten-foot circle of difficult terrain.", "reusable": False},
  140. {"name": "Pocket Sand!", "description": "Yell 'Pocket Sand!' to count a successful Body attack against you as a mixed success.", "reusable": False},
  141. {"name": "The Fabulous Grappling Hook", "description": "As an Event, instantly move 50 feet in any direction. (Make sure you have 50 feet available to move in or take 1 Body damage when you arrive short of that.)", "reusable": True},
  142. {"name": "Pocket Accordion", "description": "Make a Brains/Order or Brains/Chaos roll. On a success, any nearby opponent has -1 Brains during their next event. On a failure, everybody nearby, including you, has -1 Brains on their next event.", "reusable": True},
  143. {"name": "Huge Goggles", "description": "If you take Body damage, roll 1d6; on a 6, the lenses of the Goggles break until you take a nap.", "reusable": True},
  144. ]
  145. def __init__(self):
  146. self.name = ""
  147. self.career = ""
  148. self.stats = []
  149. def generate(self):
  150. self.gen_name()
  151. self.gen_stats()
  152. self.gen_career()
  153. self.gen_gadget()
  154. def gen_name(self):
  155. self.name = gen_name()
  156. def gen_stats(self, n=12):
  157. if n < 0:
  158. print("Too few stat points!")
  159. return [0,0,0,0]
  160. stats = [0,0,0,0]
  161. points = n
  162. slots = [0,1,2,3]
  163. for _ in range(points):
  164. tgl = False
  165. while tgl == False:
  166. slt = r.choice(slots)
  167. if slt <= 1:
  168. if stats[slt] == 6: continue
  169. if stats[slt] == 5 and r.randint(0,1) != 1: continue
  170. else:
  171. if stats[slt] == 6: continue
  172. if stats[slt] > 2 and r.randint(0,stats[slt]-2) != 1: continue
  173. stats[slt] += 1
  174. tgl = True
  175. stats[3] = stats[3] + 1
  176. if stats[3] > 6:
  177. stats[3] = 6
  178. stats[2] = stats[2] + 1
  179. if stats[2] > 6:
  180. stats[2] = 6
  181. self.stats = stats
  182. def gen_career(self):
  183. self.career = r.choice(["Soldier/Guard","Pilot","Medic","Mechanic","Politician","Spellcaster","Performer","Historian","Spy","Cook","Cartographer","Inventor","Merchant"])
  184. def gen_gadget(self):
  185. self.gadget = r.choice(Character.GADGETS)
  186. def print_name(self, html=False):
  187. if html:
  188. charText = f"<h4>Name: {self.name} (Kobold {self.career})</h4>"
  189. else:
  190. charText = f"\nName: {self.name} (Kobold {self.career})"
  191. print(charText)
  192. def print(self, html=False):
  193. if html:
  194. out = (
  195. f"<div class='kobold'>\n"
  196. f" <span class='koboldid'>\n"
  197. f" <span class='koboldname'>{self.name}</span><br>\n"
  198. f" <span class='koboldcareer'>Kobold {self.career}</span>\n"
  199. f" </span>\n"
  200. f" <br>\n"
  201. f" <span class='koboldstats'>\n"
  202. f" <ul>\n"
  203. f" <li>Order: {self.stats[0]}</li>\n"
  204. f" <li>Chaos: {self.stats[1]}</li>\n"
  205. f" <li>Brain: {self.stats[2]}</li>\n"
  206. f" <li>Body: {self.stats[3]}</li>\n"
  207. f" </ul>\n"
  208. f" </span>\n"
  209. f" <br>\n"
  210. f" <span class='koboldgadget'>\n"
  211. f" <span class='koboldgadgetname'>{self.gadget['name']}</span><br>\n"
  212. f" <span class='koboldgadgetdescription'>{self.gadget['description']}</span>\n"
  213. f" {'<br><span class=\'koboldgadgetreuse\'>Reusable</span>' if self.gadget['reusable'] else ''}\n"
  214. f" </span>"
  215. f"</div>\n"
  216. )
  217. print(out)
  218. else:
  219. self.print_name()
  220. print(f"Order: {self.stats[0]}")
  221. print(f"Chaos: {self.stats[1]}")
  222. print(f"Brain: {self.stats[2]}")
  223. print(f"Body: {self.stats[3]}")
  224. print(f"Gadget: {self.gadget['name']} ({self.gadget['description']}{'- Reusable' if self.gadget['reusable'] else ''})")
  225. class Ship:
  226. def __init__(self):
  227. self.name1 = r.choice(["Red","Orange","Yellow","Green","Blue","Violet","Dark","Light","Frenzied","Maniacal","Ancient"])
  228. self.name2 = r.choice(["Moon","Comet","Star","Saber","World-Eater","Dancer","Looter","Phlogiston","Fireball","Mecha","Raptor"])
  229. self.gqual = r.choice(["is stealthy & unarmored","is speedy & unarmored","is maneuverable & unarmored","is always repairable","is self-repairing","is flamboyant & speedy","is slow & armored","is flamboyant & armored","is hard to maneuver & armored","has Too Many Weapons!","has a prototype hyperdrive"])
  230. self.bqual = r.choice(["has an annoying AI","has inconveniently crossed circuits","has an unpredictable power source","drifts to the right","is haunted","was recently 'found' so the kobolds are unused to it","is too cold","has a constant odd smell","its interior design... changes","its water pressure shifts between slow drip and power wash","it leaves a visible smoke trail"])
  231. self.fullname = f"{self.name1} {self.name2}"
  232. def print(self, html=False):
  233. if (html):
  234. shipText = f"<p>The <strong>{self.fullname}</strong> <span style='color: blue;'>{self.gqual}</span>, but <span style='color: red;'>{self.bqual}</span>.</p>\n"
  235. else:
  236. shipText = f"The {self.fullname} {self.gqual}, but {self.bqual}.\n"
  237. print(shipText)
  238. class Campaign:
  239. def __init__(self, n=None, makeChars=True):
  240. n = 6 if n == None else n
  241. self.ship = Ship()
  242. self.params = Plot()
  243. if makeChars:
  244. self.characters = []
  245. for _ in range(n):
  246. c = Character()
  247. c.generate()
  248. self.characters.append(c)
  249. self.art = "an" if self.params.loc_desc[0] in ["a","e","i","o","u"] else "a"
  250. def print_params(self, endc=" ", html=False):
  251. st = ["Order:", "Chaos:", "Brains:", "Body:"]
  252. cst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.problem["stats"]]))])
  253. lines = [
  254. f"The Kobolds of the {self.ship.fullname}",
  255. f"have been sent out to {self.art} {self.params.loc_desc} {self.params.location}",
  256. f"in order {self.params.mission}",
  257. f"but they're challenged by {self.params.fullProblem}!",
  258. f"The stats of the {self.params.problem['shortname']}",
  259. f"{cst}"
  260. ]
  261. if self.params.secProblem:
  262. mst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.secProblem["stats"]]))])
  263. lines.append(f"The stats of the {self.params.secProblem['shortname']}")
  264. lines.append(f"{mst}")
  265. if self.params.thirdProblem:
  266. pst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.thirdProblem["stats"]]))])
  267. lines.append(f"The stats of the {self.params.thirdProblem['shortname']}")
  268. lines.append("{pst}")
  269. if html:
  270. out = (
  271. f"<div id='theship' class='firstrow'>\n"
  272. f" <span class='head'>The Ship</span>\n"
  273. f" <span id='shipname'>\n"
  274. f" The {self.ship.fullname}!\n"
  275. f" </span>\n"
  276. f" <br>\n"
  277. f" <span id='shipquality1'>\n"
  278. f" It {self.ship.gqual}...\n"
  279. f" </span><br>\n"
  280. f" <span id='shipquality2'>\n"
  281. f" But {self.ship.bqual}!\n"
  282. f" </span>\n"
  283. f"</div>\n"
  284. f"<div id='themission' class='firstrow'>\n"
  285. f" <span class='head'>The Mission</span>\n"
  286. f" <span id='missionloc'>\n"
  287. f" The kobolds have been sent to {self.art} {self.params.loc_desc} {self.params.location}\n"
  288. f" </span><br>\n"
  289. f" <span id='missiontarget'>\n"
  290. f" in order {self.params.mission}\n"
  291. f" </span>\n"
  292. f"</div>\n<br clear='all'>\n"
  293. f"<div id='theadversary' class='firstrow'>\n"
  294. f" <span class='head'>The Adversary</span>\n"
  295. f" They're challenged by <span id='advname'>{self.params.fullProblem}</span>!\n"
  296. f" <br>\n"
  297. f" <span id='problemstats'>\n"
  298. f" The stats of the {self.params.problem['shortname']}:<br>\n"
  299. f" {cst}\n"
  300. f" </span>\n"
  301. )
  302. if self.params.secProblem:
  303. out += (
  304. f" <br><span id='secprobstats'>\n"
  305. f" The stats of the {self.params.secProblem['shortname']}:<br>\n"
  306. f" {mst}\n"
  307. f" </span>\n"
  308. )
  309. if self.params.thirdProblem:
  310. out += (
  311. f" <br><span id='thirdprobstats'>\n"
  312. f" The stats of the {self.params.thirdProblem['shortname']}:<br>\n"
  313. f" {pst}\n"
  314. f" </span>\n"
  315. )
  316. out += f"</div>\n"
  317. print(out)
  318. print(f"<br clear='all'>\n")
  319. else:
  320. print(f"{lines[0]} {lines[1]} {lines[2]} -- {lines[3]}")
  321. print(f"{lines[4]}: {lines[5]}")
  322. if self.params.secProblem:
  323. print(f"- {lines[6]}: {lines[7]}")
  324. if self.params.thirdProblem:
  325. print(f"- - {lines[8]}: {lines[9]}")
  326. print()
  327. self.ship.print(html=html)
  328. def print_chars(self, html=False):
  329. if html:
  330. print(f"<div id='thekobolds'>\n")
  331. print(f"<span class='head'>The Kobolds</span>\n")
  332. else:
  333. print("The kobolds:")
  334. for k in self.characters:
  335. k.print(html=html)
  336. if html:
  337. print(f"</div>\n<br clear='all'>\n")
  338. if __name__ == "__main__":
  339. parser = argparse.ArgumentParser()
  340. group = parser.add_mutually_exclusive_group()
  341. group.add_argument("-c", "--campaign", help="print a full campaign block with N kobolds (default 6)", nargs="?", const=6, type=int, metavar="N")
  342. group.add_argument("-k", "--kobolds", help="print N kobolds", type=int, nargs="?", const=1, default=1, metavar="N")
  343. group.add_argument("-n", "--names", help="print N kobolds without stat blocks", nargs="?", const=1, type=int, metavar="N")
  344. group.add_argument("-p", "--params", help="print only the parameters of a campaign", action="store_true")
  345. group.add_argument("-s", "--ship", help="print only the ship name and description", action="store_true")
  346. parser.add_argument("--html", help="print in HTML instead of plain text", action="store_true")
  347. args = parser.parse_args()
  348. # print(args)
  349. html = True if args.html else False
  350. if args.campaign:
  351. cmp = Campaign(args.campaign)
  352. cmp.print_params(html=html)
  353. cmp.print_chars(html=html)
  354. elif args.params:
  355. cmp = Campaign(makeChars=False)
  356. cmp.print_params(html=html)
  357. elif args.ship:
  358. ship = Ship()
  359. ship.print(html=html)
  360. elif args.names:
  361. for _ in range(args.names):
  362. c = Character()
  363. c.generate()
  364. c.print_name(html=html)
  365. else:
  366. for _ in range(args.kobolds):
  367. c = Character()
  368. c.generate()
  369. c.print(html=html)