Skip to content
Open
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
197 changes: 178 additions & 19 deletions worlds/hk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from copy import deepcopy
from typing import Any, ClassVar

from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld
from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region, Location
from entrance_rando import EntranceRandomizationError, randomize_entrances
from Options import OptionError

Expand Down Expand Up @@ -71,6 +71,10 @@ class HKWorld(RandomizerCoreWorld):
item_name_groups = datapackage_item_groups
location_name_groups = datapackage_location_groups

#UT Attributes
ut_can_gen_without_yaml = True
found_entrances_datastorage_key = "Slot:{player}:visited_transitions" #A Template string, not an f-string

rc_regions: list[dict[str, Any]] = hk_regions
rc_locations: list[dict[str, Any]] = hk_locations
item_class = HKItem
Expand Down Expand Up @@ -112,10 +116,35 @@ def __init__(self, multiworld, player):
self.entrance_by_term = defaultdict(list)
self.entrance_pairs = {}
self.entrance_state_modifier_by_term = defaultdict(list)
self.pre_defined_location_costs = {}

# generate_early
def generate_early(self):

options = self.options

if hasattr(self.multiworld,"re_gen_passthrough") and self.game in getattr(self.multiworld,"re_gen_passthrough"): #UT slot_data
slot_data = getattr(self.multiworld,"re_gen_passthrough")[self.game]
remote_options = slot_data["options"]
for option_name, option_value in remote_options.items():
if hasattr(options,option_name):
setattr(getattr(options,option_name),"value",option_value)
self.pre_defined_location_costs=slot_data["location_costs"]
self.charm_costs = slot_data["notch_costs"]

self.grub_count = slot_data["grub_count"]

self.entrance_pairs = slot_data["entrance_pairs"]
else:
charm_costs = options.RandomCharmCosts.get_costs(self.random)
self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
# defaulting so completion condition isn't incorrect before pre_fill
self.grub_count = (
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
else options.GrubHuntGoal.value
)


weights = {
data.term: getattr(options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values()
Expand All @@ -142,8 +171,6 @@ def generate_early(self):
else:
self.ranges[term] = mini.value, maxi.value

charm_costs = options.RandomCharmCosts.get_costs(self.random)
self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
self.charm_names_and_costs = {name: (self.charm_costs[index] if name != "Void_Heart" else 0)
for name, index in charm_name_to_id.items()}
self.soul_modes = self.options.EntranceRandoType.soul_mode
Expand All @@ -167,11 +194,6 @@ def generate_early(self):
]
# actually connect it later once we have regions created

# defaulting so completion condition isn't incorrect before pre_fill
self.grub_count = (
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
else options.GrubHuntGoal.value
)
self.grub_player_count = {self.player: self.grub_count}

@classmethod
Expand Down Expand Up @@ -479,6 +501,8 @@ def _stateless_connect_one_way(self, source_exit: Entrance, target_entrance: Ent
def connect_entrances(self):
if not self.options.EntranceRandoType:
return
if hasattr(self.multiworld, "generation_is_fake"):
return #In UT gen we don't want GER to re-randomize the entrances
coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled

def _connect_entrances(group: str, entrances: list[HKEntrance] | None) -> None:
Expand Down Expand Up @@ -657,6 +681,17 @@ def setup_connections(self):
rule = self.create_rule(rule_data) if rule_data else None
region1.connect(region2, name, rule)

if hasattr(self.multiworld,"generation_is_fake") and getattr(self.multiworld,"enforce_deferred_connections","off") == "off": #UT flag
#In UT generation, we already got connection_pairs from slot_data
for source,target in self.entrance_pairs.items():
exit_obj = self.get_entrance(source)
exit_region = self.get_region(structure_transition_to_region_map[target])
exits = [entrance for entrance in exit_region.entrances if entrance.name==target and entrance.parent_region is None]
if len(exits) != 1:
raise Exception(f"More then one viable target somehow {exits}")
self._stateless_connect_one_way(exit_obj,exits[0])
return

if not one_ways:
return

Expand All @@ -665,6 +700,116 @@ def setup_connections(self):
for entrance, exit in zip(one_ways["OneWayOut"], one_ways["OneWayIn"], strict=True):
self._stateless_connect_one_way(exit, entrance)

def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bool]):
if not data_storage_value:
return
for entrance_checked in data_storage_value.keys():
entrance = self.get_entrance(entrance_checked)
if entrance.connected_region is None:
target = self.entrance_pairs[entrance_checked]
target_region = self.get_region(structure_transition_to_region_map[target])
exits = [entrance for entrance in target_region.entrances if entrance.name==target]
if len(exits) != 1:
raise Exception("More then one viable target somehow")
exit_name = exits[0].name
self._stateless_connect_one_way(entrance,exits[0])
if self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled:
reverse_entrance = self.get_entrance(exit_name)
if reverse_entrance.connected_region is None:
assert entrance.parent_region
reverse_exits = [entrance for entrance in entrance.parent_region.entrances if entrance.name==entrance_checked]
if len(reverse_exits) != 1:
raise Exception("More then one viable target somehow")
self._stateless_connect_one_way(reverse_entrance,reverse_exits[0])

