Files
alttpr-python/source/logic/Rule.py
2022-09-16 10:20:54 -06:00

569 lines
20 KiB
Python

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]