diff --git a/fireplace/dsl/lazynum.py b/fireplace/dsl/lazynum.py index fd92ffe0d..d4f4852a5 100644 --- a/fireplace/dsl/lazynum.py +++ b/fireplace/dsl/lazynum.py @@ -1,11 +1,15 @@ import copy import operator import random + from .evaluator import Evaluator +from abc import ABCMeta, abstractmethod -class LazyValue: - pass +class LazyValue(metaclass=ABCMeta): + @abstractmethod + def evaluate(self, source): + pass class LazyNum(LazyValue): @@ -110,6 +114,31 @@ def evaluate(self, source): return self.num(ret) +class OpAttr(LazyNum): + """ + Lazily evaluate Op over all tags in a selector. + This is analogous to lazynum.Attr, which is equivalent to OpAttr(..., ..., sum) + """ + # TODO(smallnamespace): Merge this into Attr + def __init__(self, selector, tag, op): + super().__init__() + self.selector = selector + self.tag = tag + self.op = op + + def __repr__(self): + return "%s(%r, %r)" % (self.__class__.__name__, self.selector, self.tag) + + def evaluate(self, source): + entities = self.get_entities(source) + if isinstance(self.tag, str): + ret = self.op(getattr(e, self.tag) for e in entities if e) + else: + # XXX: int() because of CardList counter tags + ret = self.op(int(e.tags[self.tag]) for e in entities if e) + return self.num(ret) + + class RandomNumber(LazyNum): def __init__(self, *args): super().__init__() diff --git a/fireplace/dsl/selector.py b/fireplace/dsl/selector.py index 74a2825da..276d49139 100644 --- a/fireplace/dsl/selector.py +++ b/fireplace/dsl/selector.py @@ -1,216 +1,121 @@ import operator import random -from enum import IntEnum + +from abc import ABCMeta, abstractmethod +from enum import Enum +from fireplace.entity import BaseEntity from hearthstone.enums import CardType, GameTag, Race, Rarity, Zone from .. import enums -from ..utils import CardList -from .lazynum import LazyValue +from .lazynum import Attr, LazyValue, OpAttr + + +# Type aliases -- forward declared for now +SelectorLike = "Union[Selector, LazyValue]" +BinaryOp = "Callable[[Any, Any], bool]" class Selector: """ - A Forth-like program consisting of methods of Selector and members of - IntEnum classes. The IntEnums must have appropriate test(entity) - methods returning a boolean, true if entity matches the condition. + Selectors take entity lists and returns a sub-list. Selectors + are closed under addition, subtraction, complementation, and ORing. + + Note that addition means set intersection and OR means set union. For + convenience, LazyValues can also treated as selectors. + + Set operations preserve ordering (necessary for cards like Echo of + Medivh, where ordering matters) """ - class MergeFilter: - """ - Signals the start of a merge: the following commands define the filter - to be passed after Merge - """ - pass + def eval(self, entities: "List[BaseEntity]", source: BaseEntity) -> "List[BaseEntity]": + return entities - class Merge: - """ - Ops between Merge and Unmerge are classes with merge(selector, entities) - methods that operate on the full collection specified by the ops between - MergeFilter and Merge. - """ - pass + def __add__(self, other: SelectorLike) -> "Selector": + return SetOpSelector(operator.and_, self, other) - class Unmerge: - pass + def __or__(self, other: SelectorLike) -> "Selector": + return SetOpSelector(operator.or_, self, other) - def __init__(self, tag=None): - self.program = [] - self.slice = None - if tag is not None: - self.program.append(tag) + def __neg__(self) -> "Selector": + # Note that here we define negation in terms of subtraction, and + # not the other way around, because selectors are implemented using + # concrete set operations instead of boolean manipulation + return Selector() - self - def __repr__(self): - prog = [] - for op in self.program: - name = "" - if hasattr(op, "__name__"): - name = op.__name__ - if name == "_and": - name = "+" - elif name == "_not": - name = "-" - elif name == "_or": - name = "|" - elif isinstance(op, IntEnum): - name = op.name - else: - name = repr(op) - prog.append(name) - return "<%s>" % (" ".join(prog)) - - def __or__(self, other): - result = Selector() - if isinstance(other, LazyValue): - other = LazySelector(other) - result.program = self.program + other.program - result.program.append(Selector._or) - return result + def __sub__(self, other: SelectorLike) -> "Selector": + return SetOpSelector(operator.sub, self, other) - def __add__(self, other): - result = Selector() + def __rsub__(self, other: SelectorLike) -> "Selector": if isinstance(other, LazyValue): - other = LazySelector(other) - result.program = self.program + other.program - result.program.append(Selector._and) - return result + other = LazyValueSelector(other) + return other - self - def __sub__(self, other): - result = Selector() - if isinstance(other, LazyValue): - other = LazySelector(other) - result.program = self.program + other.program - result.program += [Selector._not, Selector._and] - return result + def __radd__(self, other: SelectorLike) -> "Selector": + return self + other + + def __ror__(self, other: SelectorLike) -> "Selector": + return self | other - def __getitem__(self, val): - ret = Selector() - ret.program = self.program + def __getitem__(self, val: "Union[int, slice]") -> "Selector": if isinstance(val, int): - ret.slice = slice(val) - else: - ret.slice = val - return ret + val = slice(val) + return SliceSelector(self, val) - def eval(self, entities, source): - if not entities: - return [] - self.opc = 0 # outer program counter - result = [] - while self.opc < len(self.program): - if self.program[self.opc] != Selector.MergeFilter: - result += [e for e in entities if self.test(e, source)] - self.opc = self.pc - if self.opc >= len(self.program): - break - else: - self.opc += 1 - # handle merge step: - merge_input = CardList([e for e in entities if self.test(e, source)]) - self.opc = self.pc - merge_output = CardList() - while self.opc < len(self.program): - op = self.program[self.opc] - self.opc += 1 - if op == Selector.Unmerge: - break - merge_output += op.merge(self, merge_input) - negated = False - combined = False - while self.opc < len(self.program): - # special handling for operators on merged collections: - op = self.program[self.opc] - if op == Selector._or: - result += [e for e in merge_output] - combined = True - elif op == Selector._and: - result = [e for e in result if (e in merge_output) != negated] - combined = True - elif op == Selector._not: - negated = not negated - else: - break - self.opc += 1 - if not combined: - # assume or - result += merge_output - - if self.slice: - result = result[self.slice] - return result +class EnumSelector(Selector): + def __init__(self, tag_enum=None): + self.tag_enum = tag_enum - def test(self, entity, source): - stack = [] - self.pc = self.opc # program counter - while self.pc < len(self.program): - op = self.program[self.pc] - self.pc += 1 - if op == Selector.Merge or op == Selector.MergeFilter: - break - if callable(op): - op(self, stack) - else: - val = type(op).test(op, entity, source) - stack.append(val) - return stack[-1] - - # boolean ops: - def _and(self, stack): - a = stack.pop() - b = stack.pop() - stack.append(a and b) - - def _or(self, stack): - a = stack.pop() - b = stack.pop() - stack.append(a or b) - - def _not(self, stack): - stack.append(not stack.pop()) + def eval(self, entities, source): + if not self.tag_enum or not hasattr(self.tag_enum, "test"): + raise RuntimeError("Unsupported enum type {}".format(str(self.tag_enum))) + return [e for e in entities if self.tag_enum.test(e, source)] -class EnumSelector(Selector): - pass + def __repr__(self): + return "<%s>" % self.tag_enum.name -class AttrValue(Selector): + +class SelectorEntityValue(metaclass=ABCMeta): """ - Selects entities with tags matching a comparison. + SelectorEntityValues can be compared to arbitrary objects LazyValues; + the comparison's boolean result forms a selector on entities. """ - class IsAttrValue: - def __init__(self, tag, op, value): - self.tag = tag - self.op = op - self.value = value - - def __repr__(self): - return "Attr(%s(%r, %r))" % (self.op.__name__, self.tag, self.value) - - def test(self, entity, source): - value = self.value - if isinstance(value, LazyValue): - # Support AttrSelector(SELF, GameTag.CONTROLLER) == Controller(...) - value = self.value.evaluate(source) - return self.op(entity.tags.get(self.tag, 0), value) + @abstractmethod + def value(self, entity, source): + pass + + def __eq__(self, other) -> Selector: + return ComparisonSelector(operator.eq, self, other) + + def __gt__(self, other) -> Selector: + return ComparisonSelector(operator.gt, self, other) + + def __lt__(self, other) -> Selector: + return ComparisonSelector(operator.lt, self, other) + def __ge__(self, other) -> Selector: + return ComparisonSelector(operator.ge, self, other) + + def __le__(self, other) -> Selector: + return ComparisonSelector(operator.le, self, other) + + def __ne__(self, other) -> Selector: + return ComparisonSelector(operator.ne, self, other) + + +class AttrValue(SelectorEntityValue): + """Extracts attribute values from an entity to allow for boolean comparisons.""" def __init__(self, tag): - super().__init__() self.tag = tag - self.program = [] - def __call__(self, selector): - from .lazynum import Attr + def value(self, entity, source): + return entity.tags.get(self.tag, 0) + def __call__(self, selector): + """Convenience function to support uses like ARMOR(SELF)""" return Attr(selector, self.tag) - def _cmp(op): - def func(self, other): - sel = self.__class__(self.tag) - sel.program = [self.IsAttrValue(self.tag, getattr(operator, op), other)] - return sel - return func + def __repr__(self): + return "<%s>" % self.tag.name - __eq__ = _cmp("eq") - __ge__ = _cmp("ge") - __gt__ = _cmp("gt") - __le__ = _cmp("le") - __lt__ = _cmp("lt") ARMOR = AttrValue(GameTag.ARMOR) ATK = AttrValue(GameTag.ATK) @@ -222,174 +127,154 @@ def func(self, other): USED_MANA = AttrValue(GameTag.RESOURCES_USED) -class SelfSelector(Selector): - """ - Selects the source. - """ - class IsSelf: - def __repr__(self): - return "SELF" - - def test(self, entity, source): - return entity is source +class ComparisonSelector(Selector): + """A ComparisonSelector compares values of entities to + other values. Lazy values are evaluated at selector runtime.""" + def __init__(self, op: BinaryOp, left: SelectorEntityValue, right): + self.op = op + self.left = left + self.right = right - def __init__(self): - self.program = [self.IsSelf()] + def eval(self, entities, source): + right_value = (self.right.evaluate(source) + if isinstance(self.right, LazyValue) + else self.right) + return [e for e in entities if + self.op(self.left.value(e, source), right_value)] def __repr__(self): - return "" - - def eval(self, entities, source): - return [source] + if self.op.__name__ == "eq": + infix = "==" + elif self.op.__name__ == "gt": + infix = ">" + elif self.op.__name__ == "lt": + infix = "<" + elif self.op.__name__ == "ge": + infix = ">=" + elif self.op.__name__ == "le": + infix = "<=" + elif self.op.__name__ == "ne": + infix = "!=" + else: + infix = "UNKNOWN_OP" + return "<%r %s %r>" % (self.left, infix, self.right) -SELF = SelfSelector() +class FilterSelector(Selector): + def __init__(self, func: "Callable[[BaseEntity, BaseEntity], bool]") -> None: + """ + func(entity, source) returns true iff the entity + should be selected + """ + self.func = func -class OwnerSelector(Selector): - """ - Selects the source's owner. - """ - class IsOwner: - def test(self, entity, source): - return entity is source.owner + def eval(self, entities, source): + return [e for e in entities if self.func(e, source)] - def __init__(self): - self.program = [self.IsOwner()] - def __repr__(self): - return "" +class FuncSelector(Selector): + def __init__(self, func: "Callable[[List[BaseEntity], BaseEntity], List[BaseEntity]]") -> None: + """func(entities, source) returns the results""" + self.func = func def eval(self, entities, source): - if source.owner: - return [source.owner] - return [] + return self.func(entities, source) -OWNER = OwnerSelector() +class SliceSelector(Selector): + """Applies a slice to child selector at evaluation time.""" + def __init__(self, child: SelectorLike, slice_val: slice): + if isinstance(child, LazyValue): + child = LazyValueSelector(child) + self.child = child + self.slice = slice_val -class FuncSelector(Selector): - """ - Selects cards after applying a filter function to them - """ - class MatchesFunc: - def __init__(self, func): - self.func = func - - def test(self, entity, source): - return self.func(entity, source) + def eval(self, entities, source): + return list(self.child.eval(entities, source)[self.slice]) - def __init__(self, func): - self.program = [self.MatchesFunc(func)] - self.slice = None + def __repr__(self): + return "%r[%r]" % (self.child, self.slice) -def ID(id): - return FuncSelector(lambda entity, source: getattr(entity, "id", None) == id) +class SetOpSelector(Selector): + def __init__(self, op: "Callable", left: Selector, right: SelectorLike) -> None: + if isinstance(right, LazyValue): + right = LazyValueSelector(right) + self.op = op + self.left = left + self.right = right + @staticmethod + def _entity_id_set(entities: "Iterable[BaseEntity]") -> "Set[BaseEntity]": + return set(e.entity_id for e in entities if e) -def LazySelector(value): - """ - Returns a selector that evaluates the value at selection time - Useful for eg. `ALL_CHARACTERS - Attack.TARGET` - """ - return FuncSelector(lambda entity, source: entity is value.evaluate(source)) + def eval(self, entities, source): + left_children = self.left.eval(entities, source) + right_children = self.right.eval(entities, source) + result_entity_ids = self.op(self._entity_id_set(left_children), + self._entity_id_set(right_children)) + # Preserve input ordering and multiplicity + return [e for e in entities if e.entity_id in result_entity_ids] + def __repr__(self): + name = self.op.__name__ + if name == "add_": + infix = "+" + elif name == "or_": + infix = "|" + elif name == "sub": + infix = "-" + else: + infix = "UNKNOWN_OP" -TARGET = FuncSelector(lambda entity, source: entity is source.target) -TARGET.eval = lambda entity, source: [source.target] + return "<%r %s %r>" % (self.left, infix, self.right) -class MinMaxSelector(Selector): - """ - Selects the entities in \a selector whose \a tag match \a func comparison - """ - class SelectFunc: - def __init__(self, tag, func): - self.tag = tag - self.func = func - - def __repr__(self): - return "<%s(%s)>" % (self.func.__name__, self.tag) - - def merge(self, selector, entities): - key = lambda x: x.tags.get(self.tag, 0) - highest = self.func(entities, key=key).tags.get(self.tag, 0) - ret = [e for e in entities if e.tags.get(self.tag) == highest] - return random.sample(ret, min(len(ret), 1)) - - def __init__(self, selector, tag, func): - self.slice = None - self.select = self.SelectFunc(tag, func) - self.selector = selector - self.program = [Selector.MergeFilter] - self.program.extend(selector.program) - self.program.append(Selector.Merge) - self.program.append(self.select) - self.program.append(Selector.Unmerge) +SELF = FuncSelector(lambda _, source: [source]) +OWNER = FuncSelector(lambda entities, source: [source.owner] if hasattr(source, "owner") else []) - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, self.selector) +def LazyValueSelector(value): + return FuncSelector(lambda entities, source: [value.evaluate(source)]) -HIGHEST_ATK = lambda sel: MinMaxSelector(sel, GameTag.ATK, max) -LOWEST_ATK = lambda sel: MinMaxSelector(sel, GameTag.ATK, min) +def ID(id): + return FilterSelector(lambda entity, source: getattr(entity, "id", None) == id) -class LeftOfSelector(Selector): - """ - Selects the entities to the left of the targets. - """ - class SelectAdjacent: - def merge(self, selector, entities): - result = [] - for e in entities: - if e.zone == Zone.PLAY: - left = e.controller.field[:e.zone_position] - if left: - result.append(left[-1]) - return result - - def __init__(self, selector): - self.slice = None - self.program = [Selector.MergeFilter] - self.program.extend(selector.program) - self.program.append(Selector.Merge) - self.program.append(self.SelectAdjacent()) - self.program.append(Selector.Unmerge) +TARGET = FuncSelector(lambda entities, source: [source.target]) - def __repr__(self): - return "" -LEFT_OF = LeftOfSelector +class Direction(Enum): + LEFT = 1 + RIGHT = 2 -class RightOfSelector(Selector): - """ - Selects the entities to the right of the targets. - """ - class SelectAdjacent: - def merge(self, selector, entities): - result = [] - for e in entities: - if e.zone == Zone.PLAY: - right = e.controller.field[e.zone_position + 1:] - if right: - result.append(right[0]) - return result - - def __init__(self, selector): - self.slice = None - self.program = [Selector.MergeFilter] - self.program.extend(selector.program) - self.program.append(Selector.Merge) - self.program.append(self.SelectAdjacent()) - self.program.append(Selector.Unmerge) +class BoardPositionSelector(Selector): + def __init__(self, direction: Direction, child: SelectorLike): + if isinstance(child, LazyValue): + child = LazyValueSelector(child) + self.child = child + self.direction = direction - def __repr__(self): - return "" + def eval(self, entities, source): + result = [] + for e in self.child.eval(entities, source): + if getattr(e, "zone", None) == Zone.PLAY: + field = e.controller.field + position = e.zone_position + if self.direction == Direction.RIGHT: + # Swap the list, reverse the position + field = list(reversed(field)) + position = -(position + 1) + + left = field[:position] + if left: + result.append(left[-1]) -RIGHT_OF = RightOfSelector + return result +LEFT_OF = lambda s: BoardPositionSelector(Direction.LEFT, s) +RIGHT_OF = lambda s: BoardPositionSelector(Direction.RIGHT, s) ADJACENT = lambda s: LEFT_OF(s) | RIGHT_OF(s) SELF_ADJACENT = ADJACENT(SELF) TARGET_ADJACENT = ADJACENT(TARGET) @@ -397,59 +282,50 @@ def __repr__(self): class RandomSelector(Selector): """ + Selects a 1-member random sample of the targets. This selector can be multiplied to select more than 1 target. """ - class SelectRandom: - def __init__(self, times): - self.times = times - - def __repr__(self): - return "" % (self.times) - - def merge(self, selector, entities): - return random.sample(entities, min(len(entities), self.times)) - - def __init__(self, selector): - self.slice = None - self.random = self.SelectRandom(1) - self.selector = selector - self.program = [Selector.MergeFilter] - self.program.extend(selector.program) - self.program.append(Selector.Merge) - self.program.append(self.random) - self.program.append(Selector.Unmerge) + def __init__(self, child: SelectorLike, times=1): + if isinstance(child, LazyValue): + child = LazyValueSelector(child) + self.child = child + self.times = times - def __repr__(self): - return "RANDOM(%r)" % (self.selector) + def eval(self, entities, source): + child_entities = self.child.eval(entities, source) + return random.sample(child_entities, + min(len(child_entities), self.times)) def __mul__(self, other): - result = RandomSelector(self.selector) - result.random.times = self.random.times * other - return result + return RandomSelector(self.child, self.times * other) RANDOM = RandomSelector +# Selects the highest and lowest attack entities, respectively +HIGHEST_ATK = lambda sel: RANDOM(sel + (AttrValue(GameTag.ATK) == OpAttr(sel, GameTag.ATK, max))) +LOWEST_ATK = lambda sel: RANDOM(sel + (AttrValue(GameTag.ATK) == OpAttr(sel, GameTag.ATK, min))) + class Controller(LazyValue): - def __init__(self, selector=None): - self.selector = selector + def __init__(self, child: "Optional[SelectorLike]"=None) -> None: + if isinstance(child, LazyValue): + child = LazyValueSelector(child) + self.child = child def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, self.selector or "") + return "%s(%s)" % (self.__class__.__name__, self.child or "") def _get_entity_attr(self, entity): return entity.controller def evaluate(self, source): - if self.selector is None: + if self.child is None: # If we don't have an argument, we default to SELF # This allows us to skip selector evaluation altogether. return self._get_entity_attr(source) - if isinstance(self.selector, LazyValue): - entities = [self.selector.evaluate(source)] else: - entities = self.selector.eval(source.game, source) + entities = self.child.eval(source.game, source) assert len(entities) == 1 return self._get_entity_attr(entities[0]) @@ -461,6 +337,7 @@ def _get_entity_attr(self, entity): FRIENDLY = CONTROLLER == Controller() ENEMY = CONTROLLER == Opponent() + def CONTROLLED_BY(selector): return AttrValue(GameTag.CONTROLLER) == Controller(selector) @@ -534,7 +411,7 @@ def CONTROLLED_BY(selector): TARGET_PLAYER = ALL_PLAYERS + CONTROLLED_BY(TARGET) CONTROLLER = ALL_PLAYERS + FRIENDLY OPPONENT = ALL_PLAYERS + ENEMY -CURRENT_PLAYER = ALL_PLAYERS + Selector(GameTag.CURRENT_PLAYER) +CURRENT_PLAYER = ALL_PLAYERS + EnumSelector(GameTag.CURRENT_PLAYER) FRIENDLY_HAND = IN_HAND + FRIENDLY FRIENDLY_DECK = IN_DECK + FRIENDLY