def parse_clause(self, clause:HKClause, parent_region: Region, state: CollectionState) -> list:
l_return = []
for item,count in clause.hk_item_requirements.items():
valid = state.has(item,self.player,count)
l_return.append({"type":"color","color":"green" if valid else "red","text":item if count==1 else f"{item}:{count}"})
l_return.append({"type":"text","text":", "})
for region in clause.hk_region_requirements:
valid = state.can_reach_region(region,self.player)
l_return.append({"type":"color","color":"green" if valid else "red","text":region})
l_return.append({"type":"text","text":", "})
if clause.hk_state_requirements and parent_region:
valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region)
l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)})
l_return.append({"type":"text","text":", "})
return l_return[:-1]

def explain_path(self, entrance: Entrance, state: CollectionState) -> list:
hkClause = getattr(entrance,"hk_rule",None)
if not isinstance(hkClause,list):
return []
l_return = [{"type":"color","color":"blue","text":entrance.name}]
for index,clause in enumerate(hkClause):
if not isinstance(clause,HKClause):
continue #maybe fix later?
l_return.append({"type":"text","text":f"\nClause {index+1} - "})
l_return.extend(self.parse_clause(clause,entrance.parent_region,state))
return l_return

def explain_spot(self, location: Location, state: CollectionState) -> list:
hkClause = getattr(location,"hk_rule",None)
if not isinstance(hkClause,list):
return []
l_return = [{"type":"color","color":"green","text":f" -> {location.name}"}]
for index,clause in enumerate(hkClause):
if not isinstance(clause,HKClause):
continue #maybe fix later?
l_return.append({"type":"text","text":f"\nClause {index+1} - "})
l_return.extend(self.parse_clause(clause,location.parent_region,state))
return l_return


def explain_rule(self, target_name: str, state: CollectionState) -> list:
l_return = []

target = None
parent_region = None
if target_name in self.multiworld.regions.region_cache[self.player]:
target = self.get_region(target_name)
parent_region = target
# Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine
for ent in target.entrances:
l_return.extend(self.explain_path(ent,state))
l_return.append({"type":"text","text":f"\n"})
l_return.pop()
return l_return
elif target_name in self.multiworld.regions.entrance_cache[self.player]:
target = self.get_entrance(target_name)
parent_region = target.parent_region
elif target_name in self.multiworld.regions.location_cache[self.player]:
target = self.get_location(target_name)
parent_region = target.parent_region

