A save state/password generator for the original Metroid.
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.

metananas.py 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. # Metroid (NES) Random Password Generator
  2. # Author: Noëlle Anthony
  3. # License: MIT
  4. # Date: October 2019
  5. # This uses http://games.technoplaza.net/mpg/password.txt as a basis for its password algorithm
  6. import random, sys
  7. from ananas import PineappleBot, hourly
  8. from PIL import Image, ImageDraw, ImageFont
  9. class MetroidState:
  10. """ Stores the game state
  11. """
  12. def __init__(self):
  13. # Alphabet is 64 characters - 6 bits per character
  14. self.alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?-"
  15. # The password has different flags for "item available for pickup" and
  16. # "Samus has the item". I'm keeping them separate, but when the generator
  17. # selects an item as "picked up", that means Samus has it and it's not available.
  18. self.itemsCollected = {
  19. "Maru Mari": False,
  20. "Bombs": False,
  21. "Long Beam": False,
  22. "Ice Beam": False,
  23. "Wave Beam": False,
  24. "High Jump Boots": False,
  25. "Varia": False,
  26. "Screw Attack": False
  27. }
  28. self.samusHas = {
  29. "Maru Mari": False,
  30. "Bombs": False,
  31. "Long Beam": False,
  32. "Ice Beam": False,
  33. "Wave Beam": False,
  34. "High Jump Boots": False,
  35. "Varia": False,
  36. "Screw Attack": False
  37. }
  38. # Missile tanks are listed in the order in which they appear in the password,
  39. # NOT in zone order or in any reasonable collection order.
  40. self.missileTanks = {
  41. 1: False,
  42. 2: False,
  43. 3: False,
  44. 4: False,
  45. 5: False,
  46. 6: False,
  47. 7: False,
  48. 8: False,
  49. 9: False,
  50. 10: False,
  51. 11: False,
  52. 12: False,
  53. 13: False,
  54. 14: False,
  55. 15: False,
  56. 16: False,
  57. 17: False,
  58. 18: False,
  59. 19: False,
  60. 20: False,
  61. 21: False
  62. }
  63. # Likewise energy tanks are listed in password order.
  64. self.energyTanks = {
  65. 1: False,
  66. 2: False,
  67. 3: False,
  68. 4: False,
  69. 5: False,
  70. 6: False,
  71. 7: False,
  72. 8: False
  73. }
  74. # This may be left-to-right (Samus approaches from the right). I haven't checked.
  75. self.zebetitesDestroyed = {
  76. 1: False,
  77. 2: False,
  78. 3: False,
  79. 4: False,
  80. 5: False
  81. }
  82. # I'm not sure why I decided to segregate these by zone, except that that's how
  83. # truepeacein.space does it.
  84. self.doors = {
  85. "Brinstar": {
  86. 1: False,
  87. 2: False,
  88. 3: False,
  89. 4: False,
  90. 5: False
  91. }, "Norfair": {
  92. 1: False,
  93. 2: False,
  94. 3: False,
  95. 4: False
  96. }, "Kraid": {
  97. 1: False,
  98. 2: False,
  99. 3: False,
  100. 4: False,
  101. 5: False
  102. }, "Ridley": {
  103. 1: False,
  104. 2: False
  105. }, "Tourian": {
  106. 1: False,
  107. 2: False,
  108. 3: False
  109. }
  110. }
  111. # The next three are self-explanatory.
  112. self.kraidKilled = False
  113. self.ridleyKilled = False
  114. self.motherBrainKilled = False
  115. # The Kraid and Ridley statues rise when Kraid and Ridley are killed, but
  116. # their states are stored separately in the password. It's possible to
  117. # raise them without killing the bosses, granting early access to Tourian.
  118. self.kraidStatue = False
  119. self.ridleyStatue = False
  120. # Is Samus wearing her armor (False) or her swimsuit (True)?
  121. self.swimsuit = False
  122. # 0-255. You can have more missiles than 5*collected tanks (in fact, you
  123. # can only collect 21 tanks - thus 105 missiles - but can have up to 255
  124. # missiles in your inventory).
  125. self.missileCount = 0
  126. # How advanced is the game clock? After 3 hours you don't get the good ending.
  127. self.gameAge = 0
  128. # There are five possible start locations: Brinstar, where you start, and
  129. # at the bottom of the elevator where you enter each subsequent zone.
  130. self.locations = ["Brinstar", "Norfair", "Kraid's Lair", "Ridley's Lair", "Tourian"]
  131. self.startLocation = 0
  132. # Arrays to store the 144 bits that compose the password
  133. self.bitfield = []
  134. self.initializeBitfield()
  135. self.fullbitfield = []
  136. def initializeBitfield(self):
  137. """ Set the first 128 bits of the bitfield to 0.
  138. The remaining 16 bits will be set later.
  139. """
  140. self.bitfield = []
  141. for _ in range(128):
  142. self.bitfield.append(0)
  143. def toggleItem(self, itm):
  144. """ Mark an item as collected or uncollected.
  145. """
  146. if itm in self.itemsCollected.keys():
  147. self.itemsCollected[itm] = not self.itemsCollected[itm]
  148. self.samusHas[itm] = not self.samusHas[itm]
  149. else:
  150. print("Couldn't find item: {}".format(str(itm)))
  151. def toggleMissileTank(self, num):
  152. """ Mark a missile tank as collected or uncollected.
  153. """
  154. try:
  155. num = int(num)
  156. except:
  157. print("{} is not a number".format(num))
  158. return
  159. if num in self.missileTanks.keys():
  160. self.missileTanks[num] = not self.missileTanks[num]
  161. else:
  162. print("Couldn't find missile tank: {}".format(num))
  163. def toggleEnergyTank(self, num):
  164. """ Mark an energy tank as collected or uncollected.
  165. """
  166. try:
  167. num = int(num)
  168. except:
  169. print("{} is not a number".format(num))
  170. return
  171. if num in self.energyTanks.keys():
  172. self.energyTanks[num] = not self.energyTanks[num]
  173. else:
  174. print("Couldn't find energy tank: {}".format(num))
  175. def toggleZebetite(self, num):
  176. """ Mark a Zebetite stem as destroyed or intact.
  177. """
  178. try:
  179. num = int(num)
  180. except:
  181. print("{} is not a number".format(num))
  182. return
  183. if num in self.zebetitesDestroyed.keys():
  184. self.zebetitesDestroyed[num] = not self.zebetitesDestroyed[num]
  185. else:
  186. print("Couldn't find Zebetite: {}".format(num))
  187. def toggleKraid(self):
  188. """ Mark Kraid as killed or alive.
  189. """
  190. self.kraidKilled = not self.kraidKilled
  191. self.kraidStatue = self.kraidKilled
  192. def toggleKraidStatue(self):
  193. """ Mark Kraid's statue as raised or lowered.
  194. If Kraid is killed but his statue isn't raised, you can't complete the game.
  195. """
  196. self.kraidStatue = not self.kraidStatue
  197. if self.kraidKilled and not self.kraidStatue:
  198. print("WARNING: Kraid has been killed but his statue has not been raised.")
  199. def toggleRidley(self):
  200. """ Mark Ridley as killed or alive.
  201. """
  202. self.ridleyKilled = not self.ridleyKilled
  203. self.ridleyStatue = self.ridleyKilled
  204. def toggleRidleyStatue(self):
  205. """ Mark Ridley's statue as raised or lowered.
  206. If Ridley is killed but his statue isn't raised, you can't complete the game.
  207. """
  208. self.ridleyStatue = not self.ridleyStatue
  209. if self.ridleyKilled and not self.ridleyStatue:
  210. print("WARNING: Ridley has been killed but his statue has not been raised.")
  211. def toggleMotherBrain(self):
  212. """ Mark Mother Brain as killed or alive.
  213. If Mother Brain is marked as killed, the self-destruct timer won't go off
  214. when you reach her room.
  215. """
  216. self.motherBrainKilled = not self.motherBrainKilled
  217. def toggleDoor(self, area, door):
  218. """ Mark a given red/yellow door as opened or locked.
  219. """
  220. try:
  221. area = str(area)
  222. door = int(door)
  223. except:
  224. print("Area must be string, door must be a positive integer")
  225. return
  226. if area in self.doors.keys() and int(door) in self.doors[area].keys():
  227. self.doors[area][door] = not self.doors[area][door]
  228. else:
  229. print("Couldn't find door {} in area {}".format(door, area))
  230. def toggleSwimsuit(self):
  231. """ Determine whether or not Samus is wearing her armor.
  232. """
  233. self.swimsuit = not self.swimsuit
  234. def newLocation(self, loc):
  235. """ Set a new starting location (0-4).
  236. """
  237. try:
  238. loc = str(loc)
  239. except:
  240. print("Location must be a string")
  241. return
  242. if loc in self.locations:
  243. self.startLocation = self.locations.index(loc)
  244. else:
  245. print("Couldn't find location: {}".format(loc))
  246. def collectedItems(self):
  247. """ List which items have been collected.
  248. Under this generator's rules, if Samus doesn't have an item,
  249. it's available to be picked up.
  250. """
  251. o = []
  252. for k,v in self.itemsCollected.items():
  253. if v == True:
  254. o.append(k)
  255. if len(o) == 0:
  256. return "None"
  257. else:
  258. return ", ".join(o)
  259. def collectedMissiles(self):
  260. """ List which missile tanks have been collected.
  261. """
  262. o = []
  263. for k, v in self.missileTanks.items():
  264. if v == True:
  265. o.append(k)
  266. if len(o) == 0:
  267. return "None"
  268. else:
  269. return ", ".join([str(b) for b in o])
  270. def collectedEtanks(self):
  271. """ List which energy tanks have been collected.
  272. """
  273. o = []
  274. for k, v in self.energyTanks.items():
  275. if v == True:
  276. o.append(k)
  277. if len(o) == 0:
  278. return "None"
  279. else:
  280. return ", ".join([str(b) for b in o])
  281. def killedZebetites(self):
  282. """ List which Zebetite stems have been destroyed.
  283. """
  284. o = []
  285. for k, v in self.zebetitesDestroyed.items():
  286. if v == True:
  287. o.append(k)
  288. if len(o) == 0:
  289. return "None"
  290. else:
  291. return ", ".join([str(b) for b in o])
  292. def killedBosses(self):
  293. """ List which bosses have been killed.
  294. """
  295. o = []
  296. if self.kraidKilled:
  297. o.append("Kraid")
  298. if self.ridleyKilled:
  299. o.append("Ridley")
  300. if self.motherBrainKilled:
  301. o.append("Mother Brain")
  302. if len(o) == 0:
  303. return "None"
  304. else:
  305. return ", ".join(o)
  306. def raisedStatues(self):
  307. """ List which statues have been raised.
  308. """
  309. o = []
  310. if self.kraidStatue:
  311. o.append("Kraid")
  312. if self.ridleyStatue:
  313. o.append("Ridley")
  314. if len(o) == 0:
  315. return "None"
  316. else:
  317. return ", ".join(o)
  318. def inBailey(self):
  319. """ Show whether Samus is in her swimsuit or not.
  320. 'inBailey' refers to an old (false) explanation of the JUSTIN BAILEY
  321. password, in which a 'bailey' was English slang for a bathing suit,
  322. so with that password, Samus was "Just In Bailey" - i.e. in her swimsuit.
  323. """
  324. if self.swimsuit:
  325. return "Yes"
  326. else:
  327. return "No"
  328. def openedDoors(self):
  329. """ List which red/yellow doors have been unlocked.
  330. """
  331. d = {"Brinstar": 0, "Norfair": 0, "Kraid": 0, "Ridley": 0, "Tourian": 0}
  332. o = []
  333. for k,v in self.doors["Brinstar"].items():
  334. if v == True:
  335. d["Brinstar"] = d["Brinstar"] + 1
  336. for k,v in self.doors["Norfair"].items():
  337. if v == True:
  338. d["Norfair"] = d["Norfair"] + 1
  339. for k,v in self.doors["Kraid"].items():
  340. if v == True:
  341. d["Kraid"] = d["Kraid"] + 1
  342. for k,v in self.doors["Ridley"].items():
  343. if v == True:
  344. d["Ridley"] = d["Ridley"] + 1
  345. for k,v in self.doors["Tourian"].items():
  346. if v == True:
  347. d["Tourian"] = d["Tourian"] + 1
  348. for k, v in d.items():
  349. o.append("{} {}".format(k, v))
  350. return ", ".join(o)
  351. def getBits(self):
  352. """ Return the bitfield in an easily-readable format.
  353. """
  354. o = []
  355. word = []
  356. for i in range(128):
  357. word.append(str(self.bitfield[i]))
  358. if len(word) == 8:
  359. o.append("".join(word))
  360. word = []
  361. o1 = " ".join(o[:8])
  362. o2 = " ".join(o[8:])
  363. return o1 + "\n" + o2
  364. def toString(self):
  365. """ Output the game state as a newline-delimited string.
  366. """
  367. ic = "Items Collected: {}".format(self.collectedItems())
  368. mt = "Missile Tanks Collected: {}".format(self.collectedMissiles())
  369. et = "Energy Tanks Collected: {}".format(self.collectedEtanks())
  370. zb = "Zebetites Killed: {}".format(self.killedZebetites())
  371. kb = "Bosses Killed: {}".format(self.killedBosses())
  372. rs = "Statues Raised: {}".format(self.raisedStatues())
  373. sw = "Swimsuit?: {}".format(self.inBailey())
  374. sl = "Start Location: {}".format(self.locations[self.startLocation])
  375. dr = "Unlocked Doors: {}".format(self.openedDoors())
  376. ms = "Missiles: {}".format(self.missileCount)
  377. pw = "Password: {}".format(self.password)
  378. return "\n".join([ic, mt, et, zb, kb, rs, sw, sl, dr, ms, pw])
  379. def randomize(self):
  380. """ The randomizer!
  381. """
  382. # Items
  383. if random.randint(0,1) == 1:
  384. self.toggleItem("Maru Mari")
  385. if random.randint(0,1) == 1:
  386. self.toggleItem("Bombs")
  387. if random.randint(0,1) == 1:
  388. self.toggleItem("Varia")
  389. if random.randint(0,1) == 1:
  390. self.toggleItem("High Jump Boots")
  391. if random.randint(0,1) == 1:
  392. self.toggleItem("Screw Attack")
  393. if random.randint(0,1) == 1:
  394. self.toggleItem("Long Beam")
  395. beam = random.randint(0,2)
  396. if beam == 1:
  397. self.toggleItem("Ice Beam")
  398. elif beam == 2:
  399. self.toggleItem("Wave Beam")
  400. # Missile Tanks
  401. for i in range(21):
  402. if random.randint(0,1) == 1:
  403. self.toggleMissileTank(i+1)
  404. # Energy Tanks
  405. for i in range(8):
  406. if random.randint(0,1) == 1:
  407. self.toggleEnergyTank(i+1)
  408. # Zebetites
  409. for i in range(5):
  410. if random.randint(0,1) == 1:
  411. self.toggleZebetite(i+1)
  412. # Bosses killed
  413. if random.randint(0,1) == 1:
  414. self.toggleKraid()
  415. if random.randint(0,1) == 1:
  416. self.toggleRidley()
  417. if random.randint(0,1) == 1:
  418. self.toggleMotherBrain()
  419. # Statues raised
  420. if not self.kraidKilled and random.randint(0,1) == 1:
  421. self.toggleKraidStatue()
  422. if not self.ridleyKilled and random.randint(0,1) == 1:
  423. self.toggleRidleyStatue()
  424. # Doors
  425. # Brinstar 5, Norfair 4, Kraid 5, Ridley 2, Tourian 3
  426. for i in range(5):
  427. if random.randint(0,1) == 1:
  428. self.doors["Brinstar"][i+1] = True
  429. for i in range(4):
  430. if random.randint(0,1) == 1:
  431. self.doors["Norfair"][i+1] = True
  432. for i in range(5):
  433. if random.randint(0,1) == 1:
  434. self.doors["Kraid"][i+1] = True
  435. for i in range(2):
  436. if random.randint(0,1) == 1:
  437. self.doors["Ridley"][i+1] = True
  438. for i in range(3):
  439. if random.randint(0,1) == 1:
  440. self.doors["Tourian"][i+1] = True
  441. # Swimsuit
  442. # Samus has a 1/3 chance of spawning in her swimsuit.
  443. # There's no technical reason for this, just a personal choice.
  444. if random.randint(0,2) == 2:
  445. self.toggleSwimsuit()
  446. # Start Location
  447. self.startLocation = random.randint(0,4)
  448. self.missileCount = random.randint(0,255)
  449. def createBitfield(self):
  450. """ Create the 144-bit field from the game state
  451. that will generate the password.
  452. """
  453. self.initializeBitfield()
  454. # Doing this in order, which is dumb and tedious but accurate.
  455. if self.itemsCollected["Maru Mari"]:
  456. self.bitfield[0] = 1
  457. if self.missileTanks[1]:
  458. self.bitfield[1] = 1
  459. if self.doors["Brinstar"][1]:
  460. self.bitfield[2] = 1
  461. if self.doors["Brinstar"][2]:
  462. self.bitfield[3] = 1
  463. if self.energyTanks[1]:
  464. self.bitfield[4] = 1
  465. if self.doors["Brinstar"][3]:
  466. self.bitfield[5] = 1
  467. if self.itemsCollected["Bombs"]:
  468. self.bitfield[6] = 1
  469. if self.doors["Brinstar"][4]:
  470. self.bitfield[7] = 1
  471. if self.missileTanks[2]:
  472. self.bitfield[8] = 1
  473. if self.energyTanks[2]:
  474. self.bitfield[9] = 1
  475. if self.doors["Brinstar"][5]:
  476. self.bitfield[10] = 1
  477. if self.itemsCollected["Varia"]:
  478. self.bitfield[11] = 1
  479. if self.energyTanks[3]:
  480. self.bitfield[12] = 1
  481. if self.missileTanks[3]:
  482. self.bitfield[13] = 1
  483. if self.missileTanks[4]:
  484. self.bitfield[14] = 1
  485. if self.doors["Norfair"][1]:
  486. self.bitfield[15] = 1
  487. if self.missileTanks[5]:
  488. self.bitfield[16] = 1
  489. if self.missileTanks[6]:
  490. self.bitfield[17] = 1
  491. if self.missileTanks[7]:
  492. self.bitfield[18] = 1
  493. if self.missileTanks[8]:
  494. self.bitfield[19] = 1
  495. if self.missileTanks[9]:
  496. self.bitfield[20] = 1
  497. if self.missileTanks[10]:
  498. self.bitfield[21] = 1
  499. if self.missileTanks[11]:
  500. self.bitfield[22] = 1
  501. if self.doors["Norfair"][2]:
  502. self.bitfield[23] = 1
  503. if self.itemsCollected["High Jump Boots"]:
  504. self.bitfield[24] = 1
  505. if self.doors["Norfair"][3]:
  506. self.bitfield[25] = 1
  507. if self.itemsCollected["Screw Attack"]:
  508. self.bitfield[26] = 1
  509. if self.missileTanks[12]:
  510. self.bitfield[27] = 1
  511. if self.missileTanks[13]:
  512. self.bitfield[28] = 1
  513. if self.doors["Norfair"][4]:
  514. self.bitfield[29] = 1
  515. if self.energyTanks[4]:
  516. self.bitfield[30] = 1
  517. if self.missileTanks[14]:
  518. self.bitfield[31] = 1
  519. if self.doors["Kraid"][1]:
  520. self.bitfield[32] = 1
  521. if self.missileTanks[15]:
  522. self.bitfield[33] = 1
  523. if self.missileTanks[16]:
  524. self.bitfield[34] = 1
  525. if self.doors["Kraid"][2]:
  526. self.bitfield[35] = 1
  527. if self.energyTanks[5]:
  528. self.bitfield[36] = 1
  529. if self.doors["Kraid"][3]:
  530. self.bitfield[37] = 1
  531. if self.doors["Kraid"][4]:
  532. self.bitfield[38] = 1
  533. if self.missileTanks[17]:
  534. self.bitfield[39] = 1
  535. if self.missileTanks[18]:
  536. self.bitfield[40] = 1
  537. if self.doors["Kraid"][5]:
  538. self.bitfield[41] = 1
  539. if self.energyTanks[6]:
  540. self.bitfield[42] = 1
  541. if self.missileTanks[19]:
  542. self.bitfield[43] = 1
  543. if self.doors["Ridley"][1]:
  544. self.bitfield[44] = 1
  545. if self.energyTanks[7]:
  546. self.bitfield[45] = 1
  547. if self.missileTanks[20]:
  548. self.bitfield[46] = 1
  549. if self.doors["Ridley"][2]:
  550. self.bitfield[47] = 1
  551. if self.energyTanks[8]:
  552. self.bitfield[48] = 1
  553. if self.missileTanks[21]:
  554. self.bitfield[49] = 1
  555. if self.doors["Tourian"][1]:
  556. self.bitfield[50] = 1
  557. if self.doors["Tourian"][2]:
  558. self.bitfield[51] = 1
  559. if self.doors["Tourian"][3]:
  560. self.bitfield[52] = 1
  561. if self.zebetitesDestroyed[1]:
  562. self.bitfield[53] = 1
  563. if self.zebetitesDestroyed[2]:
  564. self.bitfield[54] = 1
  565. if self.zebetitesDestroyed[3]:
  566. self.bitfield[55] = 1
  567. if self.zebetitesDestroyed[4]:
  568. self.bitfield[56] = 1
  569. if self.zebetitesDestroyed[5]:
  570. self.bitfield[57] = 1
  571. if self.motherBrainKilled:
  572. self.bitfield[58] = 1
  573. # 59-63 unknown
  574. # not 64, 65, or 66 = Brinstar
  575. # 64 = Norfair
  576. # 65 and not 66 = Kraid's Lair
  577. # 66 and not 65 = Ridley's Lair
  578. # 65 and 66 = Tourian
  579. if self.startLocation == 1:
  580. self.bitfield[64] = 1
  581. if self.startLocation == 2 or self.startLocation == 4:
  582. self.bitfield[65] = 1
  583. if self.startLocation == 3 or self.startLocation == 4:
  584. self.bitfield[66] = 1
  585. # 67 is the reset bit, I want all passwords to be valid
  586. # if self.:
  587. # self.bitfield[67] = 1
  588. # 68-70 are unknown
  589. if self.swimsuit:
  590. self.bitfield[71] = 1
  591. if self.samusHas["Bombs"]:
  592. self.bitfield[72] = 1
  593. if self.samusHas["High Jump Boots"]:
  594. self.bitfield[73] = 1
  595. if self.samusHas["Long Beam"]:
  596. self.bitfield[74] = 1
  597. if self.samusHas["Screw Attack"]:
  598. self.bitfield[75] = 1
  599. if self.samusHas["Maru Mari"]:
  600. self.bitfield[76] = 1
  601. if self.samusHas["Varia"]:
  602. self.bitfield[77] = 1
  603. if self.samusHas["Wave Beam"]:
  604. self.bitfield[78] = 1
  605. if self.samusHas["Ice Beam"]:
  606. self.bitfield[79] = 1
  607. # Missile count
  608. # +2^n from 0 to 7
  609. binMissiles = bin(self.missileCount).replace('0b','')[::-1]
  610. while len(binMissiles) < 8:
  611. binMissiles += "0"
  612. if int(binMissiles[0]) == 1:
  613. self.bitfield[80] = 1
  614. if int(binMissiles[1]) == 1:
  615. self.bitfield[81] = 1
  616. if int(binMissiles[2]) == 1:
  617. self.bitfield[82] = 1
  618. if int(binMissiles[3]) == 1:
  619. self.bitfield[83] = 1
  620. if int(binMissiles[4]) == 1:
  621. self.bitfield[84] = 1
  622. if int(binMissiles[5]) == 1:
  623. self.bitfield[85] = 1
  624. if int(binMissiles[6]) == 1:
  625. self.bitfield[86] = 1
  626. if int(binMissiles[7]) == 1:
  627. self.bitfield[87] = 1
  628. # 88-119 are game age, let's randomize
  629. if random.randint(0,1) == 1:
  630. self.bitfield[88] = 1
  631. if random.randint(0,1) == 1:
  632. self.bitfield[89] = 1
  633. if random.randint(0,1) == 1:
  634. self.bitfield[90] = 1
  635. if random.randint(0,1) == 1:
  636. self.bitfield[91] = 1
  637. if random.randint(0,1) == 1:
  638. self.bitfield[92] = 1
  639. if random.randint(0,1) == 1:
  640. self.bitfield[93] = 1
  641. if random.randint(0,1) == 1:
  642. self.bitfield[94] = 1
  643. if random.randint(0,1) == 1:
  644. self.bitfield[95] = 1
  645. if random.randint(0,1) == 1:
  646. self.bitfield[96] = 1
  647. if random.randint(0,1) == 1:
  648. self.bitfield[97] = 1
  649. if random.randint(0,1) == 1:
  650. self.bitfield[98] = 1
  651. if random.randint(0,1) == 1:
  652. self.bitfield[99] = 1
  653. if random.randint(0,1) == 1:
  654. self.bitfield[100] = 1
  655. if random.randint(0,1) == 1:
  656. self.bitfield[101] = 1
  657. if random.randint(0,1) == 1:
  658. self.bitfield[102] = 1
  659. if random.randint(0,1) == 1:
  660. self.bitfield[103] = 1
  661. if random.randint(0,1) == 1:
  662. self.bitfield[104] = 1
  663. if random.randint(0,1) == 1:
  664. self.bitfield[105] = 1
  665. if random.randint(0,1) == 1:
  666. self.bitfield[106] = 1
  667. if random.randint(0,1) == 1:
  668. self.bitfield[107] = 1
  669. if random.randint(0,1) == 1:
  670. self.bitfield[108] = 1
  671. if random.randint(0,1) == 1:
  672. self.bitfield[109] = 1
  673. if random.randint(0,1) == 1:
  674. self.bitfield[110] = 1
  675. if random.randint(0,1) == 1:
  676. self.bitfield[111] = 1
  677. if random.randint(0,1) == 1:
  678. self.bitfield[112] = 1
  679. if random.randint(0,1) == 1:
  680. self.bitfield[113] = 1
  681. if random.randint(0,1) == 1:
  682. self.bitfield[114] = 1
  683. if random.randint(0,1) == 1:
  684. self.bitfield[115] = 1
  685. if random.randint(0,1) == 1:
  686. self.bitfield[116] = 1
  687. if random.randint(0,1) == 1:
  688. self.bitfield[117] = 1
  689. if random.randint(0,1) == 1:
  690. self.bitfield[118] = 1
  691. if random.randint(0,1) == 1:
  692. self.bitfield[119] = 1
  693. # 120-123 are unknown
  694. # I have no idea why these are at the end. I wonder if Kraid and
  695. # Ridley were relatively late additions to the game? (Or maybe
  696. # they were just implemented late in the game's development.)
  697. if self.ridleyKilled:
  698. self.bitfield[124] = 1
  699. if self.ridleyStatue:
  700. self.bitfield[125] = 1
  701. if self.kraidKilled:
  702. self.bitfield[126] = 1
  703. if self.kraidStatue:
  704. self.bitfield[127] = 1
  705. def generatePassword(self):
  706. """ Generate the password from the bitfield.
  707. This is a five-step process.
  708. 1) Reverse the order of each 8-bit byte to make it little-endian.
  709. 2) Cycle the entire bitfield 0-7 bits to the right.
  710. Append the number of shifts in binary to the end - again, little-endian.
  711. 3) Create the checksum by turning each byte into a decimal number,
  712. summing them, converting the sum *back* to binary, and taking the lowest
  713. 8 bits of that binary sum and adding it - BIG-ENDIAN - to the end.
  714. 4) Separate the bitstream into ***6-bit*** chunks and create a decimal
  715. number from each chunk (0-63).
  716. 5) Associate each decimal number with a letter from the Metroid Alphabet
  717. (listed at the top of __init__()).
  718. I'm not doing step 2 yet, which is equivalent to shifting 0 places
  719. and making the shift byte 00000000.
  720. """
  721. # not gonna do the bit-shifting yet
  722. bitfield = self.bitfield
  723. bitfield = bitfield + [0,0,0,0,0,0,0,0] # add the zero shift byte
  724. self.fullbitfield = "".join([str(x) for x in bitfield])
  725. newBitfield = []
  726. for i in range(17):
  727. j = i * 8
  728. k = j + 8
  729. word = self.fullbitfield[j:k][::-1] # I thought [j:k:-1] should work but it doesn't
  730. newBitfield.append(word)
  731. decChecksum = sum([int(x, 2) for x in newBitfield])
  732. bitfield = "".join(newBitfield)
  733. binChecksum = bin(decChecksum).replace('0b','')
  734. checksum = binChecksum[-8:]
  735. while len(checksum) < 8:
  736. checksum = "0" + checksum
  737. for bit in checksum:
  738. bitfield += bit
  739. letters = []
  740. letter = []
  741. for bit in bitfield:
  742. letter.append(bit)
  743. if len(letter) == 6:
  744. letters.append(self.alphabet[int("".join(letter),2)])
  745. letter = []
  746. words = []
  747. word = []
  748. for lt in letters:
  749. word.append(lt)
  750. if len(word) == 6:
  751. words.append("".join(word))
  752. word = []
  753. words.append("".join(word))
  754. self.password = " ".join(words)
  755. return self.password
  756. def decodePassword(self, pwd):
  757. """ Sanity checking! This function decodes an input password back into a bitfield,
  758. so that you can check that it was properly encoded.
  759. Q: Why doesn't this display the game state?
  760. A: I trust that https://www.truepeacein.space properly encodes the game state.
  761. So when I create a game state with the randomizer, I can recreate that
  762. game state at TPIS and use the password generates as its input, to check
  763. against my randomized game password. In other words, this is a testing
  764. function, and in the intended use case I'll know what the input bitfield is
  765. and be able to check against it.
  766. """
  767. densePwd = pwd.replace(" ","")
  768. numPwd = []
  769. for chr in densePwd:
  770. numPwd.append(self.alphabet.index(chr))
  771. bitPwd = [bin(x).replace("0b","") for x in numPwd]
  772. longBitPwd = []
  773. for word in bitPwd:
  774. longword = word
  775. while len(longword) < 6:
  776. longword = "0" + longword
  777. longBitPwd.append(longword)
  778. newBitfield = "".join(longBitPwd)
  779. csm = sum([int(x) for x in newBitfield[:136]])
  780. print(csm)
  781. for i in range(len(newBitfield)):
  782. print(newBitfield[i], end="")
  783. if i%8 == 7:
  784. print(" ", end="")
  785. if i%64 == 63:
  786. print()
  787. class MetroidPoster(PineappleBot):
  788. @hourly(minute=50)
  789. def postPassword(self):
  790. gs = MetroidState()
  791. gs.initializeBitfield()
  792. gs.randomize()
  793. gs.createBitfield()
  794. gs.generatePassword()
  795. post_text = gs.toString()
  796. img = self.createImage(gs.password)
  797. media_id = self.mastodon.media_post(img, description=gs.password)
  798. tootdct = self.mastodon.status_post(post_text, visibility = "unlisted", spoiler_text = "Metroid password: {}".format(gs.password), media_ids = [media_id])
  799. print("Metroidgen scheduled: posted {}".format(gs.password))
  800. with open("metroidgen.log", "a") as f:
  801. f.write("\n")
  802. f.write(tootdct)
  803. def createImage(self, pwd):
  804. pwd_chunks = pwd.split(" ") # This is safe because we'll never generate a password with a space in it
  805. pwd_lines = []
  806. pwd_lines.append(" ".join([pwd_chunks[0], pwd_chunks[1]]))
  807. pwd_lines.append(" ".join([pwd_chunks[2], pwd_chunks[3]]))
  808. newpwd = "\n".join(pwd_lines)
  809. fnt = ImageFont.truetype('narpasfw.ttf', size=18)
  810. img = Image.new('RGB', (300, 100))
  811. draw = ImageDraw.Draw(img)
  812. draw.text((50, 35), newpwd, font=fnt, fill=(190, 210, 255))
  813. filename = 'images/{}.png'.format("".join(pwd_chunks))
  814. img.save(filename)
  815. return filename