[OC2] Location Balancing (#1458)
This commit is contained in:
parent
18c4b4b1fe
commit
ce2553a2b3
|
@ -1,13 +1,36 @@
|
|||
from enum import Enum
|
||||
from typing import TypedDict
|
||||
from Options import DefaultOnToggle, Range, Choice
|
||||
|
||||
|
||||
class LocationBalancingMode(Enum):
|
||||
disabled = 0
|
||||
compromise = 1
|
||||
full = 2
|
||||
|
||||
|
||||
class OC2OnToggle(DefaultOnToggle):
|
||||
@property
|
||||
def result(self) -> bool:
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class LocationBalancing(Choice):
|
||||
"""Location balancing affects the density of progression items found in your world relative to other wordlds. This setting changes nothing for solo games.
|
||||
|
||||
- Disabled: Location density in your world can fluctuate greatly depending on the settings of other players. In extreme cases, your world may be entirely populated with filler items
|
||||
|
||||
- Compromise: Locations are balanced to a midpoint between "fair" and "natural"
|
||||
|
||||
- Full: Locations are balanced in an attempt to make the number of progression items sent out and received equal over the entire game"""
|
||||
auto_display_name = True
|
||||
display_name = "Location Balancing"
|
||||
option_disabled = LocationBalancingMode.disabled.value
|
||||
option_compromise = LocationBalancingMode.compromise.value
|
||||
option_full = LocationBalancingMode.full.value
|
||||
default = LocationBalancingMode.compromise.value
|
||||
|
||||
|
||||
class AlwaysServeOldestOrder(OC2OnToggle):
|
||||
"""Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps
|
||||
players dig out of a broken tip combo faster."""
|
||||
|
@ -105,6 +128,9 @@ class StarThresholdScale(Range):
|
|||
|
||||
|
||||
overcooked_options = {
|
||||
# generator options
|
||||
"location_balancing": LocationBalancing,
|
||||
|
||||
# randomization options
|
||||
"shuffle_level_order": ShuffleLevelOrder,
|
||||
"include_horde_levels": IncludeHordeLevels,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from enum import Enum
|
||||
from typing import Callable, Dict, Any, List, Optional
|
||||
|
||||
from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, Tutorial
|
||||
from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, Tutorial, LocationProgressType
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel, ITEMS_TO_EXCLUDE_IF_NO_DLC
|
||||
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
|
||||
from .Options import overcooked_options, OC2Options, OC2OnToggle
|
||||
from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode
|
||||
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies
|
||||
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful
|
||||
|
||||
|
@ -60,7 +60,6 @@ class Overcooked2World(World):
|
|||
options: Dict[str, Any]
|
||||
itempool: List[Overcooked2Item]
|
||||
|
||||
|
||||
# Helper Functions
|
||||
|
||||
def is_level_horde(self, level_id: int) -> bool:
|
||||
|
@ -106,6 +105,7 @@ class Overcooked2World(World):
|
|||
level_id: int,
|
||||
stars: int,
|
||||
is_event: bool = False,
|
||||
priority=False,
|
||||
) -> None:
|
||||
|
||||
if is_event:
|
||||
|
@ -123,6 +123,11 @@ class Overcooked2World(World):
|
|||
|
||||
location.event = is_event
|
||||
|
||||
if priority:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
else:
|
||||
location.progress_type = LocationProgressType.DEFAULT
|
||||
|
||||
# if level_id is none, then it's the 6-6 edge case
|
||||
if level_id is None:
|
||||
level_id = 36
|
||||
|
@ -145,6 +150,76 @@ class Overcooked2World(World):
|
|||
if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value
|
||||
for name, option in overcooked_options.items()})
|
||||
|
||||
def get_n_random_locations(self, n: int) -> List[int]:
|
||||
"""Return a list of n random non-repeating level locations"""
|
||||
levels = list()
|
||||
|
||||
if n == 0:
|
||||
return levels
|
||||
|
||||
for level in Overcooked2Level():
|
||||
if level.level_id == 36:
|
||||
continue
|
||||
elif not self.options["KevinLevels"] and level.level_id > 36:
|
||||
break
|
||||
|
||||
levels.append(level.level_id)
|
||||
|
||||
self.multiworld.random.shuffle(levels)
|
||||
return levels[:n]
|
||||
|
||||
def get_priority_locations(self) -> List[int]:
|
||||
"""Randomly generate list of priority locations, thus insulating this game
|
||||
from the negative effects of being shuffled with games that contain large
|
||||
ammounts of filler"""
|
||||
|
||||
if self.multiworld.players == 1:
|
||||
# random priority locations have no desirable effect on solo seeds
|
||||
return list()
|
||||
|
||||
balancing_mode = self.get_options()["LocationBalancing"]
|
||||
|
||||
if balancing_mode == LocationBalancingMode.disabled.value:
|
||||
# Location balancing is disabled, progression density is purely determined by filler
|
||||
return list()
|
||||
|
||||
# Count how many progression items are required for this overcooked instance
|
||||
game_item_count = len(self.itempool)
|
||||
game_progression_count = 0
|
||||
for item in self.itempool:
|
||||
if item.classification == ItemClassification.progression:
|
||||
game_progression_count += 1
|
||||
game_progression_density = game_progression_count/game_item_count
|
||||
|
||||
if balancing_mode == LocationBalancingMode.full.value:
|
||||
# Location balancing will be employed in an attempt to keep the number of
|
||||
# progression locations and proression items as close to equal as possible
|
||||
return self.get_n_random_locations(game_progression_count)
|
||||
|
||||
assert balancing_mode == LocationBalancingMode.compromise.value
|
||||
|
||||
# Count how many progression items are shuffled between all games
|
||||
total_item_count = len(self.multiworld.itempool)
|
||||
total_progression_count = 0
|
||||
|
||||
for item in self.multiworld.itempool:
|
||||
if item.classification == ItemClassification.progression:
|
||||
total_progression_count += 1
|
||||
total_progression_density = total_progression_count/total_item_count
|
||||
|
||||
if total_progression_density >= game_progression_density:
|
||||
# This game has more filler than the average of all other games.
|
||||
# It is not in need of location balancing
|
||||
return list()
|
||||
|
||||
# Calculate the midpoint between the two ratios
|
||||
target_progression_ratio = (game_progression_density - total_progression_density) / 2.0 + total_progression_density
|
||||
target_progression_count = int((target_progression_ratio * game_item_count) + 0.5) # I'm sorry I round like an old person
|
||||
|
||||
# Location balancing will be employed in an attempt to find a compromise at
|
||||
# the half-way point between natural probability and full artifical balancing
|
||||
return self.get_n_random_locations(target_progression_count)
|
||||
|
||||
# Helper Data
|
||||
|
||||
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
|
||||
|
@ -173,12 +248,20 @@ class Overcooked2World(World):
|
|||
else:
|
||||
self.level_mapping = None
|
||||
|
||||
def set_location_priority(self) -> None:
|
||||
for level in Overcooked2Level():
|
||||
if level.level_id in self.get_priority_locations():
|
||||
location: Location = self.multiworld.get_location(level.location_name_item, self.player)
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Menu -> Overworld
|
||||
self.add_region("Menu")
|
||||
self.add_region("Overworld")
|
||||
self.connect_regions("Menu", "Overworld")
|
||||
|
||||
# Create and populate "regions" (a.k.a. levels)
|
||||
for level in Overcooked2Level():
|
||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||
break
|
||||
|
@ -249,6 +332,7 @@ class Overcooked2World(World):
|
|||
state.has("Victory", self.player)
|
||||
self.multiworld.completion_condition[self.player] = completion_condition
|
||||
|
||||
|
||||
def create_items(self):
|
||||
self.itempool = []
|
||||
|
||||
|
@ -264,17 +348,15 @@ class Overcooked2World(World):
|
|||
if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
|
||||
# skip horde-specific items if no horde levels
|
||||
continue
|
||||
|
||||
|
||||
if not self.options["KevinLevels"] and item_name.startswith("Kevin"):
|
||||
# skip kevin items if no kevin levels
|
||||
continue
|
||||
|
||||
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
|
||||
# print(f"{item_name} is progression")
|
||||
# progression.append(item_name)
|
||||
classification = ItemClassification.progression
|
||||
else:
|
||||
# print(f"{item_name} is filler")
|
||||
if (is_useful(item_name)):
|
||||
# useful.append(item_name)
|
||||
classification = ItemClassification.useful
|
||||
|
@ -306,10 +388,8 @@ class Overcooked2World(World):
|
|||
|
||||
self.multiworld.itempool += self.itempool
|
||||
|
||||
def set_rules(self):
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
def place_events(self):
|
||||
# Add Events (Star Acquisition)
|
||||
for level in Overcooked2Level():
|
||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||
|
@ -330,6 +410,13 @@ class Overcooked2World(World):
|
|||
# Add Victory Condition
|
||||
self.place_event("6-6 Completed", "Victory")
|
||||
|
||||
def set_rules(self):
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.place_events()
|
||||
self.set_location_priority()
|
||||
|
||||
# Items get distributed to locations
|
||||
|
||||
def fill_json_data(self) -> Dict[str, Any]:
|
||||
|
|
Loading…
Reference in New Issue