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.

metroidgen.py 29KB

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