if target is None or parent_region is None:
return []
hkClause = getattr(target,"hk_rule",None)
if not isinstance(hkClause,list):
l_return.append({"type":"text","text":"Default Access"})
else:
for index,clause in enumerate(hkClause):
if not isinstance(clause,HKClause):
continue
l_return.append({"type":"text","text":f"\nClause {index+1} - "})
l_return.extend(self.parse_clause(clause,parent_region,state))
costs = getattr(target,"costs",None)
if isinstance(costs,dict):
l_return.append({"type":"text","text":"\nCosts - ["})
for cost,count in costs.items():
valid = False
if cost == "GEO":
valid = state.has("Can_Replenish_Geo", self.player)
else:
valid = state.has(cost,self.player,count)
l_return.append({"type":"color","color":"green" if valid else "red","text":f"{cost}:{count}"})
l_return.append({"type":"text","text":", "})
l_return.pop() #Remove the last comma
l_return.append({"type":"text","text":"]"}) #And replace with a close bracket
return l_return

def add_all_events(self):
location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]}

Expand Down Expand Up @@ -714,17 +859,19 @@ def add_shop_location(self, shop, index=None):
index = len(self.created_multi_locations[shop])
lookup_shop = shop if "(Requires_Charms)" not in shop else "Salubra"
# fix because Salubra_(Requires_Charms) isn't actually in the source data
location_name = f"{shop}_{index+1}"

costs = None
if shop in shop_cost_types:
if location_name in self.pre_defined_location_costs:
costs = self.pre_defined_location_costs[location_name]
elif shop in shop_cost_types:
costs = {
term: self.random.randint(*self.ranges[term])
for term in shop_cost_types[shop]
}

rule = self.rule_lookup.get(lookup_shop)
region = self.get_region(self.region_lookup[lookup_shop])
location_name = f"{shop}_{index+1}"
code = self.location_name_to_id[location_name]

loc = self.location_class(self.player, location_name, code, region)
Expand All @@ -748,22 +895,29 @@ def add_shop_locations(self):

def add_extra_shop_locations(self, count):
# Add additional shop items, as needed.
if not count > 0:
gen_is_fake = hasattr(self.multiworld, "generation_is_fake")
if not count > 0 and not gen_is_fake:
return
shops = [shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16]
if not self.options.EggShopSlots.value: # No eggshop, so don't place items there
shops.remove("Egg_Shop")

if not shops:
return
for _ in range(count):
shop = self.random.choice(shops)
index = len(self.created_multi_locations[shop])
self.add_shop_location(shop, index)
if len(self.created_multi_locations[shop]) >= 16:
shops.remove(shop)
if not shops:
break
if gen_is_fake:
for shop in shops:
while len(self.created_multi_locations[shop]) < 16:
index = len(self.created_multi_locations[shop])
self.add_shop_location(shop, index)
else:
for _ in range(count):
shop = self.random.choice(shops)
index = len(self.created_multi_locations[shop])
self.add_shop_location(shop, index)
if len(self.created_multi_locations[shop]) >= 16:
shops.remove(shop)
if not shops:
break

# create_items
def create_items(self):
Expand Down Expand Up @@ -930,6 +1084,9 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]:
continue
if not location.costs:
continue
if location.name in self.pre_defined_location_costs:
location.costs = self.pre_defined_location_costs[location.name]
continue
if location.name == "Vessel_Fragment-Basin":
continue
if setting == CostSanity.option_notshops and location.basename in self.created_multi_locations:
Expand Down Expand Up @@ -958,6 +1115,8 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]:
location.sort_costs()

def sort_shops_by_cost(self):
if hasattr(self.multiworld, "generation_is_fake"):
return #In a UT gen we've already placed these where they should go
for shop_locations in self.created_multi_locations.values():
randomized_locations = [loc for loc in shop_locations if not loc.vanilla]
if not randomized_locations:
Expand Down
3 changes: 3 additions & 0 deletions worlds/hk/resource_state_vars/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ def __init__(self, term: str, player: int):
else:
self.parse_term()

def __repr__(self) -> str:
return self.term_name

def parse_term(self, *args) -> None:
"""Subclasses should use this to expect parameter counts for init"""
pass
Expand Down