[OC2] Location Balancing (#1458)

This commit is contained in:
toasterparty 2023-02-17 00:21:56 -08:00 committed by GitHub
parent 18c4b4b1fe
commit ce2553a2b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 122 additions and 9 deletions

View File

@ -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,

View File

@ -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]: