From 6803c373e5ff738914c362b5e7a158fd528f54f7 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 8 Aug 2024 13:33:13 -0500 Subject: [PATCH] HK: add grub hunt goal (#3203) * makes grub hunt goal option that calculates the total available grubs (including item link replacements) and requires all of them to be gathered for goal completion * update slot data name for grub count * add option to set number needed for grub hub * updates to grub hunt goal based on review * copy/paste fix * account for 'any' goal and fix overriding non-grub goals * making sure godhome is in logic for any and removing redundancy on completion condition * fix typing * i hate typing * move to stage_pre_fill * modify "any" goal so all goals are in logic under minimal settings * rewrite grub counting to create lookups for grubs and groups that can be reused * use generator instead of list comprehension * fix whitespace merging wrong * minor code cleanup --- worlds/hk/Options.py | 13 ++++++++- worlds/hk/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index e2602036a24e..c1206d41ee2c 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -405,9 +405,20 @@ class Goal(Choice): option_radiance = 3 option_godhome = 4 option_godhome_flower = 5 + option_grub_hunt = 6 default = 0 +class GrubHuntGoal(NamedRange): + """The amount of grubs required to finish Grub Hunt. + On 'All' any grubs from item links replacements etc. will be counted""" + display_name = "Grub Hunt Goal" + range_start = 1 + range_end = 46 + special_range_names = {"all": -1} + default = 46 + + class WhitePalace(Choice): """ Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be @@ -522,7 +533,7 @@ class CostSanityHybridChance(Range): **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, + StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index e5065876ddf3..99277378a162 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,6 +5,7 @@ from copy import deepcopy import itertools import operator +from collections import defaultdict, Counter logger = logging.getLogger("Hollow Knight") @@ -12,12 +13,12 @@ from .Regions import create_regions from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option, HKOptions + shop_to_option, HKOptions, GrubHuntGoal from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification +from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld path_of_pain_locations = { @@ -155,6 +156,7 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} + grub_count: int def __init__(self, multiworld, player): super(HKWorld, self).__init__(multiworld, player) @@ -164,6 +166,7 @@ def __init__(self, multiworld, player): self.ranges = {} self.created_shop_items = 0 self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) + self.grub_count = 0 def generate_early(self): options = self.options @@ -201,7 +204,7 @@ def create_regions(self): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower, Goal.option_any]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -441,12 +444,67 @@ def set_rules(self): multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + elif goal == Goal.option_grub_hunt: + pass # will set in stage_pre_fill() else: # Any goal - multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \ + _hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) set_rules(self) + @classmethod + def stage_pre_fill(cls, multiworld: "MultiWorld"): + def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): + world = multiworld.worlds[player] + + if world.options.Goal == "grub_hunt": + multiworld.completion_condition[player] = grub_rule + else: + old_rule = multiworld.completion_condition[player] + multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state) + + worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] + if worlds: + grubs = [item for item in multiworld.get_items() if item.name == "Grub"] + all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + + if all_grub_players: + group_lookup = defaultdict(set) + for group_id, group in multiworld.groups.items(): + for player in group["players"]: + group_lookup[group_id].add(player) + + grub_count_per_player = Counter() + per_player_grubs_per_player = defaultdict(Counter) + + for grub in grubs: + player = grub.player + if player in group_lookup: + for real_player in group_lookup[player]: + per_player_grubs_per_player[real_player][player] += 1 + else: + per_player_grubs_per_player[player][player] += 1 + + if grub.location and grub.location.player in group_lookup.keys(): + for real_player in group_lookup[grub.location.player]: + grub_count_per_player[real_player] += 1 + else: + grub_count_per_player[player] += 1 + + for player, count in grub_count_per_player.items(): + multiworld.worlds[player].grub_count = count + + for player, grub_player_count in per_player_grubs_per_player.items(): + if player in all_grub_players: + set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items())) + + for world in worlds: + if world.player not in all_grub_players: + world.grub_count = world.options.GrubHuntGoal.value + player = world.player + set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c)) + def fill_slot_data(self): slot_data = {} @@ -484,6 +542,8 @@ def fill_slot_data(self): slot_data["notch_costs"] = self.charm_costs + slot_data["grub_count"] = self.grub_count + return slot_data def create_item(self, name: str) -> HKItem: