[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 typing import TypedDict
|
||||||
from Options import DefaultOnToggle, Range, Choice
|
from Options import DefaultOnToggle, Range, Choice
|
||||||
|
|
||||||
|
|
||||||
|
class LocationBalancingMode(Enum):
|
||||||
|
disabled = 0
|
||||||
|
compromise = 1
|
||||||
|
full = 2
|
||||||
|
|
||||||
|
|
||||||
class OC2OnToggle(DefaultOnToggle):
|
class OC2OnToggle(DefaultOnToggle):
|
||||||
@property
|
@property
|
||||||
def result(self) -> bool:
|
def result(self) -> bool:
|
||||||
return bool(self.value)
|
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):
|
class AlwaysServeOldestOrder(OC2OnToggle):
|
||||||
"""Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps
|
"""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."""
|
players dig out of a broken tip combo faster."""
|
||||||
|
@ -105,6 +128,9 @@ class StarThresholdScale(Range):
|
||||||
|
|
||||||
|
|
||||||
overcooked_options = {
|
overcooked_options = {
|
||||||
|
# generator options
|
||||||
|
"location_balancing": LocationBalancing,
|
||||||
|
|
||||||
# randomization options
|
# randomization options
|
||||||
"shuffle_level_order": ShuffleLevelOrder,
|
"shuffle_level_order": ShuffleLevelOrder,
|
||||||
"include_horde_levels": IncludeHordeLevels,
|
"include_horde_levels": IncludeHordeLevels,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable, Dict, Any, List, Optional
|
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 worlds.AutoWorld import World, WebWorld
|
||||||
|
|
||||||
from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel, ITEMS_TO_EXCLUDE_IF_NO_DLC
|
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 .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 .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
|
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]
|
options: Dict[str, Any]
|
||||||
itempool: List[Overcooked2Item]
|
itempool: List[Overcooked2Item]
|
||||||
|
|
||||||
|
|
||||||
# Helper Functions
|
# Helper Functions
|
||||||
|
|
||||||
def is_level_horde(self, level_id: int) -> bool:
|
def is_level_horde(self, level_id: int) -> bool:
|
||||||
|
@ -106,6 +105,7 @@ class Overcooked2World(World):
|
||||||
level_id: int,
|
level_id: int,
|
||||||
stars: int,
|
stars: int,
|
||||||
is_event: bool = False,
|
is_event: bool = False,
|
||||||
|
priority=False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if is_event:
|
if is_event:
|
||||||
|
@ -123,6 +123,11 @@ class Overcooked2World(World):
|
||||||
|
|
||||||
location.event = is_event
|
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, then it's the 6-6 edge case
|
||||||
if level_id is None:
|
if level_id is None:
|
||||||
level_id = 36
|
level_id = 36
|
||||||
|
@ -145,6 +150,76 @@ class Overcooked2World(World):
|
||||||
if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value
|
if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value
|
||||||
for name, option in overcooked_options.items()})
|
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
|
# Helper Data
|
||||||
|
|
||||||
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
|
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
|
||||||
|
@ -173,12 +248,20 @@ class Overcooked2World(World):
|
||||||
else:
|
else:
|
||||||
self.level_mapping = None
|
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:
|
def create_regions(self) -> None:
|
||||||
# Menu -> Overworld
|
# Menu -> Overworld
|
||||||
self.add_region("Menu")
|
self.add_region("Menu")
|
||||||
self.add_region("Overworld")
|
self.add_region("Overworld")
|
||||||
self.connect_regions("Menu", "Overworld")
|
self.connect_regions("Menu", "Overworld")
|
||||||
|
|
||||||
|
# Create and populate "regions" (a.k.a. levels)
|
||||||
for level in Overcooked2Level():
|
for level in Overcooked2Level():
|
||||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||||
break
|
break
|
||||||
|
@ -249,6 +332,7 @@ class Overcooked2World(World):
|
||||||
state.has("Victory", self.player)
|
state.has("Victory", self.player)
|
||||||
self.multiworld.completion_condition[self.player] = completion_condition
|
self.multiworld.completion_condition[self.player] = completion_condition
|
||||||
|
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
self.itempool = []
|
self.itempool = []
|
||||||
|
|
||||||
|
@ -270,11 +354,9 @@ class Overcooked2World(World):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
|
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
|
||||||
# print(f"{item_name} is progression")
|
|
||||||
# progression.append(item_name)
|
# progression.append(item_name)
|
||||||
classification = ItemClassification.progression
|
classification = ItemClassification.progression
|
||||||
else:
|
else:
|
||||||
# print(f"{item_name} is filler")
|
|
||||||
if (is_useful(item_name)):
|
if (is_useful(item_name)):
|
||||||
# useful.append(item_name)
|
# useful.append(item_name)
|
||||||
classification = ItemClassification.useful
|
classification = ItemClassification.useful
|
||||||
|
@ -306,10 +388,8 @@ class Overcooked2World(World):
|
||||||
|
|
||||||
self.multiworld.itempool += self.itempool
|
self.multiworld.itempool += self.itempool
|
||||||
|
|
||||||
def set_rules(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def generate_basic(self) -> None:
|
def place_events(self):
|
||||||
# Add Events (Star Acquisition)
|
# Add Events (Star Acquisition)
|
||||||
for level in Overcooked2Level():
|
for level in Overcooked2Level():
|
||||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||||
|
@ -330,6 +410,13 @@ class Overcooked2World(World):
|
||||||
# Add Victory Condition
|
# Add Victory Condition
|
||||||
self.place_event("6-6 Completed", "Victory")
|
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
|
# Items get distributed to locations
|
||||||
|
|
||||||
def fill_json_data(self) -> Dict[str, Any]:
|
def fill_json_data(self) -> Dict[str, Any]:
|
||||||
|
|
Loading…
Reference in New Issue