diff --git a/pokedex/tests/test_movesets.py b/pokedex/tests/test_movesets.py new file mode 100644 index 0000000..58609df --- /dev/null +++ b/pokedex/tests/test_movesets.py @@ -0,0 +1,49 @@ + +from pokedex.util.movesets import main + +result_map = {'OK': True, 'NO': False} + +def test_cases(): + for argstring in u""" + NO muk + NO beedrill rage pursuit agility endeavor toxic + NO ditto psystrike aeroblast mist-ball judgment + OK lugia aeroblast punishment dive snore + OK yanmega bug-bite bug-buzz tackle whirlwind + OK crobat brave-bird quick-attack gust zen-headbutt + OK bagon defense-curl fire-fang hydro-pump shadow-claw + OK volcarona endure toxic fly fire-blast + OK hippopotas curse revenge sleep-talk swallow + OK hippopotas curse revenge sleep-talk snore + OK smeargle bug-bite bug-buzz splash fire-blast + NO smeargle bug-bite chatter splash fire-blast + NO azurill muddy-water iron-tail scald mimic + OK salamence dragon-dance dragon-claw fire-blast earthquake -v platinum + OK crawdaunt brick-break rock-slide facade toxic -v platinum + NO cleffa tickle wish amnesia splash + OK tyrogue pursuit + NO mamoswine bite body-slam curse double-edge + OK raichu volt-tackle + OK raichu surf -v gold + OK pikachu volt-tackle thunderbolt bide + OK gyarados flail thrash iron-head outrage + OK drifblim memento gust thunderbolt pain-split + OK crobat nasty-plot brave-bird + OK crobat brave-bird hypnosis + NO crobat nasty-plot hypnosis + OK garchomp double-edge thrash outrage + OK nidoking counter disable amnesia head-smash + OK aggron stomp smellingsalt screech fire-punch + NO aggron endeavor body-slam + OK tyranitar dragon-dance outrage thunder-wave surf + NO butterfree morning-sun harden + OK pikachu reversal bide nasty-plot discharge + NO pikachu surf charge + NO blissey wish counter + NO clefairy copycat dynamicpunch + """.strip().splitlines(): + def run_test(argstring): + args = argstring.split() + assert main(args[1:]) == result_map[args[0]] + run_test.description = 'Moveset checker test: ' + argstring.strip() + yield run_test, argstring diff --git a/pokedex/util/movesets.py b/pokedex/util/movesets.py index ea78ab8..c59869e 100755 --- a/pokedex/util/movesets.py +++ b/pokedex/util/movesets.py @@ -25,6 +25,7 @@ class NoMoves(IllegalMoveCombination): pass class MovesNotLearnable(IllegalMoveCombination): pass class NoParent(IllegalMoveCombination): pass class TargetExcluded(IllegalMoveCombination): pass +class DuplicateMoves(IllegalMoveCombination): pass ### ### Generic helpers @@ -57,6 +58,10 @@ class MovesetSearch(object): self.session = session self.sketch = util.get(session, tables.Move, identifier=u'sketch').id + self.unsketchable = set([ + util.get(session, tables.Move, identifier=u'struggle').id, + util.get(session, tables.Move, identifier=u'chatter').id, + ]) self.no_eggs_group = util.get(session, tables.EggGroup, identifier=u'no-eggs').id self.ditto_group = util.get(session, tables.EggGroup, @@ -81,6 +86,9 @@ class MovesetSearch(object): self.goal_version_group = version.version_group_id self.goal_level = level + if len(self.goal_moves) < len(moves): + raise DuplicateMoves('Cannot learn duplicate moves') + if pokemon: self.goal_evolution_chain = pokemon.evolution_chain_id if self.goal_evolution_chain in self.excluded_families: @@ -103,9 +111,9 @@ class MovesetSearch(object): easy_moves, non_egg_moves = self.load_pokemon_moves( self.goal_evolution_chain, 'family') - hard_moves = self.goal_moves - easy_moves + self.hard_moves = self.goal_moves - easy_moves self.egg_moves = self.goal_moves - non_egg_moves - if hard_moves: + if self.hard_moves: # Have to breed! self.load_pokemon_moves(self.goal_evolution_chain, 'others') @@ -482,7 +490,7 @@ class MovesetSearch(object): success = True return success for group in goal_egg_groups: - handle(group, self.goal_moves, ()) + handle(group, self.hard_moves, ()) for moves in powerset(self.goal_moves): if moves: breeds_required[group][frozenset(moves)] = 1 @@ -575,10 +583,9 @@ default_costs = { 'tutor-once': 2100, # gen III: tutors only work once (well except Emerald frontier ones) # For technical reasons, 'sketch' is also used for learning Sketch and - # evolution-inducing moves by normal means, if they aren't included in the - # target moveset. + # by normal means, if it isn't included in the target moveset. # So the actual cost of a sketched move will be double this number. - 'sketch': 5, # Cheap. Exclude Smeargle if you think it's too cheap. + 'sketch': 100, # Cheap. Exclude Smeargle if you think it's too cheap. # Gimmick moves – we need to use this method to learn the move anyway, # so make a big-ish dent in the score if missing @@ -699,6 +706,12 @@ class GrowAction(Action, namedtuple('GrowAction', 'search level')): def __str__(self): return "Grow to level {0.level}".format(self) +class SketchAction(Action, namedtuple('SketchAction', 'search move_')): + keyword = 'grow' + + def __str__(self): + return "Sketch {0.move.name}".format(self) + class BreedAction(Action, namedtuple('BreedAction', 'search pokemon_ moves_')): keyword = 'grow' @@ -754,6 +767,7 @@ class PokemonNode(Node, Facade, namedtuple('PokemonNode', self.expand_grow(), self.expand_evolutions(), self.expand_breed(), + self.expand_sketch(), ) def expand_learn(self): @@ -896,6 +910,23 @@ class PokemonNode(Node, Facade, namedtuple('PokemonNode', yield cost, None, GoalBreedNode(search=self.search, dummy='g', version_group_=self.version_group_, moves_=self.moves_) + def expand_sketch(self): + moves = self.moves_ + for sketch in moves: + if sketch == self.search.sketch: + for sketched in self.search.goal_moves: + if sketched in self.search.unsketchable: + continue + if sketched not in moves: + moves = set(moves) + moves.remove(sketch) + moves.add(sketched) + action = SketchAction(self.search, sketched) + cost = self.search.costs['sketch'] + yield cost, action, self._replace( + new_level=False, moves_=frozenset(moves)) + return + class BaseBreedNode(Node): """Breed node This serves to prevent duplicate breeds, by storing only the needed info @@ -1035,7 +1066,7 @@ def main(argv): if args.debug: print 'Setup done' - template = "{cost:4} {action:50.50} {pokemon:10}{level:>3}{nl:1}{versions:2} {moves}" + template = "{cost:4} {action:50.50}{long:1} {pokemon:10}{level:>3}{nl:1}{versions:2} {moves}" for result in search: print '-' * 79 if no_results: @@ -1043,13 +1074,14 @@ def main(argv): print '**warning: search looked up output objects**' no_results = False print template.format(cost='Cost', action='Action', pokemon='Pokemon', - level='Lv.', nl='V', versions='er', + long='',level='Lv.', nl='V', versions='er', moves=''.join(m.name[0].lower() for m in moves)) for cost, action, node in reversed(list(result)): if action: print template.format( cost=cost, action=action, + long='>' if len(str(action)) > 50 else '', pokemon=node.pokemon.name, nl='.' if node.new_level else ' ', level=node.level,