Browse Source

Clean up and add documentation

master
Noëlle Anthony 5 years ago
parent
commit
2963b92cc4
1 changed files with 123 additions and 113 deletions
  1. 123
    113
      metroidgen.py

+ 123
- 113
metroidgen.py View File

# Metroid (NES) Random Password Generator
# Author: Noëlle Anthony
# License: MIT
# Date: October 2019
# This uses http://games.technoplaza.net/mpg/password.txt as a basis for its password algorithm # This uses http://games.technoplaza.net/mpg/password.txt as a basis for its password algorithm


import random, sys import random, sys


class MetroidState: class MetroidState:
""" Stores the game state
"""
def __init__(self): def __init__(self):
# Alphabet is 64 characters - 6 bits per character
self.alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?-" self.alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?-"
# The password has different flags for "item available for pickup" and
# "Samus has the item". I'm keeping them separate, but when the generator
# selects an item as "picked up", that means Samus has it and it's not available.
self.itemsCollected = { self.itemsCollected = {
"Maru Mari": False, "Maru Mari": False,
"Bombs": False, "Bombs": False,
"Varia": False, "Varia": False,
"Screw Attack": False "Screw Attack": False
} }
# Missile tanks are listed in the order in which they appear in the password,
# NOT in zone order or in any reasonable collection order.
self.missileTanks = { self.missileTanks = {
1: False, 1: False,
2: False, 2: False,
20: False, 20: False,
21: False 21: False
} }
# Likewise energy tanks are listed in password order.
self.energyTanks = { self.energyTanks = {
1: False, 1: False,
2: False, 2: False,
7: False, 7: False,
8: False 8: False
} }
# This may be left-to-right (Samus approaches from the right). I haven't checked.
self.zebetitesDestroyed = { self.zebetitesDestroyed = {
1: False, 1: False,
2: False, 2: False,
4: False, 4: False,
5: False 5: False
} }
# I'm not sure why I decided to segregate these by zone, except that that's how
# truepeacein.space does it.
self.doors = { self.doors = {
"Brinstar": { "Brinstar": {
1: False, 1: False,
3: False 3: False
} }
} }
# The next three are self-explanatory.
self.kraidKilled = False self.kraidKilled = False
self.ridleyKilled = False self.ridleyKilled = False
self.motherBrainKilled = False self.motherBrainKilled = False
# The Kraid and Ridley statues rise when Kraid and Ridley are killed, but
# their states are stored separately in the password. It's possible to
# raise them without killing the bosses, granting early access to Tourian.
self.kraidStatue = False self.kraidStatue = False
self.ridleyStatue = False self.ridleyStatue = False
# Is Samus wearing her armor (False) or her swimsuit (True)?
self.swimsuit = False self.swimsuit = False
# 0-255. You can have more missiles than 5*collected tanks (in fact, you
# can only collect 21 tanks - thus 105 missiles - but can have up to 255
# missiles in your inventory).
self.missileCount = 0 self.missileCount = 0
# How advanced is the game clock? After 3 hours you don't get the good ending.
self.gameAge = 0 self.gameAge = 0
# There are five possible start locations: Brinstar, where you start, and
# at the bottom of the elevator where you enter each subsequent zone.
self.locations = ["Brinstar", "Norfair", "Kraid's Lair", "Ridley's Lair", "Tourian"] self.locations = ["Brinstar", "Norfair", "Kraid's Lair", "Ridley's Lair", "Tourian"]
self.startLocation = 0 self.startLocation = 0
# Arrays to store the 144 bits that compose the password
self.bitfield = [] self.bitfield = []
self.initializeBitfield() self.initializeBitfield()
self.fullbitfield = [] self.fullbitfield = []


def initializeBitfield(self): def initializeBitfield(self):
""" Set the first 128 bits of the bitfield to 0.
The remaining 16 bits will be set later.
"""
self.bitfield = [] self.bitfield = []
for _ in range(128): for _ in range(128):
self.bitfield.append(0) self.bitfield.append(0)


def toggleItem(self, itm): def toggleItem(self, itm):
""" Mark an item as collected or uncollected.
"""
if itm in self.itemsCollected.keys(): if itm in self.itemsCollected.keys():
self.itemsCollected[itm] = not self.itemsCollected[itm] self.itemsCollected[itm] = not self.itemsCollected[itm]
self.samusHas[itm] = not self.samusHas[itm] self.samusHas[itm] = not self.samusHas[itm]
print("Couldn't find item: {}".format(str(itm))) print("Couldn't find item: {}".format(str(itm)))
def toggleMissileTank(self, num): def toggleMissileTank(self, num):
""" Mark a missile tank as collected or uncollected.
"""
try: try:
num = int(num) num = int(num)
except: except:
print("Couldn't find missile tank: {}".format(num)) print("Couldn't find missile tank: {}".format(num))
def toggleEnergyTank(self, num): def toggleEnergyTank(self, num):
""" Mark an energy tank as collected or uncollected.
"""
try: try:
num = int(num) num = int(num)
except: except:
print("Couldn't find energy tank: {}".format(num)) print("Couldn't find energy tank: {}".format(num))
def toggleZebetite(self, num): def toggleZebetite(self, num):
""" Mark a Zebetite stem as destroyed or intact.
"""
try: try:
num = int(num) num = int(num)
except: except:
print("Couldn't find Zebetite: {}".format(num)) print("Couldn't find Zebetite: {}".format(num))


def toggleKraid(self): def toggleKraid(self):
""" Mark Kraid as killed or alive.
"""
self.kraidKilled = not self.kraidKilled self.kraidKilled = not self.kraidKilled
self.kraidStatue = self.kraidKilled self.kraidStatue = self.kraidKilled


def toggleKraidStatue(self): def toggleKraidStatue(self):
""" Mark Kraid's statue as raised or lowered.
If Kraid is killed but his statue isn't raised, you can't complete the game.
"""
self.kraidStatue = not self.kraidStatue self.kraidStatue = not self.kraidStatue
if self.kraidKilled and not self.kraidStatue: if self.kraidKilled and not self.kraidStatue:
print("WARNING: Kraid has been killed but his statue has not been raised.") print("WARNING: Kraid has been killed but his statue has not been raised.")


def toggleRidley(self): def toggleRidley(self):
""" Mark Ridley as killed or alive.
"""
self.ridleyKilled = not self.ridleyKilled self.ridleyKilled = not self.ridleyKilled
self.ridleyStatue = self.ridleyKilled self.ridleyStatue = self.ridleyKilled


def toggleRidleyStatue(self): def toggleRidleyStatue(self):
""" Mark Ridley's statue as raised or lowered.
If Ridley is killed but his statue isn't raised, you can't complete the game.
"""
self.ridleyStatue = not self.ridleyStatue self.ridleyStatue = not self.ridleyStatue
if self.ridleyKilled and not self.ridleyStatue: if self.ridleyKilled and not self.ridleyStatue:
print("WARNING: Ridley has been killed but his statue has not been raised.") print("WARNING: Ridley has been killed but his statue has not been raised.")
def toggleMotherBrain(self): def toggleMotherBrain(self):
""" Mark Mother Brain as killed or alive.
If Mother Brain is marked as killed, the self-destruct timer won't go off
when you reach her room.
"""
self.motherBrainKilled = not self.motherBrainKilled self.motherBrainKilled = not self.motherBrainKilled


def toggleDoor(self, area, door): def toggleDoor(self, area, door):
""" Mark a given red/yellow door as opened or locked.
"""
try: try:
area = str(area) area = str(area)
door = int(door) door = int(door)
print("Couldn't find door {} in area {}".format(door, area)) print("Couldn't find door {} in area {}".format(door, area))


def toggleSwimsuit(self): def toggleSwimsuit(self):
""" Determine whether or not Samus is wearing her armor.
"""
self.swimsuit = not self.swimsuit self.swimsuit = not self.swimsuit


def newLocation(self, loc): def newLocation(self, loc):
""" Set a new starting location (0-4).
"""
try: try:
loc = str(loc) loc = str(loc)
except: except:
print("Couldn't find location: {}".format(loc)) print("Couldn't find location: {}".format(loc))


def collectedItems(self): def collectedItems(self):
""" List which items have been collected.
Under this generator's rules, if Samus doesn't have an item,
it's available to be picked up.
"""
o = [] o = []
for k,v in self.itemsCollected.items(): for k,v in self.itemsCollected.items():
if v == True: if v == True:
return ", ".join(o) return ", ".join(o)


def collectedMissiles(self): def collectedMissiles(self):
""" List which missile tanks have been collected.
"""
o = [] o = []
for k, v in self.missileTanks.items(): for k, v in self.missileTanks.items():
if v == True: if v == True:
return ", ".join([str(b) for b in o]) return ", ".join([str(b) for b in o])
def collectedEtanks(self): def collectedEtanks(self):
""" List which energy tanks have been collected.
"""
o = [] o = []
for k, v in self.energyTanks.items(): for k, v in self.energyTanks.items():
if v == True: if v == True:
return ", ".join([str(b) for b in o]) return ", ".join([str(b) for b in o])
def killedZebetites(self): def killedZebetites(self):
""" List which Zebetite stems have been destroyed.
"""
o = [] o = []
for k, v in self.zebetitesDestroyed.items(): for k, v in self.zebetitesDestroyed.items():
if v == True: if v == True:
return ", ".join([str(b) for b in o]) return ", ".join([str(b) for b in o])


def killedBosses(self): def killedBosses(self):
""" List which bosses have been killed.
"""
o = [] o = []
if self.kraidKilled: if self.kraidKilled:
o.append("Kraid") o.append("Kraid")
return ", ".join(o) return ", ".join(o)
def raisedStatues(self): def raisedStatues(self):
""" List which statues have been raised.
"""
o = [] o = []
if self.kraidStatue: if self.kraidStatue:
o.append("Kraid") o.append("Kraid")
return ", ".join(o) return ", ".join(o)


def inBailey(self): def inBailey(self):
""" Show whether Samus is in her swimsuit or not.
'inBailey' refers to an old (false) explanation of the JUSTIN BAILEY
password, in which a 'bailey' was English slang for a bathing suit,
so with that password, Samus was "Just In Bailey" - i.e. in her swimsuit.
"""
if self.swimsuit: if self.swimsuit:
return "Yes" return "Yes"
else: else:
return "No" return "No"


def openedDoors(self): def openedDoors(self):
""" List which red/yellow doors have been unlocked.
"""
d = {"Brinstar": 0, "Norfair": 0, "Kraid": 0, "Ridley": 0, "Tourian": 0} d = {"Brinstar": 0, "Norfair": 0, "Kraid": 0, "Ridley": 0, "Tourian": 0}
o = [] o = []
for k,v in self.doors["Brinstar"].items(): for k,v in self.doors["Brinstar"].items():
return ", ".join(o) return ", ".join(o)


def getBits(self): def getBits(self):
""" Return the bitfield in an easily-readable format.
"""
o = [] o = []
word = [] word = []
for i in range(128): for i in range(128):
return o1 + "\n" + o2 return o1 + "\n" + o2


def toString(self): def toString(self):
""" Output the game state as a newline-delimited string.
"""
ic = "Items Collected: {}".format(self.collectedItems()) ic = "Items Collected: {}".format(self.collectedItems())
mt = "Missile Tanks Collected: {}".format(self.collectedMissiles()) mt = "Missile Tanks Collected: {}".format(self.collectedMissiles())
et = "Energy Tanks Collected: {}".format(self.collectedEtanks()) et = "Energy Tanks Collected: {}".format(self.collectedEtanks())
dr = "Unlocked Doors: {}".format(self.openedDoors()) dr = "Unlocked Doors: {}".format(self.openedDoors())
ms = "Missiles: {}".format(self.missileCount) ms = "Missiles: {}".format(self.missileCount)
pw = "Password: {}".format(self.password) pw = "Password: {}".format(self.password)
# bf = "Bitfield:\n{}".format(self.getBits())
return "\n".join([ic, mt, et, zb, kb, rs, sw, sl, dr, ms, pw]) return "\n".join([ic, mt, et, zb, kb, rs, sw, sl, dr, ms, pw])


def randomize(self): def randomize(self):
""" The randomizer!
"""
# Items # Items
if random.randint(0,1) == 1: if random.randint(0,1) == 1:
self.toggleItem("Maru Mari") self.toggleItem("Maru Mari")
if random.randint(0,1) == 1: if random.randint(0,1) == 1:
self.doors["Tourian"][i+1] = True self.doors["Tourian"][i+1] = True
# Swimsuit # Swimsuit
# Samus has a 1/3 chance of spawning in her swimsuit.
# There's no technical reason for this, just a personal choice.
if random.randint(0,2) == 2: if random.randint(0,2) == 2:
self.toggleSwimsuit() self.toggleSwimsuit()
# Start Location # Start Location
self.missileCount = random.randint(0,255) self.missileCount = random.randint(0,255)


