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
This commit is contained in:
qwint 2024-08-08 13:33:13 -05:00 committed by GitHub
parent 575c338aa3
commit 6803c373e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 5 deletions

View File

@ -405,9 +405,20 @@ class Goal(Choice):
option_radiance = 3 option_radiance = 3
option_godhome = 4 option_godhome = 4
option_godhome_flower = 5 option_godhome_flower = 5
option_grub_hunt = 6
default = 0 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): class WhitePalace(Choice):
""" """
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
@ -522,7 +533,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
**{ **{
option.__name__: option option.__name__: option
for option in ( for option in (
StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo,
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
MinimumGeoPrice, MaximumGeoPrice, MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice, MinimumGrubPrice, MaximumGrubPrice,

View File

@ -5,6 +5,7 @@ import typing
from copy import deepcopy from copy import deepcopy
import itertools import itertools
import operator import operator
from collections import defaultdict, Counter
logger = logging.getLogger("Hollow Knight") logger = logging.getLogger("Hollow Knight")
@ -12,12 +13,12 @@ from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions from .Regions import create_regions
from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance 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, \ 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, \ from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names 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 from worlds.AutoWorld import World, LogicMixin, WebWorld
path_of_pain_locations = { path_of_pain_locations = {
@ -155,6 +156,7 @@ class HKWorld(World):
ranges: typing.Dict[str, typing.Tuple[int, int]] ranges: typing.Dict[str, typing.Tuple[int, int]]
charm_costs: typing.List[int] charm_costs: typing.List[int]
cached_filler_items = {} cached_filler_items = {}
grub_count: int
def __init__(self, multiworld, player): def __init__(self, multiworld, player):
super(HKWorld, self).__init__(multiworld, player) super(HKWorld, self).__init__(multiworld, player)
@ -164,6 +166,7 @@ class HKWorld(World):
self.ranges = {} self.ranges = {}
self.created_shop_items = 0 self.created_shop_items = 0
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
self.grub_count = 0
def generate_early(self): def generate_early(self):
options = self.options options = self.options
@ -201,7 +204,7 @@ class HKWorld(World):
# check for any goal that godhome events are relevant to # check for any goal that godhome events are relevant to
all_event_names = event_names.copy() 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 from .GodhomeData import godhome_event_names
all_event_names.update(set(godhome_event_names)) all_event_names.update(set(godhome_event_names))
@ -441,12 +444,67 @@ class HKWorld(World):
multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
elif goal == Goal.option_godhome_flower: elif goal == Goal.option_godhome_flower:
multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) 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: else:
# Any goal # 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) 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): def fill_slot_data(self):
slot_data = {} slot_data = {}
@ -484,6 +542,8 @@ class HKWorld(World):
slot_data["notch_costs"] = self.charm_costs slot_data["notch_costs"] = self.charm_costs
slot_data["grub_count"] = self.grub_count
return slot_data return slot_data
def create_item(self, name: str) -> HKItem: def create_item(self, name: str) -> HKItem: