From d1274c12b9b592ac868a819d184a42c40343b589 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Mon, 15 Apr 2024 04:23:13 +1000 Subject: [PATCH] Muse Dash: Add filler items and rework generation balance (#2809) --- worlds/musedash/MuseDashCollection.py | 14 ++- worlds/musedash/Options.py | 19 ++-- worlds/musedash/Presets.py | 3 - worlds/musedash/__init__.py | 94 ++++++++++++-------- worlds/musedash/test/TestDifficultyRanges.py | 6 +- worlds/musedash/test/__init__.py | 2 +- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index c223893d..68e4ad59 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -66,7 +66,19 @@ class MuseDashCollections: "Error SFX Trap": STARTING_CODE + 9, } - item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items) + filler_items: Dict[str, int] = { + "Great To Perfect (10 Pack)": STARTING_CODE + 30, + "Miss To Great (5 Pack)": STARTING_CODE + 31, + "Extra Life": STARTING_CODE + 32, + } + + filler_item_weights: Dict[str, int] = { + "Great To Perfect (10 Pack)": 10, + "Miss To Great (5 Pack)": 3, + "Extra Life": 1, + } + + item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items) location_names_to_id: ChainMap = ChainMap(song_locations, album_locations) def __init__(self) -> None: diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 26ad5ff5..b6953951 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -4,11 +4,13 @@ from dataclasses import dataclass from .MuseDashCollection import MuseDashCollections + class AllowJustAsPlannedDLCSongs(Toggle): """Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs. Note: The [Just As Planned] DLC contains all [Muse Plus] songs.""" display_name = "Allow [Muse Plus] DLC Songs" + class DLCMusicPacks(OptionSet): """Which non-[Muse Plus] DLC packs can be chosen as randomised songs.""" display_name = "DLC Packs" @@ -101,20 +103,10 @@ class GradeNeeded(Choice): default = 0 -class AdditionalItemPercentage(Range): - """The percentage of songs that will have 2 items instead of 1 when completing them. - - Starting Songs will always have 2 items. - - Locations will be filled with duplicate songs if there are not enough items. - """ - display_name = "Additional Item %" - range_start = 50 - default = 80 - range_end = 100 - - class MusicSheetCountPercentage(Range): - """Collecting enough Music Sheets will unlock the goal song needed for completion. - This option controls how many are in the item pool, based on the total number of songs.""" + """Controls how many music sheets are added to the pool based on the number of songs, including starting songs. + Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important. + """ range_start = 10 range_end = 40 default = 20 @@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions): streamer_mode_enabled: StreamerModeEnabled starting_song_count: StartingSongs additional_song_count: AdditionalSongs - additional_item_percentage: AdditionalItemPercentage song_difficulty_mode: DifficultyMode song_difficulty_min: DifficultyModeOverrideMin song_difficulty_max: DifficultyModeOverrideMax diff --git a/worlds/musedash/Presets.py b/worlds/musedash/Presets.py index 64591118..8dd8507d 100644 --- a/worlds/musedash/Presets.py +++ b/worlds/musedash/Presets.py @@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": False, "starting_song_count": 5, "additional_song_count": 34, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, @@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": True, "starting_song_count": 5, "additional_song_count": 34, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, @@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": True, "starting_song_count": 8, "additional_song_count": 91, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index af2d4cc2..1c009bfa 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -57,6 +57,8 @@ class MuseDashWorld(World): # Necessary Data md_collection = MuseDashCollections() + filler_item_names = list(md_collection.filler_item_weights.keys()) + filler_item_weights = list(md_collection.filler_item_weights.values()) item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()} location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()} @@ -70,7 +72,7 @@ class MuseDashWorld(World): def generate_early(self): dlc_songs = {key for key in self.options.dlc_packs.value} - if (self.options.allow_just_as_planned_dlc_songs.value): + if self.options.allow_just_as_planned_dlc_songs.value: dlc_songs.add(self.md_collection.MUSE_PLUS_DLC) streamer_mode = self.options.streamer_mode_enabled @@ -84,7 +86,7 @@ class MuseDashWorld(World): while True: # In most cases this should only need to run once available_song_keys = self.md_collection.get_songs_with_settings( - dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold) + dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold) available_song_keys = self.handle_plando(available_song_keys) @@ -161,19 +163,17 @@ class MuseDashWorld(World): break self.included_songs.append(available_song_keys.pop()) - self.location_count = len(self.starting_songs) + len(self.included_songs) - location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0) - self.location_count = floor(self.location_count * location_multiplier) - - minimum_location_count = len(self.included_songs) + self.get_music_sheet_count() - if self.location_count < minimum_location_count: - self.location_count = minimum_location_count + self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs)) def create_item(self, name: str) -> Item: if name == self.md_collection.MUSIC_SHEET_NAME: return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing, self.md_collection.MUSIC_SHEET_CODE, self.player) + filler = self.md_collection.filler_items.get(name) + if filler: + return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player) + trap = self.md_collection.vfx_trap_items.get(name) if trap: return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) @@ -189,6 +189,9 @@ class MuseDashWorld(World): song = self.md_collection.song_items.get(name) return MuseDashSongItem(name, self.player, song) + def get_filler_item_name(self) -> str: + return self.random.choices(self.filler_item_names, self.filler_item_weights)[0] + def create_items(self) -> None: song_keys_in_pool = self.included_songs.copy() @@ -199,8 +202,13 @@ class MuseDashWorld(World): for _ in range(0, item_count): self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME)) - # Then add all traps - trap_count = self.get_trap_count() + # Then add 1 copy of every song + item_count += len(self.included_songs) + for song in self.included_songs: + self.multiworld.itempool.append(self.create_item(song)) + + # Then add all traps, making sure we don't over fill + trap_count = min(self.location_count - item_count, self.get_trap_count()) trap_list = self.get_available_traps() if len(trap_list) > 0 and trap_count > 0: for _ in range(0, trap_count): @@ -209,23 +217,38 @@ class MuseDashWorld(World): item_count += trap_count - # Next fill all remaining slots with song items - needed_item_count = self.location_count - while item_count < needed_item_count: - # If we have more items needed than keys, just iterate the list and add them all - if len(song_keys_in_pool) <= needed_item_count - item_count: - for key in song_keys_in_pool: - self.multiworld.itempool.append(self.create_item(key)) + # At this point, if a player is using traps, it's possible that they have filled all locations + items_left = self.location_count - item_count + if items_left <= 0: + return - item_count += len(song_keys_in_pool) - continue + # When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs. + # First fill 50% with the filler. The rest is to be duplicate songs. + filler_count = floor(0.5 * items_left) + items_left -= filler_count - # Otherwise add a random assortment of songs - self.random.shuffle(song_keys_in_pool) - for i in range(0, needed_item_count - item_count): - self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i])) + for _ in range(0, filler_count): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) - item_count = needed_item_count + # All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression + # to cut down on the number of progression items that Muse Dash puts into the pool. + + # This is for the extraordinary case of needing to fill a lot of items. + while items_left > len(song_keys_in_pool): + for key in song_keys_in_pool: + item = self.create_item(key) + item.classification = ItemClassification.useful + self.multiworld.itempool.append(item) + + items_left -= len(song_keys_in_pool) + continue + + # Otherwise add a random assortment of songs + self.random.shuffle(song_keys_in_pool) + for i in range(0, items_left): + item = self.create_item(song_keys_in_pool[i]) + item.classification = ItemClassification.useful + self.multiworld.itempool.append(item) def create_regions(self) -> None: menu_region = Region("Menu", self.player, self.multiworld) @@ -245,8 +268,6 @@ class MuseDashWorld(World): self.random.shuffle(included_song_copy) all_selected_locations.extend(included_song_copy) - two_item_location_count = self.location_count - len(all_selected_locations) - # Make a region per song/album, then adds 1-2 item locations to them for i in range(0, len(all_selected_locations)): name = all_selected_locations[i] @@ -254,10 +275,11 @@ class MuseDashWorld(World): self.multiworld.regions.append(region) song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player)) - # Up to 2 Locations are defined per song - region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation) - if i < two_item_location_count: - region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation) + # Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler. + region.add_locations({ + name + "-0": self.md_collection.song_locations[name + "-0"], + name + "-1": self.md_collection.song_locations[name + "-1"] + }, MuseDashLocation) def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: \ @@ -276,19 +298,14 @@ class MuseDashWorld(World): return trap_list - def get_additional_item_percentage(self) -> int: - trap_count = self.options.trap_count_percentage.value - song_count = self.options.music_sheet_count_percentage.value - return max(trap_count + song_count, self.options.additional_item_percentage.value) - def get_trap_count(self) -> int: multiplier = self.options.trap_count_percentage.value / 100.0 - trap_count = (len(self.starting_songs) * 2) + len(self.included_songs) + trap_count = len(self.starting_songs) + len(self.included_songs) return max(0, floor(trap_count * multiplier)) def get_music_sheet_count(self) -> int: multiplier = self.options.music_sheet_count_percentage.value / 100.0 - song_count = (len(self.starting_songs) * 2) + len(self.included_songs) + song_count = len(self.starting_songs) + len(self.included_songs) return max(1, floor(song_count * multiplier)) def get_music_sheet_win_count(self) -> int: @@ -329,5 +346,4 @@ class MuseDashWorld(World): "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), "gradeNeeded": self.options.grade_needed.value, - "hasFiller": True, } diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index 29a9a3ef..89214d3f 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase): def test_all_difficulty_ranges(self) -> None: muse_dash_world = self.multiworld.worlds[1] dlc_set = {x for x in muse_dash_world.md_collection.DLC} - difficulty_choice = self.multiworld.song_difficulty_mode[1] - difficulty_min = self.multiworld.song_difficulty_min[1] - difficulty_max = self.multiworld.song_difficulty_max[1] + difficulty_choice = muse_dash_world.options.song_difficulty_mode + difficulty_min = muse_dash_world.options.song_difficulty_min + difficulty_max = muse_dash_world.options.song_difficulty_max def test_range(inputRange, lower, upper): self.assertEqual(inputRange[0], lower) diff --git a/worlds/musedash/test/__init__.py b/worlds/musedash/test/__init__.py index 818fd357..c77f9f6a 100644 --- a/worlds/musedash/test/__init__.py +++ b/worlds/musedash/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class MuseDashTestBase(WorldTestBase):