A save state/password generator for the original Metroid.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  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. def createImage(self):
  788. pwd_chunks = self.password.split(" ") # This is safe because we'll never generate a password with a space in it
  789. pwd_lines = []
  790. pwd_lines.append(" ".join([pwd_chunks[0], pwd_chunks[1]]))
  791. pwd_lines.append(" ".join([pwd_chunks[2], pwd_chunks[3]]))
  792. newpwd = "\n".join(pwd_lines)
  793. fnt = ImageFont.truetype('narpasfw.ttf', size=18)
  794. img = Image.new('RGB', (300, 100))
  795. draw = ImageDraw.Draw(img)
  796. draw.text((50, 5), newpwd, font=fnt, fill=(190, 210, 255))
  797. filename = 'images/{}.png'.format("".join(pwd_chunks))
  798. img.save(filename)
  799. return filename
  800. class MetroidPoster(PineappleBot):
  801. @hourly(minute=30)
  802. def postPassword(self):
  803. gs = MetroidState()
  804. gs.initializeBitfield()
  805. gs.randomize()
  806. gs.createBitfield()
  807. gs.generatePassword()
  808. post_text = gs.toString()
  809. post_text += "\n\nvalid!"
  810. img = gs.createImage()
  811. media_id = self.mastodon.media_post(img, description=gs.password)
  812. tootdct = self.mastodon.status_post(post_text, visibility = "unlisted", spoiler_text = "Metroid password: {}".format(gs.password), media_ids = [media_id])
  813. print("Metroidgen scheduled: posted {}".format(gs.password))
  814. with open("mgen.log", "a") as f:
  815. f.write("\n")
  816. f.write(str(tootdct))
  817. def main():
  818. gs = MetroidState()
  819. gs.randomize()
  820. gs.createBitfield()
  821. gs.generatePassword()
  822. print(gs.toString())
  823. print(gs.createImage())
  824. if __name__ == "__main__":
  825. if len(sys.argv) == 2:
  826. gs = MetroidState()
  827. gs.decodePassword(sys.argv[1])
  828. else:
  829. main()