Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HK: add grub hunt goal #3203

Merged
merged 15 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion worlds/hk/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
68 changes: 64 additions & 4 deletions worlds/hk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
from copy import deepcopy
import itertools
import operator
from collections import defaultdict, Counter

logger = logging.getLogger("Hollow Knight")

from .Items import item_table, lookup_type_to_names, item_name_groups
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 = {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should populate slot data if the goal is Any as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if grub hunt is a valid 'any' goal, how do you want to handle the completion condition?
leave as-is but send the count in slot data so we know when to send goal complete?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I would do it that way just so that minimal + any doesn't accidentally force something stupid (we'll leave that for minimal + random goal)

Copy link
Collaborator

@BadMagic100 BadMagic100 May 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually maybe a better approach for any goal would be to ensure every goal is completable in logic so that even on minimal the player would have any choice of goal

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 = {}

Expand Down Expand Up @@ -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:
Expand Down
Loading