def createBitfield(self): def createBitfield(self):
""" Create the 144-bit field from the game state
that will generate the password.
"""
self.initializeBitfield() self.initializeBitfield()
# Doing this in order, which is dumb and tedious but accurate. # Doing this in order, which is dumb and tedious but accurate.
if self.itemsCollected["Maru Mari"]: if self.itemsCollected["Maru Mari"]:
self.bitfield[57] = 1 self.bitfield[57] = 1
if self.motherBrainKilled: if self.motherBrainKilled:
self.bitfield[58] = 1 self.bitfield[58] = 1

# 59-63 unknown # 59-63 unknown
# if self.:
# self.bitfield[59] = 1
# if self.:
# self.bitfield[60] = 1
# if self.:
# self.bitfield[61] = 1
# if self.:
# self.bitfield[62] = 1
# if self.:
# self.bitfield[63] = 1

# not 64, 65, or 66 = Brinstar # not 64, 65, or 66 = Brinstar
# 64 = Norfair # 64 = Norfair
# 65 and not 66 = Kraid's Lair # 65 and not 66 = Kraid's Lair
self.bitfield[65] = 1 self.bitfield[65] = 1
if self.startLocation == 3 or self.startLocation == 4: if self.startLocation == 3 or self.startLocation == 4:
self.bitfield[66] = 1 self.bitfield[66] = 1

# 67 is the reset bit, I want all passwords to be valid # 67 is the reset bit, I want all passwords to be valid
# if self.: # if self.:
# self.bitfield[67] = 1 # self.bitfield[67] = 1

# 68-70 are unknown # 68-70 are unknown
# if self.:
# self.bitfield[68] = 1
# if self.:
# self.bitfield[69] = 1
# if self.:
# self.bitfield[70] = 1

if self.swimsuit: if self.swimsuit:
self.bitfield[71] = 1 self.bitfield[71] = 1
if self.samusHas["Bombs"]: if self.samusHas["Bombs"]:
self.bitfield[78] = 1 self.bitfield[78] = 1
if self.samusHas["Ice Beam"]: if self.samusHas["Ice Beam"]:
self.bitfield[79] = 1 self.bitfield[79] = 1

# Missile count # Missile count
# +2^n from 0 to 7 # +2^n from 0 to 7
binMissiles = bin(self.missileCount).replace('0b','')[::-1] binMissiles = bin(self.missileCount).replace('0b','')[::-1]
if int(binMissiles[7]) == 1: if int(binMissiles[7]) == 1:
self.bitfield[87] = 1 self.bitfield[87] = 1
# 88-119 are game age, leaving at 0 # 88-119 are game age, leaving at 0
# if self.:
# self.bitfield[88] = 1
# if self.:
# self.bitfield[89] = 1
# if self.:
# self.bitfield[90] = 1
# if self.:
# self.bitfield[91] = 1
# if self.:
# self.bitfield[92] = 1
# if self.:
# self.bitfield[93] = 1
# if self.:
# self.bitfield[94] = 1
# if self.:
# self.bitfield[95] = 1
# if self.:
# self.bitfield[96] = 1
# if self.:
# self.bitfield[97] = 1
# if self.:
# self.bitfield[98] = 1
# if self.:
# self.bitfield[99] = 1
# if self.:
# self.bitfield[100] = 1
# if self.:
# self.bitfield[101] = 1
# if self.:
# self.bitfield[102] = 1
# if self.:
# self.bitfield[103] = 1
# if self.:
# self.bitfield[104] = 1
# if self.:
# self.bitfield[105] = 1
# if self.:
# self.bitfield[106] = 1
# if self.:
# self.bitfield[107] = 1
# if self.:
# self.bitfield[108] = 1
# if self.:
# self.bitfield[109] = 1
# if self.:
# self.bitfield[110] = 1
# if self.:
# self.bitfield[111] = 1
# if self.:
# self.bitfield[112] = 1
# if self.:
# self.bitfield[113] = 1
# if self.:
# self.bitfield[114] = 1
# if self.:
# self.bitfield[115] = 1
# if self.:
# self.bitfield[116] = 1
# if self.:
# self.bitfield[117] = 1
# if self.:
# self.bitfield[118] = 1
# if self.:
# self.bitfield[119] = 1

