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

4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
4 년 전
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. import random as r
  2. import argparse
  3. import sys
  4. 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"]
  5. mid = beg + ["l","x","n","r"]
  6. def binarize(num, l=None):
  7. r = bin(num)[2:]
  8. try:
  9. l = int(l)
  10. except:
  11. l = None
  12. if l != None:
  13. while len(r) < l:
  14. r = "0" + r
  15. return r
  16. def gen_name(length=None, minimum=3):
  17. maximum = 8
  18. if length == None:
  19. lgt = r.randint(minimum,maximum)
  20. else:
  21. if length > maximum:
  22. length = maximum
  23. print("Maximum name length is 8.")
  24. lgt = length
  25. morae = []
  26. while len("".join(morae)) < lgt:
  27. if len(morae) == 0:
  28. mora = r.choice(beg)
  29. morae.append(mora[0].upper() + mora[1:])
  30. else:
  31. mora = r.choice(mid)
  32. if morae[-1] == mora:
  33. mora = r.choice(mid)
  34. morae.append(mora)
  35. fname = "".join(morae)[:lgt]
  36. return fname
  37. class Plot:
  38. loc1 = ["friendly","hostile","derelict","airless","poison-filled/covered","overgrown","looted","burning","frozen","haunted","infested"]
  39. loc2 = ["asteroid","moon","space station","spaceship","ringworld","Dyson sphere","planet","Space Whale","pocket of folded space","time vortex","Reroll"]
  40. miss = ["explore","loot everything not bolted down too securely","find the last group of kobolds who came here","find a rumored secret weapon","find a way to break someone else's secret weapon","claim this place in the name of the Kobold Empire","make friends","rediscover lost technology","find lost magical items","find and defeat a powerful enemy"]
  41. prob = [
  42. {
  43. "id": 0,
  44. "name": "an Undead Sample Pack (swarm of zombies and skeletons)",
  45. "shortname": "undead",
  46. "stats": [0,5,2,6],
  47. },
  48. {
  49. "id": 1,
  50. "name": "a rival band of kobolds",
  51. "shortname": "kobold rivals",
  52. "stats": [3,3,4,4],
  53. },
  54. {
  55. "id": 2,
  56. "name": "a detachment from the Elf Armada",
  57. "shortname": "elves",
  58. "stats": [4,3,5,4],
  59. },
  60. {
  61. "id": 3,
  62. "name": "refugees with parasites. Big parasites",
  63. "shortname": "refugees",
  64. "stats": [2,4,0,0],
  65. },
  66. {
  67. "id": 4,
  68. "name": "an artificial intelligence bent on multi-world domination",
  69. "shortname": "AI",
  70. "stats": [4,1,6,3],
  71. },
  72. {
  73. "id": 5,
  74. "name": "robot spiders",
  75. "shortname": "spiders",
  76. "stats": [3,3,2,4],
  77. },
  78. {
  79. "id": 6,
  80. "name": "semi-intelligent metal eating slime",
  81. "shortname": "slime",
  82. "stats": [0,2,1,5],
  83. },
  84. {
  85. "id": 7,
  86. "name": "a living asteroid that intends to follow the kobolds home like the largest puppy",
  87. "shortname": "asteroid",
  88. "stats": [2,3,1,6],
  89. },
  90. {
  91. "id": 8,
  92. "name": "an old lich that wants everyone to stay off of their 'lawn'",
  93. "shortname": "lich",
  94. "stats": [5,2,6,3],
  95. },
  96. {
  97. "id": 9,
  98. "name": "elder gods hailing from the dark spaces between the stars",
  99. "shortname": "gods",
  100. "stats": [0,6,6,6],
  101. },
  102. {
  103. "id": 10,
  104. "name": "a Flying Brain Monster",
  105. "shortname": "Flying Brain Monster",
  106. "stats": [2,3,6,1],
  107. },
  108. ]
  109. def __init__(self, loc_desc=None, locIndex=None, battlefield=None, location=None, missIndex=None, oops=None, mission=None, probIndex=None, problem=None, probName=None, secProblem=None, thirdProblem=None):
  110. self.loc_desc = loc_desc if loc_desc != None else Plot.loc1[r.randint(0, len(Plot.loc1)-1)]
  111. self.locIndex = int(locIndex) if locIndex != None else r.randint(0, len(Plot.loc2)-1)
  112. self.battlefield = int(battlefield) if battlefield != None else 0
  113. self.location = location if location != None else Plot.loc2[self.locIndex]
  114. if locIndex == None and self.locIndex == len(Plot.loc2) - 2:
  115. if self.battlefield == 0:
  116. self.battlefield = 1
  117. self.locIndex = r.randint(0, len(Plot.loc2)-3)
  118. elif locIndex == None and self.locIndex != len(Plot.loc2) - 1:
  119. if self.location == "":
  120. self.location = Plot.loc2[self.locIndex]
  121. if self.location[0].lower() in ["a","e","i","o","u"]:
  122. self.locart = 1
  123. else:
  124. self.locart = 0
  125. self.missIndex = missIndex if missIndex != None else r.randint(0, len(Plot.miss)-1)
  126. self.oops = int(oops) if oops != None else r.randint(1,12)
  127. if self.oops != 1:
  128. self.oops = 0
  129. self.mission = Plot.miss[self.missIndex]
  130. self.probIndex = probIndex if probIndex != None else r.randint(0, len(Plot.prob)-1)
  131. self.problem = Plot.prob[self.probIndex]
  132. self.secProblem = [x for x in Plot.prob if x["id"] == secProblem][0] if (secProblem != None and secProblem in list(filter(lambda x:x["id"], Plot.prob))) else None
  133. self.thirdProblem = [x for x in Plot.prob if x["id"] == thirdProblem][0] if (thirdProblem != None and thirdProblem in list(filter(lambda x:x["id"], Plot.prob))) else None
  134. self.problem["givenname"] = probName if probName != None else gen_name()
  135. if self.problem["id"] == 3:
  136. self.secProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  137. if self.problem["id"] == 10:
  138. self.secProbIndex = r.randint(0, len(Plot.prob)-2)
  139. self.secProblem = Plot.prob[self.secProbIndex]
  140. if self.secProbIndex == 3:
  141. self.thirdProblem = {"name": "Parasites", "shortname": "parasites", "stats": [3,4,2,3]}
  142. self.fullProblem = self.problem["givenname"] + ", " + self.problem["name"]
  143. if self.secProblem and self.secProblem["name"] != "Parasites":
  144. self.fullProblem += " and its minion, " + self.secProblem["name"]
  145. class Character:
  146. GADGETS = [ {"id": 0, "name": "Awesome Dagger of Sneak(?) Attacks", "description": "Yell 'Sneak Attack!' to count a mixed-success Body attack as a success.", "reusable": True},
  147. {"id": 1, "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},
  148. {"id": 2, "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},
  149. {"id": 3, "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},
  150. {"id": 4, "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},
  151. {"id": 5, "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},
  152. {"id": 6, "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},
  153. {"id": 7, "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},
  154. {"id": 8, "name": "Pocket Sand!", "description": "Yell 'Pocket Sand!' to count a successful Body attack against you as a mixed success.", "reusable": False},
  155. {"id": 9, "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},
  156. {"id": 10, "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},
  157. {"id": 11, "name": "Cloak of Adsorption", "description": "The cloak starts out white. Whenever you are the target of a successful attack, the cloak becomes the color of whatever hit you. If your cloak is already the same color as whatever hit you, the attack becomes a mixed success and the cloak turns black and can no longer absorb colors. The cloak becomes white again after a nap.", "reusable": True},
  158. {"id": 12, "name": "Cloak of Absorption", "description": "The cloak starts out white. You can remove the cloak and lay it over difficult terrain to make it easy terrain; if the terrain is difficult because the ground is wet, the cloak becomes wet and the ground in that area becomes dry. The cloak dries out after a nap.", "reusable": True},
  159. {"id": 13, "name": "Cloak of Desorption", "description": "The cloak starts out black. As an Event, you can cause a gas to evaporate from the cloak, leaving it a dingy gray and healing 1 Body to anyone within ten feet of you. The cloak becomes black again after a nap.", "reusable": True},
  160. {"id": 14, "name": "Cloak of Food Portions", "description": "Wearing this cloak causes it to flare out at the base, making the wearer resemble a pyramid. In conversations regarding food the wearer can add +2 to Brains rolls when trying to convince others to alter their diets. This can be done once per nap. Considering how many large things tend to snack on kobolds, this has been found to actually be quite useful.", "reusable": True},
  161. {"id": 15, "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},
  162. {"id": 123, "name": "Shortbow of the Watch", "description": "Choose a single target. You get +1 to Body damage against that target and +1 to Brain rolls to track that target. Activating this is not an Event.", "reusable": True},
  163. {"id": 124, "name": "Hammer of Thunderbolts", "description": "You get +1 Body damage on any successful or partially successful attack with this weapon. You can also throw it at a target or surface, which will stun any creature within 30 feet of the impact for 1d6 Events.", "reusable": True},
  164. {"id": 125, "name": "Eldritch Cannon", "description": "You conjure a small cannon that can produce a single spell effect of your choice as an Event.", "reusable": True},
  165. {"id": 126, "name": "Staff of Lightning", "description": "As an Event, create a lightning bolt from the tip of the staff that travels 30 feet in a straight line and deals 2 Body damage to any target in its path.", "reusable": True},
  166. {"id": 127, "name": "Voyager Staff", "description": "Any spell you cast has +1 damage. In addition, as an Event, you can move up to 50 feet in any direction - even through walls and other solid surfaces.", "reusable": True}
  167. ]
  168. CAREERS = [ {"id": 0, "name": "Soldier/Guard"},
  169. {"id": 1, "name": "Pilot"},
  170. {"id": 2, "name": "Medic"},
  171. {"id": 3, "name": "Mechanic"},
  172. {"id": 4, "name": "Politician"},
  173. {"id": 5, "name": "Spellcaster"},
  174. {"id": 6, "name": "Performer"},
  175. {"id": 7, "name": "Historian"},
  176. {"id": 8, "name": "Spy"},
  177. {"id": 9, "name": "Cook"},
  178. {"id": 10, "name": "Cartographer"},
  179. {"id": 11, "name": "Inventor"},
  180. {"id": 12, "name": "Merchant"},
  181. {"id": 123, "name": "Ranger"},
  182. {"id": 124, "name": "Barbarian"},
  183. {"id": 125, "name": "Artificer"},
  184. {"id": 126, "name": "Druid"},
  185. {"id": 127, "name": "Wizard"}
  186. ]
  187. def __init__(self, name=None, career=None, stats=None, gadget=None):
  188. self.name = name if name != None else ""
  189. if career == None:
  190. self.career = ""
  191. elif isinstance(career, str):
  192. self.career = career
  193. elif isinstance(career, int) and career in range(13):
  194. self.career = [x for x in Character.CAREERS if x["id"] == career][0]
  195. else:
  196. self.career = ""
  197. self.stats = stats if stats != None else []
  198. if gadget == None:
  199. self.gadget = ""
  200. elif isinstance(gadget, str) or isinstance(gadget, dict):
  201. self.gadget = gadget
  202. else:
  203. self.gadget = next((x for x in Character.GADGETS if x["id"] == gadget), "")
  204. self.generate()
  205. def generate(self):
  206. if self.name == "" or self.name == None:
  207. self.gen_name()
  208. if self.stats == [] or self.stats == None:
  209. self.gen_stats()
  210. if self.career == "" or self.career == None:
  211. self.gen_career()
  212. if self.gadget == "" or self.gadget == None:
  213. self.gen_gadget()
  214. def gen_name(self):
  215. self.name = gen_name()
  216. def gen_stats(self, n=12):
  217. if n < 0:
  218. print("Too few stat points!")
  219. return [0,0,0,0]
  220. stats = [0,0,0,0]
  221. points = n
  222. slots = [0,1,2,3]
  223. for _ in range(points):
  224. tgl = False
  225. while tgl == False:
  226. slt = r.choice(slots)
  227. if slt <= 1:
  228. if stats[slt] == 6: continue
  229. if stats[slt] == 5 and r.randint(0,1) != 1: continue
  230. else:
  231. if stats[slt] == 6: continue
  232. if stats[slt] > 2 and r.randint(0,stats[slt]-2) != 1: continue
  233. stats[slt] += 1
  234. tgl = True
  235. stats[3] = stats[3] + 1
  236. if stats[3] > 6:
  237. stats[3] = 6
  238. stats[2] = stats[2] + 1
  239. if stats[2] > 6:
  240. stats[2] = 6
  241. self.stats = stats
  242. def gen_career(self):
  243. cid = r.randint(0,12)
  244. self.career = next((x for x in Character.CAREERS if x["id"] == cid), "")
  245. def gen_gadget(self):
  246. gid = r.randint(0,15)
  247. self.gadget = [x for x in Character.GADGETS if x["id"] == gid][0]
  248. def print_name(self, html=False):
  249. if isinstance(self.career, str):
  250. cname = self.career
  251. cid = next((x for x in Character.CAREERS if x["name"] == cname), "")
  252. else:
  253. cname = self.career["name"]
  254. c = dict(next((x for x in Character.CAREERS if x["name"] == cname), ""))
  255. cid = c["id"]
  256. if html:
  257. charText = f"<h4>Name: {self.name} (Kobold {cname})</h4>"
  258. else:
  259. charText = f"\nName: {self.name} (Kobold {cname} {cid})"
  260. print(charText)
  261. def print(self, html=False):
  262. if html:
  263. if isinstance(self.career, str):
  264. cname = self.career
  265. else:
  266. cname = self.career["name"]
  267. if isinstance(self.gadget, str):
  268. gdg = {"id": 127, "name": self.gadget, "description": "", "reusable": True}
  269. else:
  270. gdg = self.gadget
  271. out = (
  272. f"<div class='kobold'>\n"
  273. f" <span class='koboldid'>\n"
  274. f" <span class='koboldname'>{self.name}</span><br>\n"
  275. f" <span class='koboldcareer'>Kobold {cname}</span>\n"
  276. f" </span>\n"
  277. f" <br>\n"
  278. f" <span class='koboldstats'>\n"
  279. f" <ul>\n"
  280. f" <li>Order: {self.stats[0]}</li>\n"
  281. f" <li>Chaos: {self.stats[1]}</li>\n"
  282. f" <li>Brain: {self.stats[2]}</li>\n"
  283. f" <li>Body: {self.stats[3]}</li>\n"
  284. f" </ul>\n"
  285. f" </span>\n"
  286. f" <br>\n"
  287. f" <span class='koboldgadget'>\n"
  288. f" <span class='koboldgadgetname'>{gdg['name']}</span><br>\n"
  289. f" <span class='koboldgadgetdescription'>{gdg['description']}</span>\n"
  290. f" <span class='koboldgadgetreuse'>{'<br>Reusable' if gdg['reusable'] else ''}</span>\n"
  291. f" </span>"
  292. f"</div>\n"
  293. )
  294. print(out)
  295. else:
  296. self.print_name()
  297. print(f"Order: {self.stats[0]}")
  298. print(f"Chaos: {self.stats[1]}")
  299. print(f"Brain: {self.stats[2]}")
  300. print(f"Body: {self.stats[3]}")
  301. if isinstance(self.gadget, str):
  302. print(f"Gadget: {self.gadget}")
  303. else:
  304. print(f"Gadget: {self.gadget['name']} ({self.gadget['description']}{'- Reusable' if self.gadget['reusable'] else ''})")
  305. class Ship:
  306. NAME1 = ["Red","Orange","Yellow","Green","Blue","Violet","Dark","Light","Frenzied","Maniacal","Ancient"]
  307. NAME2 = ["Moon","Comet","Star","Saber","World-Eater","Dancer","Looter","Phlogiston","Fireball","Mecha","Raptor"]
  308. GQUAL = ["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"]
  309. BQUAL = ["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"]
  310. def __init__(self, name1=None, name2=None, gqual = None, bqual = None):
  311. self.name1 = name1 if name1 != None else r.choice(Ship.NAME1)
  312. self.name2 = name2 if name2 != None else r.choice(Ship.NAME2)
  313. self.gqual = gqual if gqual != None else r.choice(Ship.GQUAL)
  314. self.bqual = bqual if bqual != None else r.choice(Ship.BQUAL)
  315. self.fullname = f"{self.name1} {self.name2}"
  316. def print(self, html=False):
  317. if (html):
  318. 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"
  319. else:
  320. shipText = f"The {self.fullname} {self.gqual}, but {self.bqual}.\n"
  321. print(shipText)
  322. class Campaign:
  323. ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?- "
  324. NAMELETS = ['a', 'e', 'i', 'o', 'u', 'b', 'y', 'd', 'f', 'g', 'k', 'm', 'n', 'p', 'r', 's', 't', 'w', 'z', 'l', 'x']
  325. def __init__(self, n=None, makeChars=True, fromPW=False, pw=None):
  326. if fromPW == True:
  327. self.ship = None
  328. self.params = None
  329. self.characters = None
  330. self.art = None
  331. self.masterpassword = pw
  332. self.decode_key(pw)
  333. else:
  334. self.masterpassword = None
  335. self.create_campaign(n, makeChars)
  336. def create_campaign(self, n, makeChars):
  337. n = 6 if n == None else n
  338. self.ship = Ship()
  339. self.params = Plot()
  340. self.params.problem["fullname"] = ""
  341. if self.params.problem["id"] in [1,2]:
  342. self.params.problem["fullname"] += " led by " + self.params.problem["name"]
  343. if self.params.problem["id"] in [4,7,8,10]:
  344. self.params.problem["fullname"] += " named " + self.params.problem["name"]
  345. if makeChars:
  346. self.characters = []
  347. for _ in range(n):
  348. c = Character()
  349. c.generate()
  350. self.characters.append(c)
  351. self.art = "an" if self.params.loc_desc[0] in ["a","e","i","o","u"] else "a"
  352. def generate_key(self):
  353. """
  354. "PACKTA CTICS! ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- -------" should generate the Season 3 Pack Tactics crew.
  355. "JUSTIN BAILEY" and "NARPAS SWORD" should do something too.
  356. Key is analphanumeric string generated from a bitfield
  357. Bitfield is:
  358. Location 1: 7 bits
  359. Location 2: 7 bits
  360. Location Battlefield: 1 bit
  361. Mission: 7 bits
  362. Oops: 1 bit
  363. Problem 1: 7 bits
  364. Problem 1 name: 40 bits
  365. Problem 2: 7 bits
  366. Ship name 1: 7 bits
  367. Ship name 2: 7 bits
  368. Ship gqual: 7 bits
  369. Ship bqual: 7 bits
  370. Character 1 name: 40 bits
  371. Character 1 career: 7 bits
  372. Character 1 order: 3 bits
  373. Character 1 chaos: 3 bits
  374. Character 1 body: 3 bits
  375. Character 1 brain: 3 bits
  376. Character 1 gadget: 7 bits
  377. Character 2 name: 40 bits
  378. Character 2 career: 7 bits
  379. Character 2 order: 3 bits
  380. Character 2 chaos: 3 bits
  381. Character 2 body: 3 bits
  382. Character 2 brain: 3 bits
  383. Character 2 gadget: 7 bits
  384. Character 3 name: 40 bits
  385. Character 3 career: 7 bits
  386. Character 3 order: 3 bits
  387. Character 3 chaos: 3 bits
  388. Character 3 body: 3 bits
  389. Character 3 brain: 3 bits
  390. Character 3 gadget: 7 bits
  391. Character 4 name: 40 bits
  392. Character 4 career: 7 bits
  393. Character 4 order: 3 bits
  394. Character 4 chaos: 3 bits
  395. Character 4 body: 3 bits
  396. Character 4 brain: 3 bits
  397. Character 4 gadget: 7 bits
  398. Character 5 name: 40 bits
  399. Character 5 career: 7 bits
  400. Character 5 order: 3 bits
  401. Character 5 chaos: 3 bits
  402. Character 5 body: 3 bits
  403. Character 5 brain: 3 bits
  404. Character 5 gadget: 7 bits
  405. Character 6 name: 40 bits
  406. Character 6 career: 7 bits
  407. Character 6 order: 3 bits
  408. Character 6 chaos: 3 bits
  409. Character 6 body: 3 bits
  410. Character 6 brain: 3 bits
  411. Character 6 gadget: 7 bits
  412. 104 for campaign
  413. 396 for characters
  414. total 501
  415. 11 remaining
  416. """
  417. self.key = ""
  418. l1 = bin(Plot.loc1.index(self.params.loc_desc))[2:]
  419. l2 = bin(Plot.loc2.index(self.params.location))[2:]
  420. lb = bin(self.params.battlefield)[2:]
  421. op = bin(self.params.oops)[2:]
  422. ms = bin(self.params.missIndex)[2:]
  423. pb1 = bin(self.params.probIndex)[2:]
  424. if self.params.probIndex == 10:
  425. pb2 = bin(self.params.secProbIndex)[2:]
  426. else:
  427. pb2 = bin(127)[2:]
  428. pbname = self.encode_name(self.params.problem["givenname"])
  429. n1 = bin(Ship.NAME1.index(self.ship.name1))[2:]
  430. n2 = bin(Ship.NAME2.index(self.ship.name2))[2:]
  431. gq = bin(Ship.GQUAL.index(self.ship.gqual))[2:]
  432. bq = bin(Ship.BQUAL.index(self.ship.bqual))[2:]
  433. chars = {}
  434. i = 0
  435. for chct in self.characters:
  436. chars[i] = {
  437. "name": self.encode_name(chct.name),
  438. "career": bin(chct.career["id"])[2:],
  439. "order": bin(chct.stats[0])[2:],
  440. "chaos": bin(chct.stats[1])[2:],
  441. "body": bin(chct.stats[2])[2:],
  442. "brains": bin(chct.stats[3])[2:],
  443. "gadget": bin(chct.gadget["id"])[2:]
  444. }
  445. i += 1
  446. l1 = lpad(l1, 7)
  447. l2 = lpad(l2, 7)
  448. ms = lpad(ms, 7)
  449. pb1 = lpad(pb1, 7)
  450. pb2 = lpad(pb2, 7)
  451. pbname = lpad(pbname, 40, "1")
  452. n1 = lpad(n1, 7)
  453. n2 = lpad(n2, 7)
  454. gq = lpad(gq, 7)
  455. bq = lpad(bq, 7)
  456. self.key += l1 + l2 + lb + ms + op + pb1 + pbname + pb2 + n1 + n2 + gq + bq
  457. for k,chct in chars.items():
  458. chct["name"] = lpad(chct["name"], 40, "1")
  459. chct["career"] = lpad(chct["career"], 7)
  460. chct["order"] = lpad(chct["order"],3)
  461. chct["chaos"] = lpad(chct["chaos"], 3)
  462. chct["body"] = lpad(chct["body"], 3)
  463. chct["brains"] = lpad(chct["brains"], 3)
  464. chct["gadget"] = lpad(chct["gadget"], 7)
  465. self.key += chct["name"] + chct["career"] + chct["order"] + chct["chaos"] + chct["body"] + chct["brains"] + chct["gadget"]
  466. while len(self.key) < 509:
  467. self.key = self.key + "0"
  468. letters = []
  469. letter = []
  470. for bit in self.key:
  471. letter.append(bit)
  472. if len(letter) == 6:
  473. letters.append(Campaign.ALPHABET[int("".join(letter),2)])
  474. letter = []
  475. words = []
  476. word = []
  477. for lt in letters:
  478. word.append(lt)
  479. if len(word) == 6:
  480. words.append("".join(word))
  481. word = []
  482. words.append("".join(word))
  483. self.password = " ".join(words)
  484. return self.password
  485. def decode_key(self, pw):
  486. densePwd = pw.replace(" ", "")
  487. if len(densePwd) != 84 and (densePwd not in ["PACKTACTICS!", "JUSTINBAILEY", "NARPASSWORD"]):
  488. print("This password is not valid. If this is a password that this generator created, please email noelle@noelle.codes and let me know.")
  489. sys.exit(0)
  490. if densePwd[:12] == "PACKTACTICS!":
  491. # Create a campaign featuring the Season 3 Pack Tactics crew.
  492. self.ship = Ship("Red", "Star", "is maneuverable & unarmored", "has a politician who thinks they're in charge of it")
  493. self.params = Plot()
  494. self.art = "an" if self.params.loc_desc[0] in ["a","e","i","o","u"] else "a"
  495. self.characters = []
  496. self.characters.append(Character("Niwri", 123, [3,4,4,3], 123))
  497. self.characters.append(Character("Zax", 124, [1, 6, 2, 5], 124))
  498. self.characters.append(Character("Chroma", 125, [4, 2, 5, 3], 125))
  499. self.characters.append(Character("Zenosha", 126, [3, 3, 5, 3], 126))
  500. self.characters.append(Character("Snax", 127, [2, 4, 5, 1], 127))
  501. # self.print_params()
  502. # self.print_chars()
  503. return
  504. elif densePwd[:12] == "JUSTINBAILEY":
  505. # Create a random campaign, but everyone's Gadget is a leotard that somehow is also an environment suit
  506. self.create_campaign(n=6, makeChars=True)
  507. for c in self.characters:
  508. c.gadget = {"id":127, "name": "The Bailey", "description": "A form-fitting leotard that somehow protects the wearer from all environmental effects except extreme heat - including vacuum and poison.", "reusable": True}
  509. # self.print_params()
  510. # self.print_chars()
  511. return
  512. elif densePwd[:12] == "NARPASSWORD":
  513. # Create a random campaign, but all the kobolds' stats are set to 6
  514. self.create_campaign(n=6, makeChars=True)
  515. for c in self.characters:
  516. c.stats = [6,6,6,6]
  517. # self.print_params()
  518. # self.print_chars()
  519. return
  520. numPwd = []
  521. for c in densePwd:
  522. numPwd.append(Campaign.ALPHABET.index(c))
  523. bitPwd = [lpad(bin(x).replace("0b",""), 6) for x in numPwd]
  524. longBitPwd = []
  525. for word in bitPwd:
  526. longword = lpad(word,6)
  527. longBitPwd.append(longword)
  528. self.newBitfield = "".join(longBitPwd)
  529. while len(self.newBitfield) < 509:
  530. self.newBitfield += "0"
  531. outkey = {}
  532. # Location 1: 7 bits
  533. i,j = 0,7
  534. outkey["loc1"] = int(self.newBitfield[i:j], 2)
  535. # Location 2: 7 bits
  536. i,j = j,j+7
  537. outkey["loc2"] = int(self.newBitfield[i:j], 2)
  538. # Location Battlefield: 1 bit
  539. i,j = j,j+1
  540. outkey["battlefield"] = int(self.newBitfield[i:j], 2)
  541. # Mission: 7 bits
  542. i,j = j,j+7
  543. outkey["miss"] = int(self.newBitfield[i:j], 2)
  544. # Oops: 1 bit
  545. i,j = j,j+1
  546. outkey["oops"] = int(self.newBitfield[i:j], 2)
  547. # Problem 1: 7 bits
  548. i,j = j,j+7
  549. outkey["prob1"] = int(self.newBitfield[i:j], 2)
  550. # Problem 1 name: 40 bits
  551. i,j = j,j+40
  552. outkey["prob1name"] = self.newBitfield[i:j]
  553. # Problem 2: 7 bits
  554. i,j = j,j+7
  555. outkey["prob2"] = int(self.newBitfield[i:j], 2)
  556. # Ship name 1: 7 bits
  557. i,j = j,j+7
  558. outkey["sname1"] = int(self.newBitfield[i:j], 2)
  559. # Ship name 2: 7 bits
  560. i,j = j,j+7
  561. outkey["sname2"] = int(self.newBitfield[i:j], 2)
  562. # Ship gqual: 7 bits
  563. i,j = j,j+7
  564. outkey["gqual"] = int(self.newBitfield[i:j], 2)
  565. # Ship bqual: 7 bits
  566. i,j = j,j+7
  567. outkey["bqual"] = int(self.newBitfield[i:j], 2)
  568. # Character 1 name: 40 bits
  569. i,j = j,j+40
  570. outkey["char1name"] = self.newBitfield[i:j]
  571. # Character 1 career: 7 bits
  572. i,j = j,j+7
  573. outkey["char1career"] = int(self.newBitfield[i:j], 2)
  574. # Character 1 order: 3 bits
  575. i,j = j,j+3
  576. outkey["char1ord"] = int(self.newBitfield[i:j], 2)
  577. # Character 1 chaos: 3 bits
  578. i,j = j,j+3
  579. outkey["char1cha"] = int(self.newBitfield[i:j], 2)
  580. # Character 1 body: 3 bits
  581. i,j = j,j+3
  582. outkey["char1bod"] = int(self.newBitfield[i:j], 2)
  583. # Character 1 brain: 3 bits
  584. i,j = j,j+3
  585. outkey["char1bra"] = int(self.newBitfield[i:j], 2)
  586. # Character 1 gadget: 7 bits
  587. i,j = j,j+7
  588. outkey["char1gad"] = int(self.newBitfield[i:j], 2)
  589. # Character 2 name: 40 bits
  590. i,j = j,j+40
  591. outkey["char2name"] = self.newBitfield[i:j]
  592. # Character 2 career: 7 bits
  593. i,j = j,j+7
  594. outkey["char2career"] = int(self.newBitfield[i:j], 2)
  595. # Character 2 order: 3 bits
  596. i,j = j,j+3
  597. outkey["char2ord"] = int(self.newBitfield[i:j], 2)
  598. # Character 2 chaos: 3 bits
  599. i,j = j,j+3
  600. outkey["char2cha"] = int(self.newBitfield[i:j], 2)
  601. # Character 2 body: 3 bits
  602. i,j = j,j+3
  603. outkey["char2bod"] = int(self.newBitfield[i:j], 2)
  604. # Character 2 brain: 3 bits
  605. i,j = j,j+3
  606. outkey["char2bra"] = int(self.newBitfield[i:j], 2)
  607. # Character 2 gadget: 7 bits
  608. i,j = j,j+7
  609. outkey["char2gad"] = int(self.newBitfield[i:j], 2)
  610. # Character 3 name: 40 bits
  611. i,j = j,j+40
  612. outkey["char3name"] = self.newBitfield[i:j]
  613. # Character 3 career: 7 bits
  614. i,j = j,j+7
  615. outkey["char3career"] = int(self.newBitfield[i:j], 2)
  616. # Character 3 order: 3 bits
  617. i,j = j,j+3
  618. outkey["char3ord"] = int(self.newBitfield[i:j], 2)
  619. # Character 3 chaos: 3 bits
  620. i,j = j,j+3
  621. outkey["char3cha"] = int(self.newBitfield[i:j], 2)
  622. # Character 3 body: 3 bits
  623. i,j = j,j+3
  624. outkey["char3bod"] = int(self.newBitfield[i:j], 2)
  625. # Character 3 brain: 3 bits
  626. i,j = j,j+3
  627. outkey["char3bra"] = int(self.newBitfield[i:j], 2)
  628. # Character 3 gadget: 7 bits
  629. i,j = j,j+7
  630. outkey["char3gad"] = int(self.newBitfield[i:j], 2)
  631. # Character 4 name: 40 bits
  632. i,j = j,j+40
  633. outkey["char4name"] = self.newBitfield[i:j]
  634. # Character 4 career: 7 bits
  635. i,j = j,j+7
  636. outkey["char4career"] = int(self.newBitfield[i:j], 2)
  637. # Character 4 order: 3 bits
  638. i,j = j,j+3
  639. outkey["char4ord"] = int(self.newBitfield[i:j], 2)
  640. # Character 4 chaos: 3 bits
  641. i,j = j,j+3
  642. outkey["char4cha"] = int(self.newBitfield[i:j], 2)
  643. # Character 4 body: 3 bits
  644. i,j = j,j+3
  645. outkey["char4bod"] = int(self.newBitfield[i:j], 2)
  646. # Character 4 brain: 3 bits
  647. i,j = j,j+3
  648. outkey["char4bra"] = int(self.newBitfield[i:j], 2)
  649. # Character 4 gadget: 7 bits
  650. i,j = j,j+7
  651. outkey["char4gad"] = int(self.newBitfield[i:j], 2)
  652. # Character 5 name: 40 bits
  653. i,j = j,j+40
  654. outkey["char5name"] = self.newBitfield[i:j]
  655. # Character 5 career: 7 bits
  656. i,j = j,j+7
  657. outkey["char5career"] = int(self.newBitfield[i:j], 2)
  658. # Character 5 order: 3 bits
  659. i,j = j,j+3
  660. outkey["char5ord"] = int(self.newBitfield[i:j], 2)
  661. # Character 5 chaos: 3 bits
  662. i,j = j,j+3
  663. outkey["char5cha"] = int(self.newBitfield[i:j], 2)
  664. # Character 5 body: 3 bits
  665. i,j = j,j+3
  666. outkey["char5bod"] = int(self.newBitfield[i:j], 2)
  667. # Character 5 brain: 3 bits
  668. i,j = j,j+3
  669. outkey["char5bra"] = int(self.newBitfield[i:j], 2)
  670. # Character 5 gadget: 7 bits
  671. i,j = j,j+7
  672. outkey["char5gad"] = int(self.newBitfield[i:j], 2)
  673. # Character 6 name: 40 bits
  674. i,j = j,j+40
  675. outkey["char6name"] = self.newBitfield[i:j]
  676. # Character 6 career: 7 bits
  677. i,j = j,j+7
  678. outkey["char6career"] = int(self.newBitfield[i:j], 2)
  679. # Character 6 order: 3 bits
  680. i,j = j,j+3
  681. outkey["char6ord"] = int(self.newBitfield[i:j], 2)
  682. # Character 6 chaos: 3 bits
  683. i,j = j,j+3
  684. outkey["char6cha"] = int(self.newBitfield[i:j], 2)
  685. # Character 6 body: 3 bits
  686. i,j = j,j+3
  687. outkey["char6bod"] = int(self.newBitfield[i:j], 2)
  688. # Character 6 brain: 3 bits
  689. i,j = j,j+3
  690. outkey["char6bra"] = int(self.newBitfield[i:j], 2)
  691. # Character 6 gadget: 7 bits
  692. i,j = j,j+7
  693. outkey["char6gad"] = int(self.newBitfield[i:j], 2)
  694. self.ship = Ship(Ship.NAME1[outkey["sname1"]], Ship.NAME2[outkey["sname2"]], Ship.GQUAL[outkey["gqual"]], Ship.BQUAL[outkey["bqual"]])
  695. self.params = Plot(loc_desc=Plot.loc1[outkey["loc1"]], locIndex=outkey["loc2"], battlefield=outkey["battlefield"], location=None, missIndex=outkey["miss"], oops=outkey["oops"], mission=None, probIndex=outkey["prob1"], problem=None, probName=self.decode_name(outkey["prob1name"]), secProblem=outkey["prob2"], thirdProblem=None)
  696. self.art = "an" if self.params.loc_desc[0] in ["a","e","i","o","u"] else "a"
  697. self.characters = []
  698. for q in range(1,7):
  699. keys = [f"char{q}name", f"char{q}career", f"char{q}ord", f"char{q}cha", f"char{q}bod", f"char{q}bra", f"char{q}gad"]
  700. c = Character(name=self.decode_name(outkey[keys[0]]), career=outkey[keys[1]], stats=[outkey[keys[2]], outkey[keys[3]], outkey[keys[4]], outkey[keys[5]]], gadget=outkey[keys[6]])
  701. self.characters.append(c)
  702. #return self.newBitfield
  703. def encode_name(self, name):
  704. field = "".join([lpad(bin(Campaign.NAMELETS.index(c.lower()))[2:], 5) for c in name])
  705. return field
  706. def decode_name(self, field):
  707. i,j = 35,40
  708. name = ""
  709. for _ in range(8):
  710. k = int(field[i:j], 2)
  711. if k != 31:
  712. name = Campaign.NAMELETS[k] + name
  713. i,j = i-5, i
  714. name = name[0].upper() + name[1:]
  715. return name
  716. def print_params(self, endc=" ", html=False):
  717. st = ["Order:", "Chaos:", "Brains:", "Body:"]
  718. cst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.problem["stats"]]))])
  719. if self.params.oops == 1:
  720. oops = "...well, they weren't paying attention, so don't tell them, but they're supposed to"
  721. else:
  722. oops = ""
  723. mission = oops + self.params.mission
  724. lines = [
  725. f"The Kobolds of the {self.ship.fullname}",
  726. f"have been sent out to {self.art} {self.params.loc_desc} {self.params.location}!",
  727. f"in order to {mission}",
  728. f"but they're challenged by {self.params.fullProblem}!",
  729. f"The stats of the {self.params.problem['shortname']}",
  730. f"{cst}"
  731. ]
  732. if self.params.secProblem:
  733. mst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.secProblem["stats"]]))])
  734. lines.append(f"The stats of the {self.params.secProblem['shortname']}")
  735. lines.append(f"{mst}")
  736. if self.params.thirdProblem:
  737. pst = ", ".join([" ".join(y) for y in list(zip(st, [str(x) for x in self.params.thirdProblem["stats"]]))])
  738. lines.append(f"The stats of the {self.params.thirdProblem['shortname']}")
  739. lines.append("{pst}")
  740. if html:
  741. out = (
  742. f"<div id='theship' class='firstrow'>\n"
  743. f" <span class='head'>The Ship</span>\n"
  744. f" <span id='shipname'>\n"
  745. f" The {self.ship.fullname}!\n"
  746. f" </span>\n"
  747. f" <br>\n"
  748. f" <span id='shipquality1'>\n"
  749. f" It {self.ship.gqual}...\n"
  750. f" </span><br>\n"
  751. f" <span id='shipquality2'>\n"
  752. f" But {self.ship.bqual}!\n"
  753. f" </span>\n"
  754. f"</div>\n"
  755. f"<div id='themission' class='firstrow'>\n"
  756. f" <span class='head'>The Mission</span>\n"
  757. f" <span id='missionloc'>\n"
  758. f" The kobolds have been sent to {self.art} {self.params.loc_desc} {self.params.location}!\n"
  759. f" </span><br>\n"
  760. f" <span id='missiontarget'>\n"
  761. f" in order {mission}\n"
  762. f" </span>\n"
  763. f"</div>\n<br clear='all'>\n"
  764. f"<div id='theadversary' class='firstrow'>\n"
  765. f" <span class='head'>The Adversary</span>\n"
  766. f" They're challenged by <span id='advname'>{self.params.fullProblem}</span>!\n"
  767. f" <br>\n"
  768. f" <span id='problemstats'>\n"
  769. f" The stats of the {self.params.problem['shortname']}:<br>\n"
  770. f" {cst}\n"
  771. f" </span>\n"
  772. )
  773. if self.params.secProblem:
  774. out += (
  775. f" <br><span id='secprobstats'>\n"
  776. f" The stats of the {self.params.secProblem['shortname']}:<br>\n"
  777. f" {mst}\n"
  778. f" </span>\n"
  779. )
  780. if self.params.thirdProblem:
  781. out += (
  782. f" <br><span id='thirdprobstats'>\n"
  783. f" The stats of the {self.params.thirdProblem['shortname']}:<br>\n"
  784. f" {pst}\n"
  785. f" </span>\n"
  786. )
  787. out += f"</div>\n"
  788. print(out)
  789. print(f"<br clear='all'>\n")
  790. else:
  791. print(f"{lines[0]} {lines[1]} {lines[2]} -- {lines[3]}")
  792. print(f"{lines[4]}: {lines[5]}")
  793. if self.params.secProblem:
  794. print(f"- {lines[6]}: {lines[7]}")
  795. if self.params.thirdProblem:
  796. print(f"- - {lines[8]}: {lines[9]}")
  797. print()
  798. self.ship.print(html=html)
  799. def print_chars(self, html=False):
  800. if html:
  801. print(f"<div id='thekobolds'>\n")
  802. print(f"<span class='head'>The Kobolds</span>\n")
  803. else:
  804. print("The kobolds:")
  805. for k in self.characters:
  806. k.print(html=html)
  807. if html:
  808. print(f"</div>\n<br clear='all'>\n")
  809. def print_password(self, html=False):
  810. if self.masterpassword:
  811. pw = self.masterpassword
  812. else:
  813. pw = self.generate_key()
  814. if html:
  815. out = (
  816. f"<div id='passwordbox'>"
  817. f"<span class='passwordhead'>Permalink to this campaign:</span><br>"
  818. f"<span><a href='http://node.noelle.codes/kobold?pw={pw.replace(' ', '')}'>{pw}</a></span>"
  819. f"</div>"
  820. )
  821. print(out)
  822. print(f"<br clear='all'>\n")
  823. else:
  824. print(f"Password: {pw}")
  825. def decode(self, pwd):
  826. pass
  827. def lpad(s, n, c="0"):
  828. while len(s) < n:
  829. s = c + s
  830. return s
  831. if __name__ == "__main__":
  832. parser = argparse.ArgumentParser()
  833. group = parser.add_mutually_exclusive_group()
  834. group.add_argument("-c", "--campaign", help="print a full campaign block with N kobolds (default 6)", nargs="?", const=6, type=int, metavar="N")
  835. group.add_argument("-k", "--kobolds", help="print N kobolds", type=int, nargs="?", const=1, default=1, metavar="N")
  836. group.add_argument("-n", "--names", help="print N kobolds without stat blocks", nargs="?", const=1, type=int, metavar="N")
  837. group.add_argument("-p", "--params", help="print only the parameters of a campaign", action="store_true")
  838. group.add_argument("-s", "--ship", help="print only the ship name and description", action="store_true")
  839. group.add_argument("-pw", "--password", help="print the campaign defined by the submitted password", type=str, nargs="?", const=1, metavar="PW")
  840. parser.add_argument("--html", help="print in HTML instead of plain text", action="store_true")
  841. args = parser.parse_args()
  842. html = True if args.html else False
  843. if args.password:
  844. cmp = Campaign(fromPW = True, pw=args.password)
  845. cmp.print_params(html=html)
  846. cmp.print_chars(html=html)
  847. cmp.print_password(html=html)
  848. elif args.campaign:
  849. cmp = Campaign(args.campaign)
  850. cmp.print_params(html=html)
  851. cmp.print_chars(html=html)
  852. cmp.print_password(html=html)
  853. elif args.params:
  854. cmp = Campaign(makeChars=False)
  855. cmp.print_params(html=html)
  856. elif args.ship:
  857. ship = Ship()
  858. ship.print(html=html)
  859. elif args.names:
  860. for _ in range(args.names):
  861. c = Character()
  862. c.generate()
  863. c.print_name(html=html)
  864. else:
  865. for _ in range(args.kobolds):
  866. c = Character()
  867. c.generate()
  868. c.print(html=html)