diff --git a/Fill.py b/Fill.py index d62b4955..153dcb2b 100644 --- a/Fill.py +++ b/Fill.py @@ -42,9 +42,10 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si for item_to_place in items_to_place: perform_access_check = True if world.accessibility[item_to_place.player] == 'none': - perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game + perform_access_check = not world.has_beaten_game(maximum_exploration_state, + item_to_place.player) if single_player_placement else not has_beaten_game for location in locations: - if (not single_player_placement or location.player == item_to_place.player)\ + if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check): spot_to_fill = location break @@ -70,6 +71,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si itempool.extend(unplaced_items) + def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: @@ -243,15 +245,14 @@ def balance_multiworld_progression(world): else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - world.random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {player: 0 for player in world.player_ids} def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) - return [loc for loc in locations if sphere_state.can_reach(loc)] + return {loc for loc in locations if sphere_state.can_reach(loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -261,14 +262,14 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if - reachables < threshold and player in balanceable_players] + balancing_players = {player for player, reachables in reachable_locations_count.items() if + reachables < threshold and player in balanceable_players} if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() - candidate_items = collections.defaultdict(list) + candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: if location.event: @@ -276,7 +277,7 @@ def balance_multiworld_progression(world): player = location.item.player # only replace items that end up in another player's world if not location.locked and player in balancing_players and location.player != player: - candidate_items[player].append(location) + candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) @@ -286,10 +287,10 @@ def balance_multiworld_progression(world): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') - unlocked_locations = collections.defaultdict(list) + unlocked_locations = collections.defaultdict(set) for l in unchecked_locations: if l not in balancing_unchecked_locations: - unlocked_locations[l.player].append(l) + unlocked_locations[l.player].add(l) items_to_replace = [] for player in balancing_players: locations_to_test = unlocked_locations[player] @@ -299,7 +300,6 @@ def balance_multiworld_progression(world): reducing_state = state.copy() for location in itertools.chain((l for l in items_to_replace if l.item.player == player), items_to_test): - reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events(locations=locations_to_test) @@ -313,33 +313,40 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + world.random.shuffle(replacement_locations) + items_to_replace.sort() + world.random.shuffle(items_to_replace) + while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() - - while not new_location.can_fill(state, old_location.item, False) or ( - new_location.item and not old_location.can_fill(state, new_location.item, False)): - replacement_locations.insert(0, new_location) - new_location = replacement_locations.pop() - - swap_location_item(old_location, new_location) - logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " - f"displacing {old_location.item} into {old_location}") - state.collect(new_location.item, True, new_location) - replaced_items = True + for new_location in replacement_locations: + if new_location.can_fill(state, old_location.item, False) and \ + old_location.can_fill(state, new_location.item, False): + replacement_locations.remove(new_location) + swap_location_item(old_location, new_location) + logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " + f"displacing {old_location.item} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") if replaced_items: - unlocked = [fresh for player in balancing_players for fresh in unlocked_locations[player]] + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) - checked_locations.extend(sphere_locations) + checked_locations |= sphere_locations if world.has_beaten_game(state): break @@ -380,7 +387,8 @@ def distribute_planned(world): set(world.player_ids) - {player}) if location.item_rule(item) ) if not unfilled: - placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError) + placement.failed(f"Could not find a world with an unfilled location {placement.location}", + FillError) continue target_world = world.random.choice(unfilled).player @@ -391,18 +399,22 @@ def distribute_planned(world): set(world.player_ids)) if location.item_rule(item) ) if not unfilled: - placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError) + placement.failed(f"Could not find a world with an unfilled location {placement.location}", + FillError) continue target_world = world.random.choice(unfilled).player elif type(target_world) == int: # target world by player id if target_world not in range(1, world.players + 1): - placement.failed(f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", ValueError) + placement.failed( + f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", + ValueError) continue else: # find world by name if target_world not in world_name_lookup: - placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.", ValueError) + placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.", + ValueError) continue target_world = world_name_lookup[target_world] diff --git a/MultiMystery.py b/MultiMystery.py index 2057bc49..9c32b92b 100644 --- a/MultiMystery.py +++ b/MultiMystery.py @@ -46,6 +46,7 @@ if __name__ == "__main__": player_name = multi_mystery_options["player_name"] meta_file_path = multi_mystery_options["meta_file_path"] weights_file_path = multi_mystery_options["weights_file_path"] + pre_roll = multi_mystery_options["pre_roll"] teams = multi_mystery_options["teams"] rom_file = options["general_options"]["rom_file"] host = options["server_options"]["host"] @@ -104,6 +105,8 @@ if __name__ == "__main__": command += f" --meta {os.path.join(player_files_path, meta_file_path)}" if os.path.exists(weights_file_path): command += f" --weights {weights_file_path}" + if pre_roll: + command += " --pre_roll" logging.info(command) import time diff --git a/Mystery.py b/Mystery.py index 880a33a1..f6da81ac9 100644 --- a/Mystery.py +++ b/Mystery.py @@ -37,6 +37,7 @@ def mystery_argparse(): parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', action='store_true') parser.add_argument('--skip_playthrough', action='store_true') + parser.add_argument('--pre_roll', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -180,6 +181,27 @@ def main(args=None, callback=ERmain): settings.sprite): logging.warning( f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.") + if args.pre_roll: + import yaml + if path == args.weights: + settings.name = f"Player{player}" + elif not settings.name: + settings.name = os.path.split(path)[-1].split(".")[0] + + if "-" not in settings.shuffle and settings.shuffle != "vanilla": + settings.shuffle += f"-{random.randint(0, 2 ** 64)}" + + pre_rolled = dict() + pre_rolled["original_seed_number"] = seed + pre_rolled["original_seed_name"] = seedname + pre_rolled["pre_rolled"] = vars(settings).copy() + if "plando_items" in pre_rolled["pre_rolled"]: + pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]] + if "plando_connections" in pre_rolled["pre_rolled"]: + pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]] + + with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f: + yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: getattr(erargs, k)[player] = v @@ -349,6 +371,21 @@ def roll_triggers(weights: dict) -> dict: return weights def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))): + if "pre_rolled" in weights: + pre_rolled = weights["pre_rolled"] + + if "plando_items" in pre_rolled: + pre_rolled["plando_items"] = [PlandoItem(item["item"], + item["location"], + item["world"], + item["from_pool"], + item["force"]) for item in pre_rolled["plando_items"]] + if "plando_connections" in pre_rolled: + pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"], + connection["exit"], + connection["direction"]) for connection in pre_rolled["plando_connections"]] + return argparse.Namespace(**pre_rolled) + if "linked_options" in weights: weights = roll_linked_options(weights) diff --git a/Utils.py b/Utils.py index 8fe7141d..08b500b3 100644 --- a/Utils.py +++ b/Utils.py @@ -188,6 +188,7 @@ def get_default_options() -> dict: "players": 0, "weights_file_path": "weights.yaml", "meta_file_path": "meta.yaml", + "pre_roll": False, "player_name": "", "create_spoiler": 1, "zip_roms": 0, diff --git a/WebHostLib/static/static/weightedSettings.json b/WebHostLib/static/static/weightedSettings.json index f1f7706c..a9bd1fca 100644 --- a/WebHostLib/static/static/weightedSettings.json +++ b/WebHostLib/static/static/weightedSettings.json @@ -1602,37 +1602,6 @@ } } }, - "triforcehud": { - "keyString": "rom.triforcehud", - "friendlyName": "Triforce Hud Options", - "description": "Hide the triforce hud in certain circumstances.", - "inputType": "range", - "subOptions": { - "normal": { - "keyString": "rom.triforcehud.normal", - "friendlyName": "Normal", - "description": "Always displays HUD as usual.", - "defaultValue": 0 - }, - "hide_goal": { - "keyString": "rom.triforcehud.hide_goal", - "friendlyName": "Hide Goal", - "description": "Hide Triforce Hud elements until a single triforce piece is acquired or spoken to Murahadala", - "defaultValue": 50 - }, - "hide_total": { - "keyString": "rom.triforcehud.hide_required", - "friendlyName": "Hide Required Total", - "description": "Hide total amount needed to win the game (unless spoken to Murahadala)", - "defaultValue": 0 - }, - "hide_both": { - "keyString": "rom.triforcehud.hide_both", - "friendlyName": "Hide Both", - "description": "Hide both of the above options", - "defaultValue": 0 - } - }, "reduceflashing": { "keyString": "rom.reduceflashing", "friendlyName": "Full-Screen Flashing Effects", @@ -1673,6 +1642,38 @@ } } }, + "triforcehud": { + "keyString": "rom.triforcehud", + "friendlyName": "Triforce Hunt HUD Options", + "description": "Hide the triforce hud in certain circumstances.", + "inputType": "range", + "subOptions": { + "normal": { + "keyString": "rom.triforcehud.normal", + "friendlyName": "Always Show", + "description": "Always display HUD", + "defaultValue": 50 + }, + "hide_goal": { + "keyString": "rom.triforcehud.hide_goal", + "friendlyName": "Hide HUD", + "description": "Hide Triforce HUD elements until a single triforce piece is acquired or you speak to Murahadala", + "defaultValue": 0 + }, + "hide_total": { + "keyString": "rom.triforcehud.hide_required", + "friendlyName": "Hide Total", + "description": "Hide total triforce pieces needed to win the game until you speak with Murahadala", + "defaultValue": 0 + }, + "hide_both": { + "keyString": "rom.triforcehud.hide_both", + "friendlyName": "Hide HUD Total", + "description": "Combination of Hide HUD and Hide Total", + "defaultValue": 0 + } + } + }, "menuspeed": { "keyString": "menuspeed", "friendlyName": "Menu Speed", diff --git a/WebHostLib/static/static/weightedSettings.yaml b/WebHostLib/static/static/weightedSettings.yaml index 657c32f6..d309f205 100644 --- a/WebHostLib/static/static/weightedSettings.yaml +++ b/WebHostLib/static/static/weightedSettings.yaml @@ -20,7 +20,7 @@ # For use with the weighted-settings page on the website. Changing this value will cause all users to be prompted # to update their settings. The version number should match the current released version number, and the revision # should be updated manually by whoever edits this file. -ws_version: 4.0.1 rev1 +ws_version: 4.1.0 rev0 description: Template Name # Used to describe your yaml. Useful if you have multiple files name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit @@ -363,6 +363,11 @@ rom: quickswap: # Enable switching items by pressing the L+R shoulder buttons on: 50 off: 0 + triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala + normal: 50 # original behavior (always visible) + hide_goal: 0 # hide counter until a piece is collected or speaking to Murahadala + hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala + hide_both: 0 # Hide both under above circumstances reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. on: 50 off: 0 diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 1fd297ba..458a66b2 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -350,8 +350,9 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): # Add items to player inventory for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}): + # logging.info(f"{ms_team}, {ms_player}, {locations_checked}") # Skip teams and players not matching the request - if ms_team != (team - 1) or ms_player != player: + if ms_team != (team - 1): continue # If the player does not have the item, do nothing @@ -360,7 +361,10 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): continue item, recipient = locations[location, ms_player] - attribute_item_solo(inventory, item) + if recipient == player: + attribute_item_solo(inventory, item) + if ms_player != player: + continue checks_done[player_location_to_area[location]] += 1 checks_done["Total"] += 1 @@ -391,7 +395,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): sword_acquired = False sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'] if "Progressive Sword" in acquired_items: - sword_url = icons[sword_names[inventory[progressive_items["Progressive Sword"]] - 1]] + sword_url = icons[sword_names[min(inventory[progressive_items["Progressive Sword"]], 4) - 1]] sword_acquired = True else: for sword in reversed(sword_names): @@ -404,7 +408,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): gloves_acquired = False glove_names = ["Power Glove", "Titan Mitts"] if "Progressive Glove" in acquired_items: - gloves_url = icons[glove_names[inventory[progressive_items["Progressive Glove"]] - 1]] + gloves_url = icons[glove_names[min(inventory[progressive_items["Progressive Glove"]], 2) - 1]] gloves_acquired = True else: for glove in reversed(glove_names): @@ -417,7 +421,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): bow_acquired = False bow_names = ["Bow", "Silver Bow"] if "Progressive Bow" in acquired_items: - bow_url = icons[bow_names[inventory[progressive_items["Progressive Bow"]] - 1]] + bow_url = icons[bow_names[min(inventory[progressive_items["Progressive Bow"]], 2) - 1]] bow_acquired = True else: for bow in reversed(bow_names): @@ -429,7 +433,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): mail_url = icons["Green Mail"] mail_names = ["Blue Mail", "Red Mail"] if "Progressive Mail" in acquired_items: - mail_url = icons[mail_names[inventory[progressive_items["Progressive Mail"]] - 1]] + mail_url = icons[mail_names[min(inventory[progressive_items["Progressive Mail"]], 2) - 1]] else: for mail in reversed(mail_names): if mail in acquired_items: @@ -440,7 +444,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): shield_acquired = False shield_names = ["Blue Shield", "Red Shield", "Mirror Shield"] if "Progressive Shield" in acquired_items: - shield_url = icons[shield_names[inventory[progressive_items["Progressive Shield"]] - 1]] + shield_url = icons[shield_names[min(inventory[progressive_items["Progressive Shield"]], 3) - 1]] shield_acquired = True else: for shield in reversed(shield_names): diff --git a/host.yaml b/host.yaml index 558f0c34..a635876d 100644 --- a/host.yaml +++ b/host.yaml @@ -64,6 +64,10 @@ multi_mystery_options: weights_file_path: "weights.yaml" # Meta file name, within the stated player_files_path location meta_file_path: "meta.yaml" + # Option to pre-roll a yaml that will be used to roll future seeds with the exact same settings every single time. + # If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml + # as well as the generated pre-rolled yaml. + pre_roll: false # Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator) # Does nothing if the name is not found # Example: player_name = "Berserker" diff --git a/playerSettings.yaml b/playerSettings.yaml index bc667b85..f8f08a13 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -404,10 +404,10 @@ rom: quickswap: # Enable switching items by pressing the L+R shoulder buttons on: 50 off: 0 - triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahalda + triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala normal: 0 # original behavior (always visible) - hide_goal: 50 # hide counter until a piece is collected or speaking to Murahalda - hide_required: 0 # Always visible, but required amount is invisible until determined by Murahalda + hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala + hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala hide_both: 0 # Hide both under above circumstances reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. on: 50