# 120-123 are unknown # 120-123 are unknown
# if self.:
# self.bitfield[120] = 1
# if self.:
# self.bitfield[121] = 1
# if self.:
# self.bitfield[122] = 1
# if self.:
# self.bitfield[123] = 1

# I have no idea why these are at the end. I wonder if Kraid and
# Ridley were relatively late additions to the game? (Or maybe
# they were just implemented late in the game's development.)
if self.ridleyKilled: if self.ridleyKilled:
self.bitfield[124] = 1 self.bitfield[124] = 1
if self.ridleyStatue: if self.ridleyStatue:
self.bitfield[127] = 1 self.bitfield[127] = 1


def generatePassword(self): def generatePassword(self):
""" Generate the password from the bitfield.
This is a five-step process.
1) Reverse the order of each 8-bit byte to make it little-endian.
2) Cycle the entire bitfield 0-7 bits to the right.
Append the number of shifts in binary to the end - again, little-endian.
3) Create the checksum by turning each byte into a decimal number,
summing them, converting the sum *back* to binary, and taking the lowest
8 bits of that binary sum and adding it - BIG-ENDIAN - to the end.
4) Separate the bitstream into ***6-bit*** chunks and create a decimal
number from each chunk (0-63).
5) Associate each decimal number with a letter from the Metroid Alphabet
(listed at the top of __init__()).
I'm not doing step 2 yet, which is equivalent to shifting 0 places
and making the shift byte 00000000.
"""
# not gonna do the bit-shifting yet # not gonna do the bit-shifting yet
# shiftbit = random.randint(0,7)
bitfield = self.bitfield bitfield = self.bitfield
# for _ in range(shiftbit):
# [bitfield[127]] + bitfield[:127]
# binShift = bin(shiftbit).replace('0b','')
# while len(binShift) < 8:
# binShift = "0" + binShift
# for bit in binShift:
# bitfield.append(int(bit))
bitfield = bitfield + [0,0,0,0,0,0,0,0]
# checking = []
# for i in range(16):
# checking.append(int("".join([str(x) for x in bitfield[i:i+8]]), 2))
# print("Full Bitfield: {}".format("".join([str(x) for x in bitfield])))
bitfield = bitfield + [0,0,0,0,0,0,0,0] # add the zero shift byte
self.fullbitfield = "".join([str(x) for x in bitfield]) self.fullbitfield = "".join([str(x) for x in bitfield])
newBitfield = [] newBitfield = []
for i in range(17): for i in range(17):
checksum = binChecksum[-8:] checksum = binChecksum[-8:]
while len(checksum) < 8: while len(checksum) < 8:
checksum = "0" + checksum checksum = "0" + checksum
# print(checksum)
for bit in checksum: for bit in checksum:
bitfield += bit bitfield += bit
# print("Real Bitfield: {}".format(bitfield))


letters = [] letters = []
letter = [] letter = []
return self.password return self.password


def decodePassword(self, pwd): def decodePassword(self, pwd):
""" Sanity checking! This function decodes an input password back into a bitfield,
so that you can check that it was properly encoded.
"""
densePwd = pwd.replace(" ","") densePwd = pwd.replace(" ","")
numPwd = [] numPwd = []
for chr in densePwd: for chr in densePwd:
while len(longword) < 6: while len(longword) < 6:
longword = "0" + longword longword = "0" + longword
longBitPwd.append(longword) longBitPwd.append(longword)
# print(self.fullbitfield)
newBitfield = "".join(longBitPwd) newBitfield = "".join(longBitPwd)
# print(newBitfield)
# print(newBitfield == self.fullbitfield)
# print(len(newBitfield[:136]))
# print(newBitfield[:136])
csm = sum([int(x) for x in newBitfield[:136]]) csm = sum([int(x) for x in newBitfield[:136]])
print(csm) print(csm)
# print(bin(csm).replace("0b",""))
for i in range(len(newBitfield)): for i in range(len(newBitfield)):
print(newBitfield[i], end="") print(newBitfield[i], end="")
if i%8 == 7: if i%8 == 7:
gs = MetroidState() gs = MetroidState()
gs.randomize() gs.randomize()
gs.createBitfield() gs.createBitfield()
pwd = gs.generatePassword()
gs.generatePassword()
print(gs.toString()) print(gs.toString())
# print(pwd)
# gs.decodePassword(pwd)


if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) == 2: if len(sys.argv) == 2:

Loading…
Cancel
Save