Multiworld core implementation By Bonta0

Does not include the server/client code or the rom writes
specific to it. Indeed it cannot write multiworld roms at
all right now, pending addition future updates to support
the official ALTTPR Multiworld client.

Includes some GUI changes by Alaszun

Co-authored-by: Alaszun <koelze@google.com>
This commit is contained in:
Bonta-kun
2019-04-18 11:23:24 +02:00
committed by Kevin Cathcart
parent d44d194de7
commit 1a62b1da28
16 changed files with 1821 additions and 1611 deletions

125
Main.py
View File

@@ -12,7 +12,7 @@ from EntranceShuffle import link_entrances
from Rom import patch_rom, Sprite, LocalRom, JsonRom
from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path
@@ -40,7 +40,7 @@ def main(args, seed=None):
start = time.clock()
# initialize the world
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
world = World(args.multi, args.shuffle, args.logic, args.mode, args.difficulty, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
logger = logging.getLogger('')
if seed is None:
random.seed(None)
@@ -49,26 +49,32 @@ def main(args, seed=None):
world.seed = int(seed)
random.seed(world.seed)
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
world.difficulty_requirements = difficulties[world.difficulty]
create_regions(world)
create_dungeons(world)
for player in range(1, world.players + 1):
create_regions(world, player)
create_dungeons(world, player)
logger.info('Shuffling the World about.')
link_entrances(world)
for player in range(1, world.players + 1):
link_entrances(world, player)
mark_light_world_regions(world)
logger.info('Generating Item Pool.')
generate_itempool(world)
for player in range(1, world.players + 1):
generate_itempool(world, player)
logger.info('Calculating Access Rules.')
set_rules(world)
for player in range(1, world.players + 1):
set_rules(world, player)
logger.info('Placing Dungeon Prizes.')
@@ -102,9 +108,9 @@ def main(args, seed=None):
elif args.algorithm == 'balanced':
distribute_items_restrictive(world, gt_filler(world))
logger.info('Calculating playthrough.')
create_playthrough(world)
if world.players > 1:
logger.info('Balancing multiworld progression.')
balance_multiworld_progression(world)
logger.info('Patching ROM.')
@@ -118,20 +124,38 @@ def main(args, seed=None):
outfilebase = 'ER_%s_%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
jsonout = {}
if not args.suppress_rom:
if args.jsonout:
rom = JsonRom()
if world.players > 1:
raise NotImplementedError("Multiworld rom writes have not been implemented")
else:
rom = LocalRom(args.rom)
patch_rom(world, rom, bytearray(logic_hash), args.heartbeep, args.heartcolor, sprite)
if args.jsonout:
print(json.dumps({'patch': rom.patches, 'spoiler': world.spoiler.to_json()}))
else:
rom.write_to_file(args.jsonout or output_path('%s.sfc' % outfilebase))
player = 1
if args.jsonout:
rom = JsonRom()
else:
rom = LocalRom(args.rom)
patch_rom(world, player, rom, bytearray(logic_hash), args.heartbeep, args.heartcolor, sprite, player_names)
if args.jsonout:
jsonout['patch'] = rom.patches
else:
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.jsonout:
print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()}))
elif args.create_spoiler and not args.skip_playthrough:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.clock() - start)
@@ -144,10 +168,12 @@ def gt_filler(world):
def copy_world(world):
# ToDo: Not good yet
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
ret.required_medallions = list(world.required_medallions)
ret.swamp_patch_required = world.swamp_patch_required
ret.ganon_at_pyramid = world.ganon_at_pyramid
ret = World(world.players, world.shuffle, world.logic, world.mode, world.difficulty, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
ret.powder_patch_required = world.powder_patch_required.copy()
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
ret.treasure_hunt_count = world.treasure_hunt_count
ret.treasure_hunt_icon = world.treasure_hunt_icon
ret.sewer_light_cone = world.sewer_light_cone
@@ -162,53 +188,56 @@ def copy_world(world):
ret.difficulty_requirements = world.difficulty_requirements
ret.fix_fake_world = world.fix_fake_world
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
create_regions(ret)
create_dungeons(ret)
for player in range(1, world.players + 1):
create_regions(ret, player)
create_dungeons(ret, player)
copy_dynamic_regions_and_locations(world, ret)
# copy bosses
for dungeon in world.dungeons:
for level, boss in dungeon.bosses.items():
ret.get_dungeon(dungeon.name).bosses[level] = boss
ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss
for shop in world.shops:
copied_shop = ret.get_region(shop.region.name).shop
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world
for region in world.regions:
copied_region = ret.get_region(region.name)
copied_region = ret.get_region(region.name, region.player)
copied_region.is_light_world = region.is_light_world
copied_region.is_dark_world = region.is_dark_world
for entrance in region.entrances:
ret.get_entrance(entrance.name).connect(copied_region)
ret.get_entrance(entrance.name, entrance.player).connect(copied_region)
# fill locations
for location in world.get_locations():
if location.item is not None:
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type)
ret.get_location(location.name).item = item
item.location = ret.get_location(location.name)
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player)
if location.event:
ret.get_location(location.name).event = True
ret.get_location(location.name, location.player).event = True
# copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool:
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type))
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
# copy progress items in state
ret.state.prog_items = list(world.state.prog_items)
ret.state.stale = True
set_rules(ret)
for player in range(1, world.players + 1):
set_rules(ret, player)
return ret
def copy_dynamic_regions_and_locations(world, ret):
for region in world.dynamic_regions:
new_reg = Region(region.name, region.type, region.hint_text)
new_reg = Region(region.name, region.type, region.hint_text, region.player)
ret.regions.append(new_reg)
ret.dynamic_regions.append(new_reg)
@@ -219,8 +248,8 @@ def copy_dynamic_regions_and_locations(world, ret):
ret.shops.append(new_reg.shop)
for location in world.dynamic_locations:
new_loc = Location(location.name, location.address, location.crystal, location.hint_text, location.parent_region)
new_reg = ret.get_region(location.parent_region.name)
new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, location.parent_region,)
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
new_reg.locations.append(new_loc)
@@ -231,7 +260,8 @@ def create_playthrough(world):
# in treasure hunt and pedestal goals, ganon is invincible
if world.goal in ['pedestal', 'triforcehunt']:
world.get_location('Ganon').item = None
for player in range(1, world.players + 1):
world.get_location('Ganon', player).item = None
# if we only check for beatable, we can do this sanity check first before writing down spheres
if world.check_beatable_only and not world.can_beat_game():
@@ -264,7 +294,7 @@ def create_playthrough(world):
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations))
if not sphere:
logging.getLogger('').debug('The following items could not be reached: %s', ['%s at %s' % (location.item.name, location.name) for location in sphere_candidates])
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if not world.check_beatable_only:
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
else:
@@ -275,11 +305,11 @@ def create_playthrough(world):
to_delete = []
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s is required to beat the game.', location.item.name)
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item
location.item = None
state.remove(old_item)
##if world.can_beat_game(state_cache[num]):
##if world.can_beat_game(state_cache[num]):
if world.can_beat_game():
to_delete.append(location)
else:
@@ -316,7 +346,7 @@ def create_playthrough(world):
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
# store the required locations for statistical analysis
old_world.required_locations = [location.name for sphere in collection_spheres for location in sphere]
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
def flist_to_iter(node):
while node:
@@ -331,9 +361,12 @@ def create_playthrough(world):
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere}
if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path):
old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop'))
old_world.spoiler.paths = dict()
for player in range(1, world.players + 1):
old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
for _, path in dict(old_world.spoiler.paths).items():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
# we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])