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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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=None, maximum=None):
  6. if length == None:
  7. minimum = 3 if minimum == None else minimum
  8. maximum = 9 if maximum == None else maximum
  9. lgt = r.randint(maximum,maximum)
  10. else:
  11. lgt = length
  12. morae = []
  13. while len("".join(morae)) < lgt:
  14. if len(morae) == 0:
  15. mora = r.choice(beg)
  16. morae.append(mora[0].upper() + mora[1:])
  17. else:
  18. mora = r.choice(mid)
  19. if morae[-1] == mora:
  20. mora = r.choice(mid)
  21. morae.append(mora)
  22. return "".join(morae)
  23. class Plot:
  24. loc1 = ["friendly","hostile","derelict","airless","poison-filled/covered","overgrown","looted","burning","frozen","haunted","infested"]
  25. loc2 = ["asteroid","moon","space station","spaceship","ringworld","Dyson sphere","planet","Space Whale","pocket of folded space","time vortex","Reroll"]
  26. 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"]
  27. prob = [
  28. {
  29. "id": 0,
  30. "name": "an Undead Sample Pack (swarm of zombies and skeletons)",
  31. "shortname": "undead",
  32. "stats": [0,5,2,6],
  33. },
  34. {
  35. "id": 1,
  36. "name": "a rival band of kobolds",
  37. "shortname": "kobolds",
  38. "stats": [3,3,4,4],
  39. },
  40. {
  41. "id": 2,
  42. "name": "a detachment from the Elf Armada",
  43. "shortname": "elves",
  44. "stats": [4,3,5,4],
  45. },
  46. {
  47. "id": 3,
  48. "name": "refugees with parasites. Big parasites",
  49. "shortname": "refugees",
  50. "stats": [2,4,0,0],
  51. },
  52. {
  53. "id": 4,
  54. "name": "an artificial intelligence bent on multi-world domination",
  55. "shortname": "AI",
  56. "stats": [4,1,6,3],
  57. },
  58. {
  59. "id": 5,
  60. "name": "robot spiders",
  61. "shortname": "spiders",
  62. "stats": [3,3,2,4],
  63. },
  64. {
  65. "id": 6,
  66. "name": "semi-intelligent metal eating slime",
  67. "shortname": "slime",
  68. "stats": [0,2,1,5],
  69. },
  70. {
  71. "id": 7,
  72. "name": "a living asteroid that intends to follow the kobolds home like the largest puppy",
  73. "shortname": "asteroid",
  74. "stats": [2,3,1,6],
  75. },
  76. {
  77. "id": 8,
  78. "name": "an old lich that wants everyone to stay off of their 'lawn'",
  79. "shortname": "lich",
  80. "stats": [5,2,6,3],
  81. },
  82. {
  83. "id": 9,
  84. "name": "elder gods hailing from the dark spaces between the stars",
  85. "shortname": "gods",
  86. "stats": [6,6,6,6],
  87. },
  88. {
  89. "id": 10,
  90. "name": "a Flying Brain Monster",
  91. "shortname": "Flying Brain Monster",
  92. "stats": [2,3,6,1],
  93. },
  94. ]
  95. def __init__(self):
  96. self.loc_desc = Plot.loc1[r.randint(0, len(Plot.loc1)-1)]
  97. self.location = Plot.loc2[r.randint(0, len(Plot.loc2)-1)]
  98. self.missIndex = r.randint(0, len(Plot.miss)-1)
  99. self.oops = r.randint(1,12)
  100. self.mission = ""
  101. if self.oops == 12:
  102. self.mission += "...well, they weren't paying attention, so don't tell them, but they're supposed "
  103. self.mission += Plot.miss[r.randint(0, len(Plot.miss)-1)]
  104. self.probIndex = r.randint(0, len(Plot.prob)-1)
  105. self.problem = Plot.prob[self.probIndex]
  106. self.secProblem = None
  107. self.thirdProblem = None
  108. if self.problem["id"] in [1,2]:
  109. self.problem["name"] += " led by " + gen_name()
  110. if self.problem["id"] in [4,7,8,10]:
  111. self.problem["name"] += " named " + gen_name(minimum=5, maximum=12)
  112. if self.problem["id"] == 3:
  113. self.secProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  114. if self.problem["id"] == 10:
  115. self.secProbIndex = r.randint(0, len(Plot.prob)-2)
  116. self.secProblem = Plot.prob[self.secProbIndex]
  117. if self.secProbIndex == 3:
  118. self.thirdProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  119. self.fullProblem = self.problem["name"]
  120. if self.secProblem and self.secProblem["name"] != "Parasites":
  121. self.fullProblem += " and its minion, " + self.secProblem["name"]
  122. class Character:
  123. def __init__(self):
  124. self.name = ""
  125. self.career = ""
  126. self.stats = []
  127. def generate(self):
  128. self.gen_name()
  129. self.gen_stats()
  130. self.gen_career()
  131. def gen_name(self):
  132. self.name = gen_name()
  133. def gen_stats(self, n=12):
  134. if n < 0:
  135. print("Too few stat points!")
  136. return [0,0,0,0]
  137. stats = [0,0,0,0]
  138. points = n
  139. slots = [0,1,2,3]
  140. for _ in range(points):
  141. tgl = False
  142. while tgl == False:
  143. slt = r.choice(slots)
  144. if slt <= 1:
  145. if stats[slt] == 6: continue
  146. if stats[slt] == 5 and r.randint(0,1) != 1: continue
  147. else:
  148. if stats[slt] == 6: continue
  149. if stats[slt] > 2 and r.randint(0,stats[slt]-2) != 1: continue
  150. stats[slt] += 1
  151. tgl = True
  152. stats[3] = stats[3] + 2
  153. if stats[3] > 6:
  154. stats[3] = 6
  155. stats[2] = stats[2] + 2
  156. if stats[2] > 6:
  157. stats[2] = 6
  158. self.stats = stats
  159. def gen_career(self):
  160. self.career = r.choice(["Soldier/Guard","Pilot","Medic","Mechanic","Politician","Spellcaster","Performer","Historian","Spy","Cook","Cartographer","Inventor","Merchant"])
  161. def print_name(self):
  162. print(f"Name: {self.name} (Kobold {self.career})")
  163. def print(self):
  164. self.print_name()
  165. print(f"Order: {self.stats[0]}")
  166. print(f"Chaos: {self.stats[1]}")
  167. print(f"Brain: {self.stats[2]}")
  168. print(f"Body: {self.stats[3]}")
  169. class Ship:
  170. def __init__(self):
  171. self.name1 = r.choice(["Red","Orange","Yellow","Green","Blue","Violet","Dark","Light","Frenzied","Maniacal","Ancient"])
  172. self.name2 = r.choice(["Moon","Comet","Star","Saber","World-Eater","Dancer","Looter","Phlogiston","Fireball","Mecha","Raptor"])
  173. 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"])
  174. 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"])
  175. self.fullname = f"{self.name1} {self.name2}"
  176. def print(self):
  177. print(f"The {self.fullname} {self.gqual}, but {self.bqual}.")
  178. print()
  179. class Campaign:
  180. def __init__(self, n=None, makeChars=True):
  181. n = 6 if n == None else n
  182. self.ship = Ship()
  183. self.params = Plot()
  184. if makeChars:
  185. self.characters = []
  186. for _ in range(n):
  187. c = Character()
  188. c.generate()
  189. self.characters.append(c)
  190. self.art = "an" if self.params.loc_desc[0] in ["a","e","i","o","u"] else "a"
  191. def print_params(self, endc=""):
  192. st = ["Order:", "Chaos:", "Brains:", "Body:"]
  193. print(f"The Kobolds of the {self.ship.fullname} ", end=endc)
  194. print(f"have been sent out to {self.art} {self.params.loc_desc} {self.params.location} ", end=endc)
  195. print(f"in order {self.params.mission} ", end=endc)
  196. print(f"-- but they're challenged by {self.params.fullProblem}!")
  197. cst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.problem["stats"]]))])
  198. print(f"The stats of the {self.params.problem['shortname']}: {cst}")
  199. if self.params.secProblem:
  200. mst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.secProblem["stats"]]))])
  201. print(f"- The stats of the {self.params.secProblem['shortname']}: {mst}")
  202. if self.params.thirdProblem:
  203. pst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.thirdProblem["stats"]]))])
  204. print(f"- - The stats of the {self.params.thirdProblem['shortname']}: {pst}")
  205. print()
  206. self.ship.print()
  207. def print_chars(self):
  208. print("The kobolds:")
  209. for k in self.characters:
  210. k.print()
  211. if __name__ == "__main__":
  212. parser = argparse.ArgumentParser()
  213. group = parser.add_mutually_exclusive_group()
  214. group.add_argument("-c", "--campaign", help="print a full campaign block with N kobolds (default 6)", nargs="?", const=6, type=int, metavar="N")
  215. group.add_argument("-k", "--kobolds", help="print N kobolds", type=int, nargs="?", const=1, default=1, metavar="N")
  216. group.add_argument("-n", "--names", help="print N kobolds without stat blocks", nargs="?", const=1, type=int, metavar="N")
  217. group.add_argument("-p", "--params", help="print only the parameters of a campaign", action="store_true")
  218. group.add_argument("-s", "--ship", help="print only the ship name and description", action="store_true")
  219. args = parser.parse_args()
  220. # print(args)
  221. if args.campaign:
  222. cmp = Campaign(args.campaign)
  223. cmp.print_params()
  224. cmp.print_chars()
  225. elif args.params:
  226. cmp = Campaign(makeChars=False)
  227. cmp.print_params()
  228. elif args.ship:
  229. ship = Ship()
  230. ship.print()
  231. elif args.names:
  232. for _ in range(args.names):
  233. c = Character()
  234. c.generate()
  235. c.print_name()
  236. else:
  237. for _ in range(args.kobolds):
  238. c = Character()
  239. c.generate()
  240. c.print()