From 4be7d07d63df52a969e53c32d5f5a393c0bb5f6d Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Fri, 11 Oct 2024 23:04:59 +0200 Subject: [PATCH] Count recursions in entry rule check in client --- worlds/sc2/client.py | 19 ++---------- worlds/sc2/mission_order/entry_rules.py | 39 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index 2f3f0ded2d95..378a9c6fad2e 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -1424,31 +1424,18 @@ def calc_available_nodes(ctx: SC2Context) -> typing.Tuple[typing.List[int], typi received_items[network_item.item] += 1 accessible_rules: typing.Set[int] = set() - # Determine accessibility from top to bottom to avoid a recursion problem from - # missions trying to access layouts & campaigns appearing later than themselves - # Campaigns for campaign_idx, campaign in enumerate(ctx.custom_mission_order): available_layouts[campaign_idx] = [] - if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_campaigns.append(campaign_idx) - - # Layouts - for campaign_idx, campaign in enumerate(ctx.custom_mission_order): - if campaign_idx in available_campaigns: for layout_idx, layout in enumerate(campaign.layouts): - if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_layouts[campaign_idx].append(layout_idx) - - # Missions - for campaign_idx, campaign in enumerate(ctx.custom_mission_order): - if campaign_idx in available_campaigns: - for layout_idx, layout in enumerate(campaign.layouts): - if layout_idx in available_layouts[campaign_idx]: for column in layout.missions: for mission in column: if mission.mission_id == -1: continue - if mission.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if mission.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_missions.append(mission.mission_id) return available_missions, available_layouts, available_campaigns diff --git a/worlds/sc2/mission_order/entry_rules.py b/worlds/sc2/mission_order/entry_rules.py index 1b03f337fbf8..afa4f3e3e40f 100644 --- a/worlds/sc2/mission_order/entry_rules.py +++ b/worlds/sc2/mission_order/entry_rules.py @@ -70,7 +70,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: return False @@ -121,7 +121,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Beat rules are accessible if all their missions are beaten and accessible if not beaten_missions.issuperset(self.mission_ids): @@ -182,7 +182,9 @@ def tooltip(self, indents: int, missions: Dict[int, SC2Mission]) -> str: req = self.visual_reqs[0] req_str = missions[req].mission_name if type(req) == int else req if self.amount == 1: - return f"Beat {req_str}" + if type(req) == int: + return f"Beat {req_str}" + return f"Beat any mission from {req_str}" return f"Beat {amount} missions from {req_str}" if self.amount == 1: tooltip = f"Beat any mission from:\n{indent}- " @@ -198,7 +200,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Count rules are accessible if enough of their missions are beaten and accessible return self.amount <= sum( @@ -279,11 +281,16 @@ def parse_from_dict(data: Dict[str, Any]) -> SubRuleRuleData: **{field: value for field, value in rule_data.items()} ) sub_rules.append(rule) - return SubRuleRuleData( + rule = SubRuleRuleData( rule_id, sub_rules, amount ) + # Add an accessibility buffer for top level rules + # This is an optimization to make recalculations of large mission orders feel smoother + if rule.rule_id >= 0: + rule.buffer_accessible = False + return rule @staticmethod def empty() -> SubRuleRuleData: @@ -309,26 +316,30 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Early exit check for top-level entry rules if self.rule_id >= 0: - if self.rule_id in accessible_rules: + if self.buffer_accessible or self.rule_id in accessible_rules: return True # Never consider rules discovered via recursion to be accessible # (unless they succeeded, in which case they will be in accessible_rules) if self.rule_id in seen_rules: return False - seen_rules.add(self.rule_id) + seen_rules.append(self.rule_id) # Sub-rule rules are accessible if enough of their child rules are accessible - if self.amount <= sum( + success = self.amount <= sum( rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules) for rule in self.sub_rules - ): - if self.rule_id >= 0: + ) + if self.rule_id >= 0: + if success: accessible_rules.add(self.rule_id) - return True - return False + self.buffer_accessible = True + # Rules that failed in the middle of calculating another rule + # should be allowed to try again at a later point + seen_rules.remove(self.rule_id) + return success class ItemEntryRule(EntryRule): items_to_check: Dict[str, int] @@ -375,7 +386,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: return all( item in received_items and received_items[item] >= amount