Muse Dash: Add filler items and rework generation balance (#2809)

This commit is contained in:
Justus Lind 2024-04-15 04:23:13 +10:00 committed by GitHub
parent 7c44d749d4
commit d1274c12b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 61 deletions

View File

@ -66,7 +66,19 @@ class MuseDashCollections:
"Error SFX Trap": STARTING_CODE + 9, "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) location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
def __init__(self) -> None: def __init__(self) -> None:

View File

@ -4,11 +4,13 @@ from dataclasses import dataclass
from .MuseDashCollection import MuseDashCollections from .MuseDashCollection import MuseDashCollections
class AllowJustAsPlannedDLCSongs(Toggle): class AllowJustAsPlannedDLCSongs(Toggle):
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs. """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.""" Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
display_name = "Allow [Muse Plus] DLC Songs" display_name = "Allow [Muse Plus] DLC Songs"
class DLCMusicPacks(OptionSet): class DLCMusicPacks(OptionSet):
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs.""" """Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
display_name = "DLC Packs" display_name = "DLC Packs"
@ -101,20 +103,10 @@ class GradeNeeded(Choice):
default = 0 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): class MusicSheetCountPercentage(Range):
"""Collecting enough Music Sheets will unlock the goal song needed for completion. """Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
This option controls how many are in the item pool, based on the total number of songs.""" Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
"""
range_start = 10 range_start = 10
range_end = 40 range_end = 40
default = 20 default = 20
@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
streamer_mode_enabled: StreamerModeEnabled streamer_mode_enabled: StreamerModeEnabled
starting_song_count: StartingSongs starting_song_count: StartingSongs
additional_song_count: AdditionalSongs additional_song_count: AdditionalSongs
additional_item_percentage: AdditionalItemPercentage
song_difficulty_mode: DifficultyMode song_difficulty_mode: DifficultyMode
song_difficulty_min: DifficultyModeOverrideMin song_difficulty_min: DifficultyModeOverrideMin
song_difficulty_max: DifficultyModeOverrideMax song_difficulty_max: DifficultyModeOverrideMax

View File

@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": False, "allow_just_as_planned_dlc_songs": False,
"starting_song_count": 5, "starting_song_count": 5,
"additional_song_count": 34, "additional_song_count": 34,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20, "music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90, "music_sheet_win_count_percentage": 90,
}, },
@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": True, "allow_just_as_planned_dlc_songs": True,
"starting_song_count": 5, "starting_song_count": 5,
"additional_song_count": 34, "additional_song_count": 34,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20, "music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90, "music_sheet_win_count_percentage": 90,
}, },
@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": True, "allow_just_as_planned_dlc_songs": True,
"starting_song_count": 8, "starting_song_count": 8,
"additional_song_count": 91, "additional_song_count": 91,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20, "music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90, "music_sheet_win_count_percentage": 90,
}, },

View File

@ -57,6 +57,8 @@ class MuseDashWorld(World):
# Necessary Data # Necessary Data
md_collection = MuseDashCollections() 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()} 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()} 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): def generate_early(self):
dlc_songs = {key for key in self.options.dlc_packs.value} 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) dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
streamer_mode = self.options.streamer_mode_enabled streamer_mode = self.options.streamer_mode_enabled
@ -84,7 +86,7 @@ class MuseDashWorld(World):
while True: while True:
# In most cases this should only need to run once # In most cases this should only need to run once
available_song_keys = self.md_collection.get_songs_with_settings( 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) available_song_keys = self.handle_plando(available_song_keys)
@ -161,19 +163,17 @@ class MuseDashWorld(World):
break break
self.included_songs.append(available_song_keys.pop()) self.included_songs.append(available_song_keys.pop())
self.location_count = len(self.starting_songs) + len(self.included_songs) self.location_count = 2 * (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
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
if name == self.md_collection.MUSIC_SHEET_NAME: if name == self.md_collection.MUSIC_SHEET_NAME:
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing, return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
self.md_collection.MUSIC_SHEET_CODE, self.player) 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) trap = self.md_collection.vfx_trap_items.get(name)
if trap: if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
@ -189,6 +189,9 @@ class MuseDashWorld(World):
song = self.md_collection.song_items.get(name) song = self.md_collection.song_items.get(name)
return MuseDashSongItem(name, self.player, song) 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: def create_items(self) -> None:
song_keys_in_pool = self.included_songs.copy() song_keys_in_pool = self.included_songs.copy()
@ -199,8 +202,13 @@ class MuseDashWorld(World):
for _ in range(0, item_count): for _ in range(0, item_count):
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME)) self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
# Then add all traps # Then add 1 copy of every song
trap_count = self.get_trap_count() 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() trap_list = self.get_available_traps()
if len(trap_list) > 0 and trap_count > 0: if len(trap_list) > 0 and trap_count > 0:
for _ in range(0, trap_count): for _ in range(0, trap_count):
@ -209,23 +217,38 @@ class MuseDashWorld(World):
item_count += trap_count item_count += trap_count
# Next fill all remaining slots with song items # At this point, if a player is using traps, it's possible that they have filled all locations
needed_item_count = self.location_count items_left = self.location_count - item_count
while item_count < needed_item_count: if items_left <= 0:
# If we have more items needed than keys, just iterate the list and add them all return
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))
item_count += len(song_keys_in_pool) # When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
continue # 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 for _ in range(0, filler_count):
self.random.shuffle(song_keys_in_pool) self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
for i in range(0, needed_item_count - item_count):
self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
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: def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld) menu_region = Region("Menu", self.player, self.multiworld)
@ -245,8 +268,6 @@ class MuseDashWorld(World):
self.random.shuffle(included_song_copy) self.random.shuffle(included_song_copy)
all_selected_locations.extend(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 # Make a region per song/album, then adds 1-2 item locations to them
for i in range(0, len(all_selected_locations)): for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i] name = all_selected_locations[i]
@ -254,10 +275,11 @@ class MuseDashWorld(World):
self.multiworld.regions.append(region) self.multiworld.regions.append(region)
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player)) song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
# Up to 2 Locations are defined per song # 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"]}, MuseDashLocation) region.add_locations({
if i < two_item_location_count: name + "-0": self.md_collection.song_locations[name + "-0"],
region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation) name + "-1": self.md_collection.song_locations[name + "-1"]
}, MuseDashLocation)
def set_rules(self) -> None: def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \ self.multiworld.completion_condition[self.player] = lambda state: \
@ -276,19 +298,14 @@ class MuseDashWorld(World):
return trap_list 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: def get_trap_count(self) -> int:
multiplier = self.options.trap_count_percentage.value / 100.0 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)) return max(0, floor(trap_count * multiplier))
def get_music_sheet_count(self) -> int: def get_music_sheet_count(self) -> int:
multiplier = self.options.music_sheet_count_percentage.value / 100.0 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)) return max(1, floor(song_count * multiplier))
def get_music_sheet_win_count(self) -> int: def get_music_sheet_win_count(self) -> int:
@ -329,5 +346,4 @@ class MuseDashWorld(World):
"deathLink": self.options.death_link.value, "deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(), "musicSheetWinCount": self.get_music_sheet_win_count(),
"gradeNeeded": self.options.grade_needed.value, "gradeNeeded": self.options.grade_needed.value,
"hasFiller": True,
} }

View File

@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None: def test_all_difficulty_ranges(self) -> None:
muse_dash_world = self.multiworld.worlds[1] muse_dash_world = self.multiworld.worlds[1]
dlc_set = {x for x in muse_dash_world.md_collection.DLC} dlc_set = {x for x in muse_dash_world.md_collection.DLC}
difficulty_choice = self.multiworld.song_difficulty_mode[1] difficulty_choice = muse_dash_world.options.song_difficulty_mode
difficulty_min = self.multiworld.song_difficulty_min[1] difficulty_min = muse_dash_world.options.song_difficulty_min
difficulty_max = self.multiworld.song_difficulty_max[1] difficulty_max = muse_dash_world.options.song_difficulty_max
def test_range(inputRange, lower, upper): def test_range(inputRange, lower, upper):
self.assertEqual(inputRange[0], lower) self.assertEqual(inputRange[0], lower)

View File

@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase from test.bases import WorldTestBase
class MuseDashTestBase(WorldTestBase): class MuseDashTestBase(WorldTestBase):