import itertools from collections import OrderedDict try: from fast_enum import FastEnum except ImportError: from enum import IntFlag as FastEnum from BaseClasses import CrystalBarrier, KeyRuleType from Dungeons import dungeon_keys class RuleType(FastEnum): Conjunction = 0 Disjunction = 1 Item = 2 Glitch = 3 Reachability = 4 Static = 5 Bottle = 6 Crystal = 7 Barrier = 8 Hearts = 9 Unlimited = 10 ExtendMagic = 11 Boss = 12 Negate = 13 LocationCheck = 14 SmallKeyDoor = 15 class Rule(object): def __init__(self, rule_type): self.rule_type = rule_type self.sub_rules = [] self.principal = None self.player = 0 self.resolution_hint = None self.barrier = None self.flag = None self.locations = [] self.count = 1 self.std_req = None self.rule_lambda = lambda state: True def eval(self, state): return self.rule_lambda(state) def get_requirements(self, progressive_flag=True): if not self.std_req: reqs = rule_requirements[self.rule_type](self, progressive_flag) self.std_req = standardize_requirements(reqs, progressive_flag) return self.std_req def __str__(self): return str(self.__unicode__()) def __unicode__(self): return rule_prints[self.rule_type](self) rule_prints = { RuleType.Conjunction: lambda self: f'({" and ".join([str(x) for x in self.sub_rules])})', RuleType.Disjunction: lambda self: f'({" or ".join([str(x) for x in self.sub_rules])})', RuleType.Item: lambda self: f'has {self.principal}' if self.count == 1 else f'has {self.count} {self.principal}(s)', RuleType.Reachability: lambda self: f'canReach {self.principal}', RuleType.Static: lambda self: f'{self.principal}', RuleType.Crystal: lambda self: f'has {self.principal} crystals', RuleType.Barrier: lambda self: f'{self.barrier} @ {self.principal}', RuleType.Hearts: lambda self: f'has {self.principal} hearts', RuleType.Unlimited: lambda self: f'canBuyUnlimited {self.principal}', RuleType.ExtendMagic: lambda self: f'magicNeeded {self.principal}', RuleType.Boss: lambda self: f'canDefeat({self.principal.defeat_rule})', RuleType.Negate: lambda self: f'not ({self.sub_rules[0]})', RuleType.LocationCheck: lambda self: f'{self.principal} in [{", ".join(self.locations)}]', RuleType.SmallKeyDoor: lambda self: f'doorOpen {self.principal[0]}:{self.principal[1]}' } def or_rule(rule1, rule2): return lambda state: rule1(state) or rule2(state) def and_rule(rule1, rule2): return lambda state: rule1(state) and rule2(state) class RuleFactory(object): @staticmethod def static_rule(boolean): rule = Rule(RuleType.Static) rule.principal = boolean rule.rule_lambda = lambda state: boolean return rule @staticmethod def conj(rules): if len(rules) == 1: return rules[0] rule = Rule(RuleType.Conjunction) rule_lambda = None for r in rules: if r.rule_type == RuleType.Conjunction: rule.sub_rules.extend(r.sub_rules) # todo: this extension for the lambda calc elif r.rule_type == RuleType.Static and r.principal: # remove static flag if unnecessary continue else: rule.sub_rules.append(r) if not rule_lambda: rule_lambda = r.rule_lambda else: rule_lambda = and_rule(rule_lambda, r.rule_lambda) rule.rule_lambda = rule_lambda return rule @staticmethod def disj(rules): if len(rules) == 1: return rules[0] rule = Rule(RuleType.Disjunction) rule_lambda = None for r in rules: if r.rule_type == RuleType.Disjunction: rule.sub_rules.extend(r.sub_rules) # todo: this extension for the lambda calc elif r.rule_type == RuleType.Static and not r.principal: # remove static flag if unnecessary continue else: rule.sub_rules.append(r) if not rule_lambda: rule_lambda = r.rule_lambda else: rule_lambda = or_rule(rule_lambda, r.rule_lambda) rule.rule_lambda = rule_lambda return rule @staticmethod def item(item, player, count=1): rule = Rule(RuleType.Item) rule.principal = item rule.player = player rule.count = count rule.rule_lambda = lambda state: state.has(item, player, count) return rule @staticmethod def bottle(player): rule = Rule(RuleType.Bottle) rule.player = player rule.rule_lambda = lambda state: state.has_bottle(player) return rule @staticmethod def crystals(number, player): rule = Rule(RuleType.Crystal) rule.principal = number rule.player = player rule.rule_lambda = lambda state: state.has_crystals(number, player) return rule @staticmethod def barrier(region, player, barrier): rule = Rule(RuleType.Barrier) rule.principal = region rule.player = player rule.barrier = barrier rule.rule_lambda = lambda state: state.can_cross_barrier(region, player, barrier) return rule @staticmethod def hearts(number, player): rule = Rule(RuleType.Hearts) rule.principal = number rule.player = player rule.rule_lambda = lambda state: state.has_hearts(number, player) return rule @staticmethod def unlimited(item, player, shop_regions): rule = Rule(RuleType.Unlimited) rule.principal = item rule.player = player rule.locations = shop_regions # list of regions where said item can be bought rule.rule_lambda = lambda state: state.can_buy_unlimited(item, player) return rule @staticmethod def extend_magic(player, magic, difficulty, magic_potion_regions, flag): rule = Rule(RuleType.ExtendMagic) rule.principal = magic rule.player = player rule.resolution_hint = difficulty # world difficulty setting rule.locations = magic_potion_regions # list of regions where blue/green can be bought rule.flag = flag rule.rule_lambda = lambda state: state.can_extend_magic(player, magic, flag) return rule @staticmethod def boss(boss): rule = Rule(RuleType.Boss) rule.principal = boss rule.rule_lambda = lambda state: boss.defeat_rule.eval(state) return rule @staticmethod def neg(orig): rule = Rule(RuleType.Negate) rule.sub_rules.append(orig) rule.rule_lambda = lambda state: not orig.rule_lambda(state) return rule @staticmethod def check_location(item, location, player): rule = Rule(RuleType.LocationCheck) rule.principal = item rule.location = location rule.player = player rule.rule_lambda = eval_location(item, location, player) return rule @staticmethod def small_key_door(door_name, dungeon, player, door_rules): rule = Rule(RuleType.SmallKeyDoor) rule.principal = (door_name, dungeon) rule.player = player rule.resolution_hint = door_rules # door_rule object from KeyDoorShuffle rule.rule_lambda = eval_small_key_door(door_name, dungeon, player) return rule def eval_location(item, location, player): return lambda state: eval_location_main(item, location, player, state) def eval_location_main(item, location, player, state): location = state.world.get_location(location, player) return location.item and location.item.name == item and location.player == player def eval_small_key_door_main(state, door_name, dungeon, player): if state.is_door_open(door_name, player): return True key_logic = state.world.key_logic[player][dungeon] door_rule = key_logic.door_rules[door_name] door_openable = False for ruleType, number in door_rule.new_rules.items(): if door_openable: return True if ruleType == KeyRuleType.WorstCase: door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) elif ruleType == KeyRuleType.AllowSmall: if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name and door_rule.small_location.item.player == player): return True # always okay if allow small is on elif isinstance(ruleType, tuple): lock, lock_item = ruleType # this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't for loc in door_rule.alternate_big_key_loc: spot = state.world.get_location(loc, player) if spot.item and spot.item.name == lock_item: door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) break return door_openable def eval_small_key_door(door_name, dungeon, player): return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) def conjunction_requirements(rule, f): combined = [ReqSet()] for r in rule.sub_rules: result = r.get_requirements(f) combined = merge_requirements(combined, result) return combined def disjunction_requirements(rule, f): results = [] for r in rule.sub_rules: result = r.get_requirements(f) results.extend(result) return results rule_requirements = { RuleType.Conjunction: conjunction_requirements, RuleType.Disjunction: disjunction_requirements, RuleType.Item: lambda rule, f: [ReqSet([Requirement(ReqType.Item, rule.principal, rule.player, rule, rule.count)])], RuleType.Reachability: lambda rule, f: [ReqSet([Requirement(ReqType.Reachable, rule.principal, rule.player, rule)])], RuleType.Static: lambda rule, f: static_req(rule), RuleType.Crystal: lambda rule, f: crystal_requirements(rule), RuleType.Bottle: lambda rule, f: [ReqSet([Requirement(ReqType.Item, 'Bottle', rule.player, rule, 1)])], RuleType.Barrier: lambda rule, f: barrier_req(rule), RuleType.Hearts: lambda rule, f: empty_req(), # todo: the one heart container RuleType.Unlimited: lambda rule, f: unlimited_buys(rule), RuleType.ExtendMagic: lambda rule, f: magic_requirements(rule), RuleType.Boss: lambda rule, f: rule.principal.defeat_rule.get_requirements(f), RuleType.Negate: lambda rule, f: empty_req(), # ignore these and just don't flood the key too early RuleType.LocationCheck: lambda rule, f: location_check(rule), RuleType.SmallKeyDoor: lambda rule, f: small_key_reqs(rule) } avail_crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] def crystal_requirements(rule): crystal_rules = map(lambda c: Requirement(ReqType.Item, c, rule.player, rule), avail_crystals) combinations = itertools.combinations(crystal_rules, rule.principal) counter_list = [] for combo in combinations: counter_list.append(ReqSet(combo)) return counter_list # todo: 1/4 magic def magic_requirements(rule): if rule.principal <= 8: return [set()] bottle_val = 1.0 if rule.resolution_hint == 'expert' and not rule.flag: bottle_val = 0.25 elif rule.resolution_hint == 'hard' and not rule.flag: bottle_val = 0.5 base, min_bot, reqs = 8, None, [] for i in range(1, 5): if base + bottle_val*base*i >= rule.principal: min_bot = i break if min_bot: for region in rule.locations: reqs.append(ReqSet([Requirement(ReqType.Item, 'Bottle', rule.player, rule, min_bot), Requirement(ReqType.Reachable, region, rule.player, rule)])) if rule.principal <= 16: reqs.append(ReqSet([Requirement(ReqType.Item, 'Magic Upgrade (1/2)', rule.player, rule, 1)])) return reqs else: base, min_bot = 16, 4 for i in range(1, 5): if base + bottle_val*base*i >= rule.principal: min_bot = i break if min_bot: for region in rule.locations: reqs.append(ReqSet([Requirement(ReqType.Item, 'Magic Upgrade (1/2)', rule.player, rule, 1), Requirement(ReqType.Item, 'Bottle', rule.player, rule, min_bot), Requirement(ReqType.Reachable, region, rule.player, rule)])) return reqs def static_req(rule): return [ReqSet()] if rule.principal else [ReqSet([Requirement(ReqType.Item, 'Impossible', rule.player, rule)])] def barrier_req(rule): return [ReqSet([Requirement(ReqType.Reachable, rule.principal, rule.player, rule, crystal=rule.barrier)])] def empty_req(): return [ReqSet()] def location_check(rule): return [ReqSet([Requirement(ReqType.Placement, rule.principal, rule.player, rule, locations=rule.locations)])] def unlimited_buys(rule): requirements = [] for region in rule.locations: requirements.append(ReqSet([Requirement(ReqType.Reachable, region, rule.player, rule)])) return requirements def small_key_reqs(rule): requirements = [] door_name, dungeon = rule.principal key_name = dungeon_keys[dungeon] for rule_type, number in rule.resolution_hint.new_rules.items(): if rule_type == KeyRuleType.WorstCase: requirements.append(ReqSet([Requirement(ReqType.Item, key_name, rule.player, rule, number)])) elif rule_type == KeyRuleType.AllowSmall: small_loc = rule.resolution_hint.small_location.name requirements.append(ReqSet([ Requirement(ReqType.Placement, key_name, rule.player, rule, locations=[small_loc]), Requirement(ReqType.Item, key_name, rule.player, rule, number)])) elif isinstance(rule_type, tuple): lock, lock_item = rule_type locs = [x.name for x in rule.resolution_hint.alternate_big_key_loc] requirements.append(ReqSet([ Requirement(ReqType.Placement, lock_item, rule.player, rule, locations=locs), Requirement(ReqType.Item, key_name, rule.player, rule, number)])) return requirements class ReqType(FastEnum): Item = 0 Placement = 2 class ReqSet(object): def __init__(self, requirements=None): if requirements is None: requirements = [] self.keyed = OrderedDict() for r in requirements: self.keyed[r.simple_key()] = r def append(self, req): self.keyed[req.simple_key()] = req def get_values(self): return self.keyed.values() def merge(self, other): new_set = ReqSet(self.get_values()) for r in other.get_values(): key = r.simple_key() if key in new_set.keyed: new_set.keyed[key] = max(r, new_set.keyed[key], key=lambda r: r.amount) else: new_set.keyed[key] = r return new_set def redundant(self, other): for k, req in other.keyed.items(): if k not in self.keyed: return False elif self.keyed[k].amount < req.amount: return False return True def different(self, other): for key in self.keyed.keys(): if key not in other.keyed: return True if key in other.keyed and self.keyed[key].amount > other.keyed[key].amount: return True return False def find_item(self, item_name): for key, req in self.keyed.items(): if req.req_type == ReqType.Item and req.item == item_name: return req return None def __eq__(self, other): for key, req in self.keyed.items(): if key not in other.keyed: return False if req.amount != other.keyed[key].amount: return False for key in other.keyed: if key not in self.keyed: return False return True def __str__(self): return str(self.__unicode__()) def __unicode__(self): return " and ".join([str(x) for x in self.keyed.values()]) class Requirement(object): def __init__(self, req_type, item, player, rule, amount=1, crystal=CrystalBarrier.Null, locations=()): self.req_type = req_type self.item = item self.player = player self.rule = rule self.amount = amount self.crystal = crystal self.locations = tuple(locations) def simple_key(self): return self.req_type, self.item, self.player, self.crystal, self.locations def key(self): return self.req_type, self.item, self.player, self.amount, self.crystal, self.locations def __eq__(self, other): if isinstance(other, Requirement): return self.key() == other.key() return NotImplemented def __hash__(self): return hash(self.key()) def __str__(self): return str(self.__unicode__()) def __unicode__(self): if self.req_type == ReqType.Item: return f'has {self.item}' if self.amount == 1 else f'has {self.amount} {self.item}(s)' elif self.req_type == ReqType.Placement: return f'{self.item} located @ {",".join(self.locations)}' # requirement utility methods def merge_requirements(starting_requirements, new_requirements): merge = [] for req in starting_requirements: for new_r in new_requirements: merge.append(req.merge(new_r)) return reduce_requirements(merge) only_one = {'Moon Pearl', 'Hammer', 'Blue Boomerang', 'Red Boomerang', 'Hookshot', 'Mushroom', 'Powder', 'Fire Rod', 'Ice Rod', 'Bombos', 'Ether', 'Quake', 'Lamp', 'Shovel', 'Ocarina', 'Bug Catching Net', 'Book of Mudora', 'Magic Mirror', 'Cape', 'Cane of Somaria', 'Cane of Byrna', 'Flippers', 'Pegasus Boots'} def standardize_requirements(requirements, progressive_flag): assert isinstance(requirements, list) for req in requirements: for thing in req.get_values(): if thing.item in only_one and thing.amount > 1: thing.amount = 1 if progressive_flag: substitute_progressive(req) return reduce_requirements(requirements) def reduce_requirements(requirements): removals = [] reduced = list(requirements) # subset manip ttl = len(reduced) for i in range(0, ttl - 1): for j in range(i + 1, ttl): req, other_req = reduced[i], reduced[j] if req.redundant(other_req): removals.append(req) elif other_req.redundant(req): removals.append(other_req) for removal in removals: if removal in reduced: reduced.remove(removal) assert len(reduced) != 0 return reduced progress_sub = { 'Fighter Sword': ('Progressive Sword', 1), 'Master Sword': ('Progressive Sword', 2), 'Tempered Sword': ('Progressive Sword', 3), 'Golden Sword': ('Progressive Sword', 4), 'Power Glove': ('Progressive Glove', 1), 'Titans Mitts': ('Progressive Glove', 2), 'Bow': ('Progressive Bow', 1), 'Silver Arrows': ('Progressive Bow', 2), 'Blue Mail': ('Progressive Armor', 1), 'Red Mail': ('Progressive Armor', 2), 'Blue Shield': ('Progressive Shield', 1), 'Red Shield': ('Progressive Shield', 2), 'Mirror Shield': ('Progressive Shield', 3), } def substitute_progressive(req): for item in req.get_values(): if item.item in progress_sub.keys(): item.item, item.amount = progress_sub[item.item]