Skip to content

Instantly share code, notes, and snippets.

@Myriachan
Created May 8, 2024 04:20
Show Gist options
  • Select an option

  • Save Myriachan/083b2a9d45f3b3a630f27643168ead0e to your computer and use it in GitHub Desktop.

Select an option

Save Myriachan/083b2a9d45f3b3a630f27643168ead0e to your computer and use it in GitHub Desktop.
Script to see whether a Beyond Chaos seed allows getting every item
import sys
import re
# Flags for the item map
FLAG_SAFE = 1 << 0
FLAG_STEAL = 1 << 1
FLAG_DROP = 1 << 2
FLAG_MORPH = 1 << 3
FLAG_TREASURE = 1 << 4
FLAG_SHOP = 1 << 5
FLAG_UNCURSE = 1 << 6
FLAG_COLISEUM = 1 << 7
FLAG_SAFE_VIA_UNCURSE = 1 << 8
SAFE_FLAGS = FLAG_SAFE | FLAG_SAFE_VIA_UNCURSE
PROPAGATE_FLAGS = SAFE_FLAGS | FLAG_COLISEUM
# Locations accessible indefinitely
WOR_SAFE_LOCATIONS = """
Ancient Castle
Cave on the Veldt
Daryl's Tomb
Fanatics Tower
Figaro Castle Basement
Final Dungeon
Hidon's Cave
Mt. Zozo
Narshe (WoR)
Owzer's Mansion
Phoenix Cave
South Figaro Cave (WoR)
South Figaro Basement
Yeti's Lair
Zone Eater's Belly
Zozo
"""
# Shops accessible indefinitely
WOR_SAFE_SHOPS = """
ALBROOK (WOR) ARMOR
ALBROOK (WOR) ITEMS
ALBROOK (WOR) RELICS
ALBROOK (WOR) WEAPONS
FIGARO CASTLE (WOR) ITEMS
JIDOOR (WOR) ARMOR
JIDOOR (WOR) ITEMS
JIDOOR (WOR) RELICS
JIDOOR (WOR) WEAPONS
KOHLINGEN (WOR) ARMOR
KOHLINGEN (WOR) ITEMS
KOHLINGEN (WOR) WEAPONS
MARANDA (WOR) ARMOR
MARANDA (WOR) WEAPONS
NIKEAH (WOR) ARMOR
NIKEAH (WOR) ITEMS
NIKEAH (WOR) RELICS
NIKEAH (WOR) WEAPONS
SOUTH FIGARO (WOR) ARMOR
SOUTH FIGARO (WOR) ITEMS
SOUTH FIGARO (WOR) RELICS
SOUTH FIGARO (WOR) WEAPONS
THAMASA (WOR) ARMOR
THAMASA (WOR) ITEMS
THAMASA (WOR) RELICS
THAMASA (WOR) WEAPONS
TZEN (WOR) ARMOR
TZEN (WOR) ITEMS
TZEN (WOR) RELICS
TZEN (WOR) WEAPONS
"""
# Final bosses, whose items can't be kept.
# Kefka and Fake Kefka are in code.
MONSTER_EXCLUDE = """
Face
Girl
Hit
Magic
Long Arm
Short Arm
Sleep
Tiger
Tools
"""
# Monster parameters that should be considered lists.
MONSTER_PARAM_LISTS = """
OTHER
SKILLS
SKETCH
STEAL
DROPS
MORPH
LOCATION
"""
# Monster parameter lists that can contain item names.
# Each entry must also be in MONSTER_PARAM_LISTS.
MONSTER_PARAM_ITEMS = {
"STEAL" : FLAG_STEAL,
"DROPS" : FLAG_DROP,
"MORPH" : FLAG_MORPH,
}
# Cursed Shield possible names.
CURSED_SHIELD_NAMES = """
Cursed Shld
Cursed Shld?
"""
# Paladin Shield possible names.
PALADIN_SHIELD_NAMES = """
Paladin Shld
Paladin Shl?
"""
def strings_to_lookup_dict(data):
temp = list(filter(lambda x: x != "", data.split("\n")))
return {value : None for dontcare, value in enumerate(temp)}
WOR_SAFE_LOCATIONS = strings_to_lookup_dict(WOR_SAFE_LOCATIONS)
WOR_SAFE_SHOPS = strings_to_lookup_dict(WOR_SAFE_SHOPS)
MONSTER_EXCLUDE = strings_to_lookup_dict(MONSTER_EXCLUDE)
MONSTER_PARAM_LISTS = strings_to_lookup_dict(MONSTER_PARAM_LISTS)
CURSED_SHIELD_NAMES = strings_to_lookup_dict(CURSED_SHIELD_NAMES)
PALADIN_SHIELD_NAMES = strings_to_lookup_dict(PALADIN_SHIELD_NAMES)
def is_monster_excluded(monster):
if monster["NAME"] in MONSTER_EXCLUDE:
return True
# Don't exclude Kefka at Narshe.
if monster["NAME"] == "Kefka" and monster["LEVEL"] >= 40:
return True
# Fake Kefka should be excluded, too, but how?
return False
def is_location_safe(location):
if location in WOR_SAFE_LOCATIONS:
return True
if location[0:14] == "World of Ruin ":
return True
return False
def flags_to_string(flags):
l = []
if (flags & SAFE_FLAGS) != 0:
l += ["safe"]
if (flags & FLAG_TREASURE) != 0:
l += ["treasure"]
if (flags & FLAG_SHOP) != 0:
l += ["shop"]
if (flags & FLAG_STEAL) != 0:
l += ["steal"]
if (flags & FLAG_DROP) != 0:
l += ["drop"]
if (flags & FLAG_MORPH) != 0:
l += ["morph"]
if (flags & FLAG_UNCURSE) != 0:
l += ["uncurse"]
if (flags & FLAG_COLISEUM) != 0:
l += ["coliseum"]
return ", ".join(l)
class Reader:
def __init__(self, lines):
self.lines = lines
self.lineno = 1
def next(self):
if self.lineno - 1 >= len(self.lines):
raise IndexError("read past end of file")
self.lineno += 1
return self.lines[self.lineno - 2]
def at_end(self):
return self.lineno - 1 == len(self.lines)
def process(document):
item_list = []
item_map = {}
item_junction_fixup = {}
coliseum_map = {}
regex_section = re.compile(r"^-\d+- (.*)$")
def is_section_name(l, n):
m = regex_section.match(l)
if m == None:
return False
return m.group(1) == n
while True:
if document.next() == "============================================================":
if is_section_name(document.next(), "COLOSSEUM"):
if document.next() != "------------------------------------------------------------":
raise ValueError("bad coliseum block")
break
while True:
line = document.next()
if line == "":
break
# Wall Ring? -> Back Guard? : LV 26 Migril
if line[12:16] != " -> ":
raise ValueError("invalid coliseum line (1)")
if line[28:36] != " : LV ":
raise ValueError("invalid coliseum line (2)")
from_item = line[0:12].strip()
to_item = line[16:28].strip()
if from_item in coliseum_map:
raise ValueError("duplicate item %s in coliseum" % from_item)
item_list += [from_item]
coliseum_map[from_item] = to_item
item_map[from_item] = 0
# Build mapping table for junctioned item names, ugh.
# Need this because coliseum and shop lists don't have
# the exclamation points added.
for item in item_list:
item_junction_fixup[item] = item
if item[-1] == "?":
if len(item) - 1 <= 10:
new_item = item[:-1] + "?!"
else:
new_item = item[:-1] + "!"
else:
new_item = item[:11] + "!"
item_junction_fixup[new_item] = item
# Wait for monster list.
while True:
if document.next() == "============================================================":
if is_section_name(document.next(), "MONSTERS"):
if document.next() != "------------------------------------------------------------":
raise ValueError("bad monster block")
break
line = document.next()
if line != "":
raise ValueError("missing blank before monster blocks")
# Abidettu (Level 23)
regex_monster_name = re.compile(r"^(.*) \(Level (\d+)\)$")
# \--------------------------------/
regex_monster_stats_end = re.compile(r"^\\-+\/$")
# SKETCH: Battle, Cling Ball
regex_monster_param = re.compile(r"^([^:]+): *(.*)$")
# MORPH (50%)
regex_monster_morph = re.compile(r"^MORPH \((\d+)%\)$")
while True:
line = document.next()
if line == "":
break
elif line == "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~":
pass
elif line == "============================================================":
break
else:
raise ValueError("bad monster entry: " + line)
line = document.next()
match = regex_monster_name.match(line)
if match == None:
raise ValueError("bad monster name line: " + line)
monster = { "NAME" : match.group(1), "LEVEL" : int(match.group(2)) }
while True:
line = document.next()
if regex_monster_stats_end.match(line) != None:
break
# Parse the monster information.
while True:
line = document.next()
if line == "":
break
match = regex_monster_param.match(line)
if match == None:
raise ValueError("bad monster parameter for " + monster["NAME"])
# Check for "MORPH (3%)" and change to MORPH plus MORPH%.
param_name = match.group(1)
param_value = match.group(2)
match = regex_monster_morph.match(param_name)
if match != None:
param_name = "MORPH"
monster["MORPH%"] = int(match.group(1))
# Check for a parameter type that should be interpreted as a list.
if param_name in MONSTER_PARAM_LISTS:
param_value = param_value.split(", ")
# Turn empty string into [] rather than [""]
if len(param_value) == 1:
if param_value[0] == "":
param_value = []
# Remove exclamation marks, since the Coliseum list doesn't have them!
monster[param_name] = param_value
# Is this a monster we don't want to consider? These are final bosses.
# Sadly, excluding Fake Kefka would be complicated.
if is_monster_excluded(monster):
#print("%s (%d): on excluded list" % (monster["NAME"], monster["LEVEL"]))
continue
# Is this monster present in a safe location?
safe_monster = False
if "LOCATION" in monster:
for location in monster["LOCATION"]:
if is_location_safe(location):
safe_monster = True
break
# Flag all the items available from this monster.
for param in monster.keys():
# Skip morph for unsafe locations, because maybe no Ragnarok yet.
if (param == "MORPH") and (not safe_monster):
continue
if param in MONSTER_PARAM_ITEMS:
flags_to_set = MONSTER_PARAM_ITEMS[param]
if safe_monster:
flags_to_set |= FLAG_SAFE
for item in monster[param]:
# Monster info has the junction !.
item = item_junction_fixup[item]
item_map[item] |= flags_to_set
# Wait for shop list.
while True:
if document.next() == "============================================================":
if is_section_name(document.next(), "SHOPS"):
if document.next() != "------------------------------------------------------------":
raise ValueError("bad shop block")
if document.next() != "":
raise ValueError("bad shop block")
break
# Shop processing.
# ThunderBlad? 18000
regex_shop_item = re.compile(r"^(.{12}) +\d+$")
while True:
shop_name = document.next()
if shop_name == "":
break
# Shop item list.
while True:
line = document.next()
if line == "":
break
if line == "Discounts for female characters.":
continue
match = regex_shop_item.match(line)
if match == None:
raise ValueError("bad shop item")
item = match.group(1).rstrip()
flags_to_set = FLAG_SHOP
if shop_name in WOR_SAFE_SHOPS:
flags_to_set |= FLAG_SAFE
#print("shop: %s (%d)" % (item, flags_to_set))
item_map[item] |= flags_to_set
# Wait for treasure list.
while True:
if document.next() == "============================================================":
if is_section_name(document.next(), "TREASURE CHESTS"):
if document.next() != "------------------------------------------------------------":
raise ValueError("bad treasure chest block")
if document.next() != "":
raise ValueError("bad treasure chest block")
break
# Treasure processing. We skip lines with * because they
# are not necessarily available.
# Empty! (238)
regex_treasure_empty = re.compile(r"^Empty! \(\d+\)$")
# Treasure 227: Air Anchor
regex_treasure_entry = re.compile(r"^(\*?(?:Treasure|Enemy)) \d+: (.*)$")
# 20400 GP
regex_treasure_money = re.compile(r"^\d+ GP$")
while True:
area_name = document.next()
if area_name == "":
break
# Treasure chest list.
while True:
line = document.next()
if line == "":
break
# Skip "Empty!"
if regex_treasure_empty.match(line) != None:
continue
# Skip Enemy, *Enemy, *Treasure.
match = regex_treasure_entry.match(line)
if match == None:
raise ValueError("bad treasure chest: " + line)
type = match.group(1)
item = match.group(2)
if type != "Treasure":
continue
# Skip money
if regex_treasure_money.match(item) != None:
continue
flags_to_set = FLAG_TREASURE
#print("treasure: %s %s %s (%d)" % (area_name, type, item, flags_to_set))
# Monster info has the junction !.
item = item_junction_fixup[item]
item_map[item] |= flags_to_set
# Wait for secret item list.
while True:
if document.next() == "============================================================":
if is_section_name(document.next(), "SECRET ITEMS"):
if document.next() != "------------------------------------------------------------":
raise ValueError("bad secret item block")
break
# Secret item list.
while True:
if document.at_end():
break
line = document.next()
if line == "":
continue
if line == "============================================================":
break
# If not previously encountered, add this item to the list.
if line not in item_map:
item_list += [line]
item_map[line] = 0
coliseum_map[line] = line # Map it to itself I guess
# Finally, we're done reading the file.
item_list = sorted(item_list)
# Consider uncursing the Cursed Shield.
cursed_shld_name = None
for item in CURSED_SHIELD_NAMES:
if item in item_map:
cursed_shld_name = item
break
paladin_shld_name = None
for item in PALADIN_SHIELD_NAMES:
if item in item_map:
paladin_shld_name = item
break
item_map[paladin_shld_name] |= FLAG_UNCURSE | item_map[cursed_shld_name]
# Compute the transitive closure using the coliseum.
more = True
loop_count = 0
while more:
more = False
loop_count += 1
# Process coliseum
for from_item in item_list:
to_item = coliseum_map[from_item]
if to_item == from_item:
continue
from_flags = item_map[from_item]
to_flags = item_map[to_item]
if from_flags == 0:
continue
new_flags = (from_flags & PROPAGATE_FLAGS) | FLAG_COLISEUM
if (to_flags & new_flags) == new_flags:
continue
item_map[to_item] |= new_flags
more = True
# If Cursed Shld is safe, flag Paladin Shld as safe-via-uncurse.
if (item_map[cursed_shld_name] & SAFE_FLAGS) != 0:
if (item_map[paladin_shld_name] & FLAG_SAFE_VIA_UNCURSE) == 0:
item_map[paladin_shld_name] |= FLAG_SAFE_VIA_UNCURSE
more = True
#print("loop count: %d" % loop_count)
# Print results!
#print("")
#print("*** SAFE ITEMS ***")
#for item in item_list:
# if (item_map[item] & FLAG_SAFE) == FLAG_SAFE:
# print(" " + item)
print("")
print("*** SAFE VIA UNCURSE ITEMS ***")
for item in item_list:
if (item_map[item] & SAFE_FLAGS) == FLAG_SAFE_VIA_UNCURSE:
print(" %s (%s)" % (item, flags_to_string(item_map[item])))
print("")
print("*** UNSAFE ITEMS ***")
for item in item_list:
if ((item_map[item] & SAFE_FLAGS) == 0) and (item_map[item] != 0):
print(" %s (%s)" % (item, flags_to_string(item_map[item])))
print("")
print("*** MISSING ITEMS ***")
for item in item_list:
if item_map[item] == 0:
print(" %s (%s)" % (item, flags_to_string(item_map[item])))
def main(argv):
for filename in argv[1:]:
with open(filename, "r") as file:
lines = file.readlines()
for i in range(len(lines)):
if lines[i][-1] == "\n":
lines[i] = lines[i][:-1]
process(Reader(lines))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment