def skills_list(self): skills = (db.pokedex_session.query(t.ConquestWarriorSkill) .join(t.ConquestWarriorSkill.names_local) .order_by(t.ConquestWarriorSkill.names_table.name.asc())) # We want to split the list up between generic skills anyone can get # and the unique skills a specific warlord gets at a specific rank. # The two player characters throw a wrench in that though so we just # assume any skill known only by warlords is unique, which happens to # work. warriors_and_ranks = sqla.orm.join(t.ConquestWarrior, t.ConquestWarriorRank) generic_clause = (sqla.sql.exists(warriors_and_ranks.select()) .where(sqla.and_( t.ConquestWarrior.archetype_id != None, t.ConquestWarriorRank.skill_id == t.ConquestWarriorSkill.id)) ) c.generic_skills = skills.filter(generic_clause).all() c.unique_skills = (skills.filter(~generic_clause) .options( sqla.orm.joinedload('warrior_ranks'), sqla.orm.joinedload('warrior_ranks.warrior') ) .all()) # Decide randomly which player gets displayed c.player_index = randint(0, 1) return render('/pokedex/conquest/skill_list.mako')
def threads(self, forum_id): c.forum = meta.Session.query(forum_model.Forum).get(forum_id) if not c.forum: abort(404) c.write_thread_form = WriteThreadForm() # nb: This will never show post-less threads. Oh well! last_post = aliased(forum_model.Post) threads_q = c.forum.threads \ .join((last_post, forum_model.Thread.last_post)) \ .order_by(last_post.posted_time.desc()) \ .options( contains_eager(forum_model.Thread.last_post, alias=last_post), joinedload('last_post.author'), ) c.num_threads = threads_q.count() try: c.skip = int(request.params.get('skip', 0)) except ValueError: abort(404) c.per_page = 89 c.threads = threads_q.offset(c.skip).limit(c.per_page) return render('/forum/threads.mako')
def profile_edit_commit(self, id, name=None): """Save profile changes.""" c.page_user = meta.Session.query(users_model.User).get(id) if not c.page_user: abort(404) # XXX could use some real permissions if c.page_user != c.user: abort(403) c.form = ProfileEditForm(request.params, name=c.page_user.name, ) if not c.form.validate(): return render('/users/profile_edit.mako') c.page_user.name = c.form.name.data meta.Session.add(c.page_user) meta.Session.commit() h.flash('Saved your profile.', icon='tick') redirect( url(controller='users', action='profile', id=c.page_user.id, name=c.page_user.name), code=303, )
def permissions(self): if not c.user.can('administrate'): abort(403) c.roles = meta.Session.query(users_model.Role) \ .order_by(users_model.Role.id.asc()).all() return render('/users/admin/permissions.mako')
def whos_that_pokemon(self): u"""A silly game that asks you to identify Pokémon by silhouette, cry, et al. """ c.javascripts.append(('pokedex', 'whos-that-pokemon')) return render('/pokedex/gadgets/whos_that_pokemon.mako')
def skills_list(self): skills = (db.pokedex_session.query(t.ConquestWarriorSkill).join( t.ConquestWarriorSkill.names_local).order_by( t.ConquestWarriorSkill.names_table.name.asc())) # We want to split the list up between generic skills anyone can get # and the unique skills a specific warlord gets at a specific rank. # The two player characters throw a wrench in that though so we just # assume any skill known only by warlords is unique, which happens to # work. warriors_and_ranks = sqla.orm.join(t.ConquestWarrior, t.ConquestWarriorRank) generic_clause = (sqla.sql.exists(warriors_and_ranks.select()).where( sqla.and_( t.ConquestWarrior.archetype_id != None, t.ConquestWarriorRank.skill_id == t.ConquestWarriorSkill.id))) c.generic_skills = skills.filter(generic_clause).all() c.unique_skills = (skills.filter(~generic_clause).options( sqla.orm.joinedload('warrior_ranks'), sqla.orm.joinedload('warrior_ranks.warrior')).all()) # Decide randomly which player gets displayed c.player_index = randint(0, 1) return render('/pokedex/conquest/skill_list.mako')
def abilities_list(self): c.abilities = (db.pokedex_session.query(t.Ability).join( t.Ability.names_local).filter( t.Ability.conquest_pokemon.any()).order_by( t.Ability.names_table.name.asc()).all()) return render('/pokedex/conquest/ability_list.mako')
def profile_edit_commit(self, id, name=None): """Save profile changes.""" c.page_user = meta.Session.query(users_model.User).get(id) if not c.page_user: abort(404) # XXX could use some real permissions if c.page_user != c.user: abort(403) c.form = ProfileEditForm( request.params, name=c.page_user.name, ) if not c.form.validate(): return render('/users/profile_edit.mako') c.page_user.name = c.form.name.data meta.Session.add(c.page_user) meta.Session.commit() h.flash('Saved your profile.', icon='tick') redirect( url(controller='users', action='profile', id=c.page_user.id, name=c.page_user.name), code=303, )
def warriors_list(self): c.warriors = (db.pokedex_session.query(t.ConquestWarrior).options( sqla.orm.subqueryload('ranks'), sqla.orm.subqueryload('ranks.stats'), sqla.orm.subqueryload('types')).order_by( t.ConquestWarrior.id).all()) return render('/pokedex/conquest/warrior_list.mako')
def abilities_list(self): c.abilities = (db.pokedex_session.query(t.Ability) .join(t.Ability.names_local) .filter(t.Ability.conquest_pokemon.any()) .order_by(t.Ability.names_table.name.asc()) .all() ) return render('/pokedex/conquest/ability_list.mako')
def moves_list(self): c.moves = (db.pokedex_session.query(t.Move).filter( t.Move.conquest_data.has()).options( sqla.orm.joinedload('conquest_data'), sqla.orm.joinedload('conquest_data.move_displacement'), ).join(t.Move.names_local).order_by( t.Move.names_table.name.asc()).all()) return render('/pokedex/conquest/move_list.mako')
def pokemon_list(self): c.pokemon = (db.pokedex_session.query(t.PokemonSpecies).filter( t.PokemonSpecies.conquest_order != None).options( sqla.orm.subqueryload('conquest_abilities'), sqla.orm.joinedload('conquest_move'), sqla.orm.subqueryload('conquest_stats'), sqla.orm.subqueryload('default_pokemon.types')).order_by( t.PokemonSpecies.conquest_order).all()) return render('/pokedex/conquest/pokemon_list.mako')
def kingdoms_list(self): c.kingdoms = (db.pokedex_session.query(t.ConquestKingdom) .options( sqla.orm.joinedload('type') ) .order_by(t.ConquestKingdom.id) .all() ) return render('/pokedex/conquest/kingdom_list.mako')
def kingdoms(self, name): try: c.kingdom = db.get_by_name_query(t.ConquestKingdom, name).one() except NoResultFound: return self._not_found() # We have pretty much nothing for kingdoms. Yet. c.prev_kingdom, c.next_kingdom = self._prev_next_id( c.kingdom, t.ConquestKingdom, 'id') return render('/pokedex/conquest/kingdom.mako')
def warriors_list(self): c.warriors = (db.pokedex_session.query(t.ConquestWarrior) .options( sqla.orm.subqueryload('ranks'), sqla.orm.subqueryload('ranks.stats'), sqla.orm.subqueryload('types') ) .order_by(t.ConquestWarrior.id) .all() ) return render('/pokedex/conquest/warrior_list.mako')
def document(self): """Render the error document.""" # code and messae might come from GET, *or* from the Pylons response # object. They seem to come from the latter most of the time, but # let's be safe anyway. response = request.environ.get('pylons.original_response') c.message = request.GET.get('message', response and response.status) c.code = request.GET.get('code', response and response.status_int) c.code = int(c.code) return render('/error.mako')
def list(self): u"""Show a list of all Pokémon currently uploaded to the GTS.""" gts_pokemons = meta.Session.query(gts_model.GTSPokemon).all() c.savefiles = [] for gts_pokemon in gts_pokemons: savefile = SaveFilePokemon(gts_pokemon.pokemon_blob) savefile.use_database_session(db.pokedex_session) c.savefiles.append(savefile) return render('/gts/list.mako')
def css(self): """Returns all the CSS in every plugin, concatenated.""" # This solution sucks donkey balls, but it's marginally better than # loading every single stylesheet manually, so it stays until I have # a better idea response.headers['Content-type'] = 'text/css; charset=utf-8' stylesheets = [] for css_file in config['spline.plugins.stylesheets']: stylesheets.append(render("/css/%s" % css_file)) return '\n'.join(stylesheets)
def skills(self, name): try: c.skill = (db.get_by_name_query(t.ConquestWarriorSkill, name) .one()) except NoResultFound: return self._not_found() ### Prev/next for header c.prev_skill, c.next_skill = self._prev_next_name( t.ConquestWarriorSkill, c.skill) return render('/pokedex/conquest/skill.mako')
def profile(self, id, name=None): """Main user profile. URL is /users/id:name, where 'name' only exists for readability and is entirely optional and ignored. """ c.page_user = meta.Session.query(users_model.User).get(id) if not c.page_user: abort(404) return render('/users/profile.mako')
def skills(self, name): try: c.skill = (db.get_by_name_query(t.ConquestWarriorSkill, name).one()) except NoResultFound: return self._not_found() ### Prev/next for header c.prev_skill, c.next_skill = self._prev_next_name( t.ConquestWarriorSkill, c.skill) return render('/pokedex/conquest/skill.mako')
def write(self, forum_id, thread_id): """Provides a form for posting to a thread.""" if not c.user.can('forum:create-post'): abort(403) try: c.thread = meta.Session.query(forum_model.Thread) \ .filter_by(id=thread_id, forum_id=forum_id).one() except NoResultFound: abort(404) c.write_post_form = WritePostForm(request.params) return render('/forum/write.mako')
def moves_list(self): c.moves = (db.pokedex_session.query(t.Move) .filter(t.Move.conquest_data.has()) .options( sqla.orm.joinedload('conquest_data'), sqla.orm.joinedload('conquest_data.move_displacement'), ) .join(t.Move.names_local) .order_by(t.Move.names_table.name.asc()) .all() ) return render('/pokedex/conquest/move_list.mako')
def write_thread(self, forum_id): """Provides a form for posting a new thread.""" if not c.user.can('forum:create-thread'): abort(403) try: c.forum = meta.Session.query(forum_model.Forum) \ .filter_by(id=forum_id).one() except NoResultFound: abort(404) c.write_thread_form = WriteThreadForm(request.params) return render('/forum/write_thread.mako')
def write_thread_commit(self, forum_id): """Posts a new thread.""" if not c.user.can('forum:create-thread'): abort(403) try: c.forum = meta.Session.query(forum_model.Forum) \ .filter_by(id=forum_id).one() except NoResultFound: abort(404) c.write_thread_form = WriteThreadForm(request.params) # Reshow the form on failure if not c.write_thread_form.validate(): return render('/forum/write_thread.mako') # Otherwise, add the post. c.forum = meta.Session.query(forum_model.Forum) \ .with_lockmode('update') \ .get(c.forum.id) thread = forum_model.Thread( forum_id=c.forum.id, subject=c.write_thread_form.subject.data, post_count=1, ) source = c.write_thread_form.content.data post = forum_model.Post( position=1, author_user_id=c.user.id, raw_content=source, content=spline.lib.markdown.translate(source), ) thread.posts.append(post) c.forum.threads.append(thread) meta.Session.commit() # Redirect to the new thread h.flash( "Contribution to the collective knowledge of the species successfully recorded." ) redirect( url(controller='forum', action='posts', forum_id=forum_id, thread_id=thread.id), code=303, )
def abilities(self, name): try: c.ability = db.get_by_name_query(t.Ability, name).one() except NoResultFound: return self._not_found() # XXX The ability might exist, but not in Conquest if not c.ability.conquest_pokemon: return self._not_found() c.prev_ability, c.next_ability = self._prev_next_name( t.Ability, c.ability, filters=[t.Ability.conquest_pokemon.any()]) return render('/pokedex/conquest/ability.mako')
def pokemon_list(self): c.pokemon = (db.pokedex_session.query(t.PokemonSpecies) .filter(t.PokemonSpecies.conquest_order != None) .options( sqla.orm.subqueryload('conquest_abilities'), sqla.orm.joinedload('conquest_move'), sqla.orm.subqueryload('conquest_stats'), sqla.orm.subqueryload('default_pokemon.types') ) .order_by(t.PokemonSpecies.conquest_order) .all() ) return render('/pokedex/conquest/pokemon_list.mako')
def profile_edit(self, id, name=None): """Main user profile editing.""" c.page_user = meta.Session.query(users_model.User).get(id) if not c.page_user: abort(404) # XXX could use some real permissions if c.page_user != c.user: abort(403) c.form = ProfileEditForm(request.params, name=c.page_user.name, ) return render('/users/profile_edit.mako')
def profile_edit(self, id, name=None): """Main user profile editing.""" c.page_user = meta.Session.query(users_model.User).get(id) if not c.page_user: abort(404) # XXX could use some real permissions if c.page_user != c.user: abort(403) c.form = ProfileEditForm( request.params, name=c.page_user.name, ) return render('/users/profile_edit.mako')
def write_thread_commit(self, forum_id): """Posts a new thread.""" if not c.user.can('forum:create-thread'): abort(403) try: c.forum = meta.Session.query(forum_model.Forum) \ .filter_by(id=forum_id).one() except NoResultFound: abort(404) c.write_thread_form = WriteThreadForm(request.params) # Reshow the form on failure if not c.write_thread_form.validate(): return render('/forum/write_thread.mako') # Otherwise, add the post. c.forum = meta.Session.query(forum_model.Forum) \ .with_lockmode('update') \ .get(c.forum.id) thread = forum_model.Thread( forum_id = c.forum.id, subject = c.write_thread_form.subject.data, post_count = 1, ) source = c.write_thread_form.content.data post = forum_model.Post( position = 1, author_user_id = c.user.id, raw_content = source, content = spline.lib.markdown.translate(source), ) thread.posts.append(post) c.forum.threads.append(thread) meta.Session.commit() # Redirect to the new thread h.flash("Contribution to the collective knowledge of the species successfully recorded.") redirect( url(controller='forum', action='posts', forum_id=forum_id, thread_id=thread.id), code=303, )
def forums(self): c.forums = meta.Session.query(forum_model.Forum) \ .order_by(forum_model.Forum.id.asc()) \ .all() # Get some forum stats. Cache them because they're a bit expensive to # compute. Expire after an hour. # XXX when there are admin controls, they'll need to nuke this cache # when messing with the forum list forum_cache = cache.get_cache('spline-forum', expiretime=3600) c.forum_activity = forum_cache.get_value(key='forum_activity', createfunc=get_forum_activity) c.forum_volume = forum_cache.get_value(key='forum_volume', createfunc=get_forum_volume) try: c.max_volume = max(c.forum_volume.itervalues()) or 1 except ValueError: # Empty database c.max_volume = 1 # Need to know the last post for each forum, in realtime c.last_post = {} last_post_subq = meta.Session.query( forum_model.Forum.id.label('forum_id'), func.max(forum_model.Post.posted_time).label('posted_time'), ) \ .outerjoin(forum_model.Thread) \ .outerjoin(forum_model.Post) \ .group_by(forum_model.Forum.id) \ .subquery() last_post_q = meta.Session.query( forum_model.Post, last_post_subq.c.forum_id, ) \ .join(( last_post_subq, forum_model.Post.posted_time == last_post_subq.c.posted_time, )) \ .options( joinedload('thread'), joinedload('author'), ) for post, forum_id in last_post_q: c.last_post[forum_id] = post return render('/forum/forums.mako')
def forums(self): c.forums = meta.Session.query(forum_model.Forum) \ .order_by(forum_model.Forum.id.asc()) \ .all() # Get some forum stats. Cache them because they're a bit expensive to # compute. Expire after an hour. # XXX when there are admin controls, they'll need to nuke this cache # when messing with the forum list forum_cache = cache.get_cache('spline-forum', expiretime=3600) c.forum_activity = forum_cache.get_value( key='forum_activity', createfunc=get_forum_activity) c.forum_volume = forum_cache.get_value( key='forum_volume', createfunc=get_forum_volume) try: c.max_volume = max(c.forum_volume.itervalues()) or 1 except ValueError: # Empty database c.max_volume = 1 # Need to know the last post for each forum, in realtime c.last_post = {} last_post_subq = meta.Session.query( forum_model.Forum.id.label('forum_id'), func.max(forum_model.Post.posted_time).label('posted_time'), ) \ .outerjoin(forum_model.Thread) \ .outerjoin(forum_model.Post) \ .group_by(forum_model.Forum.id) \ .subquery() last_post_q = meta.Session.query( forum_model.Post, last_post_subq.c.forum_id, ) \ .join(( last_post_subq, forum_model.Post.posted_time == last_post_subq.c.posted_time, )) \ .options( joinedload('thread'), joinedload('author'), ) for post, forum_id in last_post_q: c.last_post[forum_id] = post return render('/forum/forums.mako')
def write_commit(self, forum_id, thread_id): """Post to a thread.""" if not c.user.can('forum:create-post'): abort(403) try: c.thread = meta.Session.query(forum_model.Thread) \ .filter_by(id=thread_id, forum_id=forum_id).one() except NoResultFound: abort(404) c.write_post_form = WritePostForm(request.params) # Reshow the form on failure if not c.write_post_form.validate(): return render('/forum/write.mako') # Otherwise, add the post. c.thread = meta.Session.query(forum_model.Thread) \ .with_lockmode('update') \ .get(c.thread.id) source = c.write_post_form.content.data post = forum_model.Post( position=c.thread.post_count + 1, author_user_id=c.user.id, raw_content=source, content=spline.lib.markdown.translate(source), ) c.thread.posts.append(post) c.thread.post_count += 1 meta.Session.commit() # Redirect to the thread # XXX probably to the post instead; anchor? depends on paging scheme h.flash('Your uniqueness has been added to our own.') redirect( url(controller='forum', action='posts', forum_id=forum_id, thread_id=thread_id), code=303, )
def write_commit(self, forum_id, thread_id): """Post to a thread.""" if not c.user.can('forum:create-post'): abort(403) try: c.thread = meta.Session.query(forum_model.Thread) \ .filter_by(id=thread_id, forum_id=forum_id).one() except NoResultFound: abort(404) c.write_post_form = WritePostForm(request.params) # Reshow the form on failure if not c.write_post_form.validate(): return render('/forum/write.mako') # Otherwise, add the post. c.thread = meta.Session.query(forum_model.Thread) \ .with_lockmode('update') \ .get(c.thread.id) source = c.write_post_form.content.data post = forum_model.Post( position = c.thread.post_count + 1, author_user_id = c.user.id, raw_content = source, content = spline.lib.markdown.translate(source), ) c.thread.posts.append(post) c.thread.post_count += 1 meta.Session.commit() # Redirect to the thread # XXX probably to the post instead; anchor? depends on paging scheme h.flash('Your uniqueness has been added to our own.') redirect( url(controller='forum', action='posts', forum_id=forum_id, thread_id=thread_id), code=303, )
def moves(self, name): try: c.move = (db.get_by_name_query(t.Move, name).options( sqla.orm.joinedload('conquest_data'), sqla.orm.joinedload('conquest_pokemon'), sqla.orm.subqueryload('conquest_pokemon.conquest_abilities'), sqla.orm.subqueryload('conquest_pokemon.conquest_stats'), ).one()) except NoResultFound: return self._not_found() if not c.move.conquest_pokemon: return self._not_found() ### Prev/next for header c.prev_move, c.next_move = self._prev_next_name( t.Move, c.move, filters=[t.Move.conquest_pokemon.any()]) return render('/pokedex/conquest/move.mako')
def posts(self, forum_id, thread_id): try: c.thread = meta.Session.query(forum_model.Thread) \ .filter_by(id=thread_id, forum_id=forum_id).one() except NoResultFound: abort(404) c.write_post_form = WritePostForm() posts_q = c.thread.posts \ .order_by(forum_model.Post.position.asc()) \ .options(joinedload('author')) c.num_posts = c.thread.post_count try: c.skip = int(request.params.get('skip', 0)) except ValueError: abort(404) c.per_page = 89 c.posts = posts_q.offset(c.skip).limit(c.per_page) return render('/forum/posts.mako')
def moves(self, name): try: c.move = (db.get_by_name_query(t.Move, name) .options( sqla.orm.joinedload('conquest_data'), sqla.orm.joinedload('conquest_pokemon'), sqla.orm.subqueryload('conquest_pokemon.conquest_abilities'), sqla.orm.subqueryload('conquest_pokemon.conquest_stats'), ) .one()) except NoResultFound: return self._not_found() if not c.move.conquest_pokemon: return self._not_found() ### Prev/next for header c.prev_move, c.next_move = self._prev_next_name(t.Move, c.move, filters=[t.Move.conquest_pokemon.any()]) return render('/pokedex/conquest/move.mako')
def chain_breeding(self): u"""Given a Pokémon and an egg move it can learn, figure out the fastest way to get it that move. """ # XXX validate that the move matches the pokemon, in the form # TODO correctly handle e.g. munchlax-vs-snorlax # TODO write tests for this man c.form = ChainBreedingForm(request.GET) if not request.GET or not c.form.validate(): c.did_anything = False return render('/pokedex/gadgets/chain_breeding.mako') # The result will be an entire hierarchy of Pokémon, like this: # TARGET # |--- something compatible # | '--- something compatible here too # '--- something else compatible # ... with Pokémon as high in the tree as possible. # TODO make this a control yo version_group = db.pokedex_session.query(tables.VersionGroup).get( 11) # b/w target = c.form.pokemon.data # First, find every potential Pokémon in the tree: that is, every # Pokémon that can learn this move at all. # It's useful to know which methods go with which Pokémon, so let's # store the pokemon_moves rows per Pokémon. # XXX this should exclude Ditto and unbreedables candidates = {} pokemon_moves = db.pokedex_session.query(tables.PokemonMove) \ .filter_by( move_id=c.form.moves.data.id, version_group_id=version_group.id, ) pokemon_by_egg_group = defaultdict(set) for pokemon_move in pokemon_moves: candidates \ .setdefault(pokemon_move.pokemon, []) \ .append(pokemon_move) for egg_group in pokemon_move.pokemon.egg_groups: pokemon_by_egg_group[egg_group].add(pokemon_move.pokemon) # Breeding only really cares about egg group combinations, not the # individual Pokémon; for all intents and purposes, any (5, 9) Pokémon # can be replaced by any other. So build the tree out of those, first. egg_group_candidates = set( tuple(pokemon.egg_groups) for pokemon in candidates.keys()) # The above are actually edges in a graph; (5, 9) indicates that # there's a viable connection between all Pokémon in egg groups 5 and # 9. The target Pokémon is sort of an anonymous node that has edges to # its two breeding groups. So build a graph! egg_graph = dict() # Create an isolated node for every group all_egg_groups = set(egg_group for pair in egg_group_candidates for egg_group in pair) all_egg_groups.add('me') # special sentinel value for the target for egg_group in all_egg_groups: egg_graph[egg_group] = dict( node=egg_group, adjacent=[], ) # Fill in the adjacent edges for egg_group in target.egg_groups: egg_graph['me']['adjacent'].append(egg_graph[egg_group]) egg_graph[egg_group]['adjacent'].append(egg_graph['me']) for egg_groups in egg_group_candidates: if len(egg_groups) == 1: # Pokémon in only one egg group aren't useful here continue a, b = egg_groups egg_graph[a]['adjacent'].append(egg_graph[b]) egg_graph[b]['adjacent'].append(egg_graph[a]) # And now trim that down to just a tree, where nodes are placed as # close to the root as possible. # Start from the root ('me'), expand outwards, and remove edges that # lead to nodes on a higher level. Duplicates within a level are OK. egg_tree = egg_graph['me'] seen = set(['me']) current_level = [egg_tree] current_seen = True while current_seen: next_level = [] current_seen = set() for node in current_level: node['adjacent'] = [ _ for _ in node['adjacent'] if _['node'] not in seen ] node['adjacent'].sort(key=lambda _: _['node'].id) current_seen.update(_['node'] for _ in node['adjacent']) next_level.extend(node['adjacent']) current_level = next_level seen.update(current_seen) c.pokemon = c.form.pokemon.data c.pokemon_by_egg_group = pokemon_by_egg_group c.egg_group_tree = egg_tree c.did_anything = True return render('/pokedex/gadgets/chain_breeding.mako')
def capture_rate(self): """Calculate the successful capture rate of every Ball given a target Pokémon and a set of battle conditions. """ c.javascripts.append(('pokedex', 'pokedex-gadgets')) c.form = CaptureRateForm(request.params) valid_form = False if request.params: valid_form = c.form.validate() if valid_form: c.results = {} c.pokemon = c.form.pokemon.data level = c.form.level.data # Overrule a 'yes' for opposite genders if this Pokémon is a # genderless or single-gender species if c.pokemon.species.gender_rate in (-1, 0, 8): c.form.twitterpating.data = False percent_hp = c.form.current_hp.data / 100 status_bonus = 10 if c.form.status_ailment.data in ('PAR', 'BRN', 'PSN'): status_bonus = 15 elif c.form.status_ailment.data in ('SLP', 'FRZ'): status_bonus = 20 # Little wrapper around capture_chance... def capture_chance(ball_bonus=10, **kwargs): return pokedex.formulae.capture_chance( percent_hp=percent_hp, capture_rate=c.pokemon.species.capture_rate, status_bonus=status_bonus, ball_bonus=ball_bonus, **kwargs) ### Do some math! # c.results is a dict of ball_name => chance_tuples. # (It would be great, but way inconvenient, to use item objects.) # chance_tuples is a list of (condition, is_active, chances): # - condition: a string describing some mutually-exclusive # condition the ball responds to # - is_active: a boolean indicating whether this condition is # currently met # - chances: an iterable of chances as returned from capture_chance # This is a teeny shortcut. only = lambda _: [CaptureChance('', True, _)] normal_chance = capture_chance() # Gen I c.results[u'Poké Ball'] = only(normal_chance) c.results[u'Great Ball'] = only(capture_chance(15)) c.results[u'Ultra Ball'] = only(capture_chance(20)) c.results[u'Master Ball'] = only((1.0, 0, 0, 0, 0)) c.results[u'Safari Ball'] = only(capture_chance(15)) # Gen II # NOTE: All the Gen II balls, as of HG/SS, modify CAPTURE RATE and # leave the ball bonus alone. relative_level = None if c.form.level.data and c.form.your_level.data: # -1 because equality counts as bucket zero relative_level = (c.form.your_level.data - 1) \ // c.form.level.data # Heavy Ball partitions by 102.4 kg. Weights are stored as... # hectograms. So. weight_class = int((c.pokemon.weight - 1) / 1024) # Ugh. is_moony = c.pokemon.species.identifier in ( u'nidoran-m', u'nidorina', u'nidoqueen', u'nidoran-f', u'nidorino', u'nidoking', u'cleffa', u'clefairy', u'clefable', u'igglybuff', u'jigglypuff', u'wigglytuff', u'skitty', u'delcatty', ) is_skittish = c.pokemon.base_stat('speed', 0) >= 100 c.results[u'Level Ball'] = [ CaptureChance(u'Your level ≤ target level', relative_level == 0, normal_chance), CaptureChance(u'Target level < your level ≤ 2 * target level', relative_level == 1, capture_chance(capture_bonus=20)), CaptureChance( u'2 * target level < your level ≤ 4 * target level', relative_level in (2, 3), capture_chance(capture_bonus=40)), CaptureChance(u'4 * target level < your level', relative_level >= 4, capture_chance(capture_bonus=80)), ] c.results[u'Lure Ball'] = [ CaptureChance(u'Hooked on a rod', c.form.terrain.data == 'fishing', capture_chance(capture_bonus=30)), CaptureChance(u'Otherwise', c.form.terrain.data != 'fishing', normal_chance), ] c.results[u'Moon Ball'] = [ CaptureChance(u'Target evolves with a Moon Stone', is_moony, capture_chance(capture_bonus=40)), CaptureChance(u'Otherwise', not is_moony, normal_chance), ] c.results[u'Friend Ball'] = only(normal_chance) c.results[u'Love Ball'] = [ CaptureChance( u'Target is opposite gender of your Pokémon and the same species', c.form.twitterpating.data, capture_chance(capture_bonus=80)), CaptureChance(u'Otherwise', not c.form.twitterpating.data, normal_chance), ] c.results[u'Heavy Ball'] = [ CaptureChance(u'Target weight ≤ 102.4 kg', weight_class == 0, capture_chance(capture_modifier=-20)), CaptureChance( u'102.4 kg < target weight ≤ 204.8 kg', weight_class == 1, capture_chance(capture_modifier=-20)), # sic; game bug CaptureChance(u'204.8 kg < target weight ≤ 307.2 kg', weight_class == 2, capture_chance(capture_modifier=20)), CaptureChance(u'307.2 kg < target weight ≤ 409.6 kg', weight_class == 3, capture_chance(capture_modifier=30)), CaptureChance(u'409.6 kg < target weight', weight_class >= 4, capture_chance(capture_modifier=40)), ] c.results[u'Fast Ball'] = [ CaptureChance(u'Target has base Speed of 100 or more', is_skittish, capture_chance(capture_bonus=40)), CaptureChance(u'Otherwise', not is_skittish, normal_chance), ] c.results[u'Sport Ball'] = only(capture_chance(15)) # Gen III is_nettable = any(_.identifier in ('bug', 'water') for _ in c.pokemon.types) c.results[u'Premier Ball'] = only(normal_chance) c.results[u'Repeat Ball'] = [ CaptureChance(u'Target is already in Pokédex', c.form.caught_before.data, capture_chance(30)), CaptureChance(u'Otherwise', not c.form.caught_before.data, normal_chance), ] # Timer and Nest Balls use a gradient instead of partitions! Keep # the same desc but just inject the right bonus if there's enough # to get the bonus correct. Otherwise, assume the best case c.results[u'Timer Ball'] = [ CaptureChance(u'Better in later turns, caps at turn 30', True, capture_chance(40)), ] if c.form.level.data: c.results[u'Nest Ball'] = [ CaptureChance( u'Better against lower-level targets, worst at level 30+', True, capture_chance(max(10, 40 - c.form.level.data))) ] else: c.results[u'Nest Ball'] = [ CaptureChance( u'Better against lower-level targets, worst at level 30+', False, capture_chance(40)), ] c.results[u'Net Ball'] = [ CaptureChance(u'Target is Water or Bug', is_nettable, capture_chance(30)), CaptureChance(u'Otherwise', not is_nettable, normal_chance), ] c.results[u'Dive Ball'] = [ CaptureChance(u'Currently fishing or surfing', c.form.terrain.data in ('fishing', 'surfing'), capture_chance(35)), CaptureChance(u'Otherwise', c.form.terrain.data == 'land', normal_chance), ] c.results[u'Luxury Ball'] = only(normal_chance) # Gen IV c.results[u'Heal Ball'] = only(normal_chance) c.results[u'Quick Ball'] = [ CaptureChance(u'First turn', True, capture_chance(40)), CaptureChance(u'Otherwise', True, normal_chance), ] c.results[u'Dusk Ball'] = [ CaptureChance(u'During the night and while walking in caves', c.form.is_dark.data, capture_chance(35)), CaptureChance(u'Otherwise', not c.form.is_dark.data, normal_chance), ] c.results[u'Cherish Ball'] = only(normal_chance) c.results[u'Park Ball'] = only(capture_chance(2550)) # Template needs to know how to find expected number of attempts c.capture_chance = capture_chance c.expected_attempts = expected_attempts c.expected_attempts_oh_no = expected_attempts_oh_no # Template also needs real item objects to create links pokeball_query = db.pokedex_session.query(tables.Item) \ .join(tables.ItemCategory, tables.ItemPocket) \ .filter(tables.ItemPocket.identifier == 'pokeballs') c.pokeballs = dict((item.name, item) for item in pokeball_query) else: c.results = None return render('/pokedex/gadgets/capture_rate.mako')
def warriors(self, name): try: c.warrior = db.get_by_name_query(t.ConquestWarrior, name).one() except NoResultFound: return self._not_found() c.prev_warrior, c.next_warrior = self._prev_next_id( c.warrior, t.ConquestWarrior, 'id') c.rank_count = len(c.warrior.ranks) c.perfect_links = (c.warrior.ranks[-1].max_links.filter_by( max_link=100).join(t.PokemonSpecies).order_by( t.PokemonSpecies.conquest_order).all()) ### Stats # Percentiles! Percentiles are hard. stats = t.ConquestWarriorRankStatMap all_stats = sqla.orm.aliased(t.ConquestWarriorRankStatMap) # We need this to be a float so the percentile equation can divide by it stat_count = sqla.cast(sqla.func.count(all_stats.base_stat), sqla.types.FLOAT) # Grab all of a rank's stats, and also get percentiles stat_q = (db.pokedex_session.query( stats.warrior_stat_id, stats.base_stat).join( all_stats, stats.warrior_stat_id == all_stats.warrior_stat_id).group_by( stats.warrior_rank_id, stats.warrior_stat_id, stats. base_stat).order_by(stats.warrior_stat_id).add_columns( sqla.func.sum( sqla.cast(stats.base_stat > all_stats.base_stat, sqla.types.INT)) / stat_count + sqla.func.sum( sqla.cast(stats.base_stat == all_stats.base_stat, sqla.types.INT)) / stat_count / 2)) # XXX There's probably a better way to query all the names stat_names = [ stat.name for stat in db.pokedex_session.query(t.ConquestWarriorStat). order_by(t.ConquestWarriorStat.id).all() ] # Go through the query for each rank c.stats = [] for rank in c.warrior.ranks: c.stats.append([]) info = stat_q.filter(stats.warrior_rank_id == rank.id).all() # We need a bit more info than what the query directly provides for stat, value, percentile in info: percentile = float(percentile) c.stats[-1].append( (stat_names[stat - 1], value, percentile, bar_color(percentile, 0.9), bar_color(percentile, 0.8))) ### Max links default_link = 70 if c.warrior.archetype else 90 c.link_form = LinkThresholdForm(request.params, link=default_link) if request.params and c.link_form.validate(): link_threshold = c.link_form.link.data else: link_threshold = default_link link_pokemon = (db.pokedex_session.query( t.ConquestMaxLink.pokemon_species_id).filter( t.ConquestMaxLink.warrior_rank_id == c.warrior.ranks[-1].id). filter(t.ConquestMaxLink.max_link >= link_threshold)) max_links = [] for rank in c.warrior.ranks: max_links.append( rank.max_links.filter( t.ConquestMaxLink.pokemon_species_id.in_(link_pokemon)). join(t.PokemonSpecies).order_by( t.PokemonSpecies.conquest_order).options( sqla.orm.joinedload('pokemon'), sqla.orm.subqueryload('pokemon.conquest_abilities'), sqla.orm.subqueryload('pokemon.conquest_stats'), ).all()) c.max_links = izip(*max_links) return render('/pokedex/conquest/warrior.mako')
def stat_calculator(self): """Calculates, well, stats.""" # XXX this form handling is all pretty bad. consider ripping it out # and really thinking about how this ought to work. # possible TODO: # - more better error checking # - track effort gained on the fly (as well as exp for auto level up?) # - track evolutions? # - graphs of potential stats? # - given a pokemon and its genes and effort, graph all stats by level # - given a pokemon and its gene results, graph approximate stats by level...? # - given a pokemon, graph its min and max possible calc'd stats... # - this logic is pretty hairy; use a state object? # Add the stat-based fields # XXX get rid of this stupid filter c.stats = db.pokedex_session.query(tables.Stat) \ .filter(tables.Stat.id <= 6) \ .all() # Make sure there are the same number of level, stat, and effort # fields. Add an extra one (for more data), as long as we're not about # to shorten num_dupes = c.num_data_points = len(request.GET.getall('level')) if not request.GET.get('shorten', False): num_dupes += 1 class F(StatCalculatorForm): level = DuplicateField( fields.IntegerField(u'Level', default=100, validators=[wtforms.validators.NumberRange(1, 100)]), min_entries=num_dupes, ) stat = DuplicateField( StatField(c.stats, fields.IntegerField(default=0, validators=[ wtforms.validators.NumberRange(min=0, max=700)])), min_entries=num_dupes, ) effort = DuplicateField( StatField(c.stats, fields.IntegerField(default=0, validators=[ wtforms.validators.NumberRange(min=0, max=255)])), min_entries=num_dupes, ) ### Parse form and so forth c.form = F(request.GET) c.results = None # XXX shim if not request.GET or not c.form.validate(): return render('/pokedex/gadgets/stat_calculator.mako') if not c.num_data_points: # Zero? How did you manage that? # XXX this doesn't actually appear in the page :D c.form.level.errors.append(u"Please enter at least one level") return render('/pokedex/gadgets/stat_calculator.mako') # Possible shorten and redirect if c.form.needs_shortening: # This is stupid, but update_params doesn't understand unicode kwargs = c.form.short_formdata for key, vals in kwargs.iteritems(): kwargs[key] = [unicode(val).encode('utf8') for val in vals] redirect(h.update_params(url.current(), **kwargs)) def filter_genes(genes, f): """Teeny helper function to only keep possible genes that fit the given lambda. """ genes &= set(gene for gene in genes if f(gene)) # Okay, do some work! # Dumb method for now -- XXX change this to do a binary search. # Run through every possible value for each stat, see if it matches # input, and give the green light if so. pokemon = c.pokemon = c.form.pokemon.data nature = c.form.nature.data if nature and nature.is_neutral: # Neutral nature is equivalent to none at all nature = None # Start with lists of possibly valid genes and cut down from there c.valid_range = defaultdict(dict) # stat => level => (min, max) valid_genes = {} # Stuff for finding the next useful level level_indices = sorted(range(c.num_data_points), key=lambda i: c.form.level[i].data) max_level_index = level_indices[-1] max_given_level = c.form.level[max_level_index].data c.next_useful_level = 100 for stat in c.stats: ### Bunch of setup, per stat # XXX let me stop typing this, christ if stat.identifier == u'hp': func = pokedex.formulae.calculated_hp else: func = pokedex.formulae.calculated_stat base_stat = pokemon.stat(stat).base_stat nature_mod = 1.0 if not nature: pass elif nature.increased_stat == stat: nature_mod = 1.1 elif nature.decreased_stat == stat: nature_mod = 0.9 meta_calculate_stat = functools.partial(func, base_stat=base_stat, nature=nature_mod) # Start out with everything being considered valid valid_genes[stat] = set(range(32)) for i in range(c.num_data_points): stat_in = c.form.stat[i][stat].data effort_in = c.form.effort[i][stat].data level = c.form.level[i].data calculate_stat = functools.partial(meta_calculate_stat, effort=effort_in, level=level) c.valid_range[stat][level] = min_stat, max_stat = \ calculate_stat(iv=0), calculate_stat(iv=31) ### Actual work! # Quick simple check: if the input is totally outside the valid # range, no need to calculate anything if not min_stat <= stat_in <= max_stat: valid_genes[stat] = set() if not valid_genes[stat]: continue # Run through and maybe invalidate each gene filter_genes(valid_genes[stat], lambda gene: calculate_stat(iv=gene) == stat_in) # Find the next "useful" level. This is the lowest level at which # at least two possible genes give different stats, given how much # effort the Pokémon has now. # TODO should this show the *highest* level necessary to get exact? if valid_genes[stat]: min_gene = min(valid_genes[stat]) max_gene = max(valid_genes[stat]) max_effort = c.form.effort[max_level_index][stat].data while level < c.next_useful_level and \ meta_calculate_stat(level=level, effort=max_effort, iv=min_gene) == \ meta_calculate_stat(level=level, effort=max_effort, iv=max_gene): level += 1 c.next_useful_level = level c.form.level[-1].data = c.next_useful_level # Hidden Power type if c.form.hp_type.data: # Shift the type id to make Fighting (id=2) #0 hp_type = c.form.hp_type.data.id - 2 # See below for how this is calculated. # We know x * 15 // 63 == hp_type, and want a range for x. # hp_type * 63 / 15 is the LOWER bound, though you need to ceil() # it to find the lower integral bound. # The same thing for (hp_type + 1) is the lower bound for the next # type, which is one more than our upper bound. Cool. min_x = int(math.ceil(hp_type * 63 / 15)) max_x = int(math.ceil((hp_type + 1) * 63 / 15) - 1) # Now we need to find how many bits from the left will stay the # same throughout this x-range, so we know that those bits must # belong to the corresponding stats. Easy if you note that, if # min_x and max_x have the same leftmost n bits, so will every # integer between them. first_good_bit = None for n in range(6): # Convert "3" to 0b111000 # 3 -> 0b1000 -> 0b111 -> 0b111000 mask = 63 ^ ((1 << n) - 1) if min_x & mask == max_x & mask: first_good_bit = n break if first_good_bit is not None: # OK, cool! Now we know some number of stats are either # definitely odd or definitely even. for stat_id in range(first_good_bit, 6): bit = (min_x >> stat_id) & 1 stat = c.stats[stat_id] filter_genes(valid_genes[stat], lambda gene: gene & 1 == bit) # Characteristic; needs to be last since it imposes a maximum hint = c.form.hint.data if hint: # Knock out everything that doesn't match its mod-5 filter_genes(valid_genes[hint.stat], lambda gene: gene % 5 == hint.gene_mod_5) # Also, the characteristic is only shown for the highest gene. So, # no other stat can be higher than the new maximum for the hinted # stat. (Need the extra -1 in case there are actually no valid # genes left; max() dies with an empty sequence.) max_gene = max(itertools.chain(valid_genes[hint.stat], (-1,))) for genes in valid_genes.values(): filter_genes(genes, lambda gene: gene <= max_gene) # Possibly calculate Hidden Power's type and power, if the results are # exact c.exact = all(len(genes) == 1 for genes in valid_genes.values()) if c.exact: # HP uses bit 0 of each gene for the type, and bit 1 for the power. # These bits are used to make new six-bit numbers, where HP goes to # bit 0, Attack to bit 1, etc. Our stats are, conveniently, # already in the right order type_det = 0 power_det = 0 for i, stat in enumerate(c.stats): stat_value, = valid_genes[stat] type_det += (stat_value & 0x01) << i power_det += (stat_value & 0x02) << i # Our types are also in the correct order, except that we start # from 1 rather than 0, and HP skips Normal c.hidden_power_type = db.pokedex_session.query(tables.Type) \ .get(type_det * 15 // 63 + 2) c.hidden_power_power = power_det * 40 // 63 + 30 # Used for a link c.hidden_power = db.pokedex_session.query(tables.Move) \ .filter_by(identifier='hidden-power').one() # Turn those results into something more readable. # Template still needs valid_genes for drawing the graph c.results = {} c.valid_genes = valid_genes for stat in c.stats: # 1, 2, 3, 5 => "1-3, 5" # Find consecutive ranges of numbers and turn them into strings. # nb: The final dummy iteration with n = None is to more easily add # the last range to the parts list left_endpoint = None parts = [] elements = sorted(valid_genes[stat]) for last_n, n in zip([None] + elements, elements + [None]): if (n is None and left_endpoint is not None) or \ (last_n is not None and last_n + 1 < n): # End of a subrange; break off what we have if left_endpoint == last_n: parts.append(u"{0}".format(last_n)) else: parts.append(u"{0}–{1}".format(left_endpoint, last_n)) if left_endpoint is None or last_n + 1 < n: # Starting a new subrange; remember the new left end left_endpoint = n c.results[stat] = u', '.join(parts) c.stat_graph_chunk_color = stat_graph_chunk_color c.prompt_for_more = ( not c.exact and c.next_useful_level > max_given_level) return render('/pokedex/gadgets/stat_calculator.mako')
def compare_pokemon(self): u"""Pokémon comparison. Takes up to eight Pokémon and shows a page that lists their stats, moves, etc. side-by-side. """ # Note that this gadget doesn't use wtforms at all, since there're only # two fields and the major one is handled very specially. c.did_anything = False # Form controls use version group c.version_groups = db.pokedex_session.query(tables.VersionGroup) \ .order_by(tables.VersionGroup.id.asc()) \ .options(eagerload('versions')) \ .all() # Grab the version to use for moves, defaulting to the most current try: c.version_group = db.pokedex_session.query(tables.VersionGroup) \ .get(request.params['version_group']) except (KeyError, NoResultFound): c.version_group = c.version_groups[-1] # Some manual URL shortening, if necessary... if request.params.get('shorten', False): short_params = self._shorten_compare_pokemon( request.params.getall('pokemon')) redirect(url.current(**short_params)) FoundPokemon = namedtuple('FoundPokemon', ['pokemon', 'form', 'suggestions', 'input']) # The Pokémon themselves go into c.pokemon. This list should always # have eight FoundPokemon elements c.found_pokemon = [None] * self.NUM_COMPARED_POKEMON # Run through the list, ensuring at least 8 Pokémon are entered pokemon_input = request.params.getall('pokemon') \ + [u''] * self.NUM_COMPARED_POKEMON for i in range(self.NUM_COMPARED_POKEMON): raw_pokemon = pokemon_input[i].strip() if not raw_pokemon: # Use a junk placeholder tuple c.found_pokemon[i] = FoundPokemon( pokemon=None, form=None, suggestions=None, input=u'') continue results = db.pokedex_lookup.lookup( raw_pokemon, valid_types=['pokemon_species', 'pokemon_form']) # Two separate things to do here. # 1: Use the first result as the actual Pokémon pokemon = None form = None if results: result = results[0].object c.did_anything = True # 1.5: Deal with form matches if isinstance(result, tables.PokemonForm): pokemon = result.pokemon form = result else: pokemon = result.default_pokemon form = pokemon.default_form # 2: Use the other results as suggestions. Doing this informs the # template that this was a multi-match suggestions = None if len(results) == 1 and results[0].exact: # Don't do anything for exact single matches pass else: # OK, extract options. But no more than, say, three. # Remember both the language and the Pokémon, in the case of # foreign matches suggestions = [ (_.name, _.iso3166) for _ in results[1:4] ] # Construct a tuple and slap that bitch in there c.found_pokemon[i] = FoundPokemon(pokemon, form, suggestions, raw_pokemon) # There are a lot of links to similar incarnations of this page. # Provide a closure for constructing the links easily def create_comparison_link(target, replace_with=None, move=0): u"""Manipulates the list of Pokémon before creating a link. `target` is the FoundPokemon to be operated upon. It can be either replaced with a new string or moved left/right. """ new_found_pokemon = c.found_pokemon[:] # Do the swapping first if move: idx1 = new_found_pokemon.index(target) idx2 = (idx1 + move) % len(new_found_pokemon) new_found_pokemon[idx1], new_found_pokemon[idx2] = \ new_found_pokemon[idx2], new_found_pokemon[idx1] # Construct a new query query_pokemon = [] for found_pokemon in new_found_pokemon: if found_pokemon is None: # Empty slot query_pokemon.append(u'') elif found_pokemon is target and replace_with != None: # Substitute a new Pokémon query_pokemon.append(replace_with) else: # Keep what we have now query_pokemon.append(found_pokemon.input) short_params = self._shorten_compare_pokemon(query_pokemon) return url.current(**short_params) c.create_comparison_link = create_comparison_link # Setup only done if the page is actually showing if c.did_anything: c.stats = db.pokedex_session.query(tables.Stat) \ .filter(~ tables.Stat.is_battle_only) \ .all() # Relative numbers -- breeding and stats # Construct a nested dictionary of label => pokemon => (value, pct) # `pct` is percentage from the minimum to maximum value c.relatives = dict() # Use the label from the page as the key, because why not relative_things = [ (u'Base EXP', lambda pokemon: pokemon.base_experience), (u'Base happiness', lambda pokemon: pokemon.species.base_happiness), (u'Capture rate', lambda pokemon: pokemon.species.capture_rate), ] def relative_stat_factory(local_stat): return lambda pokemon: pokemon.stat(local_stat).base_stat for stat in c.stats: relative_things.append((stat.name, relative_stat_factory(stat))) relative_things.append(( u'Base stat total', lambda pokemon: sum(pokemon.stat(stat).base_stat for stat in c.stats) )) # Assemble the data unique_pokemon = set(fp.pokemon for fp in c.found_pokemon if fp.pokemon ) for label, getter in relative_things: c.relatives[label] = dict() # Get all the values at once; need to get min and max to figure # out relative position numbers = dict() for pokemon in unique_pokemon: numbers[pokemon] = getter(pokemon) min_number = min(numbers.values()) max_number = max(numbers.values()) # Rig a little function to figure out the percentage, making # sure to avoid division by zero if min_number == max_number: calc = lambda n: 1.0 else: calc = lambda n: 1.0 * (n - min_number) \ / (max_number - min_number) for pokemon in unique_pokemon: c.relatives[label][pokemon] \ = numbers[pokemon], calc(numbers[pokemon]) ### Relative sizes raw_heights = dict(enumerate( fp.pokemon.height if fp and fp.pokemon else 0 for fp in c.found_pokemon )) raw_heights['trainer'] = pokedex_helpers.trainer_height c.heights = pokedex_helpers.scale_sizes(raw_heights) raw_weights = dict(enumerate( fp.pokemon.weight if fp and fp.pokemon else 0 for fp in c.found_pokemon )) raw_weights['trainer'] = pokedex_helpers.trainer_weight c.weights = pokedex_helpers.scale_sizes(raw_weights, dimensions=2) ### Moves # Constructs a table like the pokemon-moves table, except each row # is a move and it indicates which Pokémon learn it. Still broken # up by method. # So, need a dict of method => move => pokemons. c.moves = defaultdict(lambda: defaultdict(set)) # And similarly for level moves, level => pokemon => moves c.level_moves = defaultdict(lambda: defaultdict(list)) q = db.pokedex_session.query(tables.PokemonMove) \ .filter(tables.PokemonMove.version_group == c.version_group) \ .filter(tables.PokemonMove.pokemon_id.in_( _.id for _ in unique_pokemon)) \ .options( eagerload('move'), eagerload('method'), ) for pokemon_move in q: c.moves[pokemon_move.method][pokemon_move.move].add( pokemon_move.pokemon) if pokemon_move.level: c.level_moves[pokemon_move.level] \ [pokemon_move.pokemon].append(pokemon_move.move) return render('/pokedex/gadgets/compare_pokemon.mako')
def chain_breeding(self): u"""Given a Pokémon and an egg move it can learn, figure out the fastest way to get it that move. """ # XXX validate that the move matches the pokemon, in the form # TODO correctly handle e.g. munchlax-vs-snorlax # TODO write tests for this man c.form = ChainBreedingForm(request.GET) if not request.GET or not c.form.validate(): c.did_anything = False return render('/pokedex/gadgets/chain_breeding.mako') # The result will be an entire hierarchy of Pokémon, like this: # TARGET # |--- something compatible # | '--- something compatible here too # '--- something else compatible # ... with Pokémon as high in the tree as possible. # TODO make this a control yo version_group = db.pokedex_session.query(tables.VersionGroup).get(11) # b/w target = c.form.pokemon.data # First, find every potential Pokémon in the tree: that is, every # Pokémon that can learn this move at all. # It's useful to know which methods go with which Pokémon, so let's # store the pokemon_moves rows per Pokémon. # XXX this should exclude Ditto and unbreedables candidates = {} pokemon_moves = db.pokedex_session.query(tables.PokemonMove) \ .filter_by( move_id=c.form.moves.data.id, version_group_id=version_group.id, ) pokemon_by_egg_group = defaultdict(set) for pokemon_move in pokemon_moves: candidates \ .setdefault(pokemon_move.pokemon, []) \ .append(pokemon_move) for egg_group in pokemon_move.pokemon.egg_groups: pokemon_by_egg_group[egg_group].add(pokemon_move.pokemon) # Breeding only really cares about egg group combinations, not the # individual Pokémon; for all intents and purposes, any (5, 9) Pokémon # can be replaced by any other. So build the tree out of those, first. egg_group_candidates = set( tuple(pokemon.egg_groups) for pokemon in candidates.keys() ) # The above are actually edges in a graph; (5, 9) indicates that # there's a viable connection between all Pokémon in egg groups 5 and # 9. The target Pokémon is sort of an anonymous node that has edges to # its two breeding groups. So build a graph! egg_graph = dict() # Create an isolated node for every group all_egg_groups = set(egg_group for pair in egg_group_candidates for egg_group in pair) all_egg_groups.add('me') # special sentinel value for the target for egg_group in all_egg_groups: egg_graph[egg_group] = dict( node=egg_group, adjacent=[], ) # Fill in the adjacent edges for egg_group in target.egg_groups: egg_graph['me']['adjacent'].append(egg_graph[egg_group]) egg_graph[egg_group]['adjacent'].append(egg_graph['me']) for egg_groups in egg_group_candidates: if len(egg_groups) == 1: # Pokémon in only one egg group aren't useful here continue a, b = egg_groups egg_graph[a]['adjacent'].append(egg_graph[b]) egg_graph[b]['adjacent'].append(egg_graph[a]) # And now trim that down to just a tree, where nodes are placed as # close to the root as possible. # Start from the root ('me'), expand outwards, and remove edges that # lead to nodes on a higher level. Duplicates within a level are OK. egg_tree = egg_graph['me'] seen = set(['me']) current_level = [egg_tree] current_seen = True while current_seen: next_level = [] current_seen = set() for node in current_level: node['adjacent'] = [_ for _ in node['adjacent'] if _['node'] not in seen] node['adjacent'].sort(key=lambda _: _['node'].id) current_seen.update(_['node'] for _ in node['adjacent']) next_level.extend(node['adjacent']) current_level = next_level seen.update(current_seen) c.pokemon = c.form.pokemon.data c.pokemon_by_egg_group = pokemon_by_egg_group c.egg_group_tree = egg_tree c.did_anything = True return render('/pokedex/gadgets/chain_breeding.mako')
def capture_rate(self): """Calculate the successful capture rate of every Ball given a target Pokémon and a set of battle conditions. """ c.javascripts.append(('pokedex', 'pokedex-gadgets')) c.form = CaptureRateForm(request.params) valid_form = False if request.params: valid_form = c.form.validate() if valid_form: c.results = {} c.pokemon = c.form.pokemon.data level = c.form.level.data # Overrule a 'yes' for opposite genders if this Pokémon is a # genderless or single-gender species if c.pokemon.species.gender_rate in (-1, 0, 8): c.form.twitterpating.data = False percent_hp = c.form.current_hp.data / 100 status_bonus = 10 if c.form.status_ailment.data in ('PAR', 'BRN', 'PSN'): status_bonus = 15 elif c.form.status_ailment.data in ('SLP', 'FRZ'): status_bonus = 20 # Little wrapper around capture_chance... def capture_chance(ball_bonus=10, **kwargs): return pokedex.formulae.capture_chance( percent_hp=percent_hp, capture_rate=c.pokemon.species.capture_rate, status_bonus=status_bonus, ball_bonus=ball_bonus, **kwargs ) ### Do some math! # c.results is a dict of ball_name => chance_tuples. # (It would be great, but way inconvenient, to use item objects.) # chance_tuples is a list of (condition, is_active, chances): # - condition: a string describing some mutually-exclusive # condition the ball responds to # - is_active: a boolean indicating whether this condition is # currently met # - chances: an iterable of chances as returned from capture_chance # This is a teeny shortcut. only = lambda _: [CaptureChance( '', True, _ )] normal_chance = capture_chance() # Gen I c.results[u'Poké Ball'] = only(normal_chance) c.results[u'Great Ball'] = only(capture_chance(15)) c.results[u'Ultra Ball'] = only(capture_chance(20)) c.results[u'Master Ball'] = only((1.0, 0, 0, 0, 0)) c.results[u'Safari Ball'] = only(capture_chance(15)) # Gen II # NOTE: All the Gen II balls, as of HG/SS, modify CAPTURE RATE and # leave the ball bonus alone. relative_level = None if c.form.level.data and c.form.your_level.data: # -1 because equality counts as bucket zero relative_level = (c.form.your_level.data - 1) \ // c.form.level.data # Heavy Ball partitions by 102.4 kg. Weights are stored as... # hectograms. So. weight_class = int((c.pokemon.weight - 1) / 1024) # Ugh. is_moony = c.pokemon.species.identifier in ( u'nidoran-m', u'nidorina', u'nidoqueen', u'nidoran-f', u'nidorino', u'nidoking', u'cleffa', u'clefairy', u'clefable', u'igglybuff', u'jigglypuff', u'wigglytuff', u'skitty', u'delcatty', ) is_skittish = c.pokemon.stat('speed').base_stat >= 100 c.results[u'Level Ball'] = [ CaptureChance(u'Your level ≤ target level', relative_level == 0, normal_chance), CaptureChance(u'Target level < your level ≤ 2 * target level', relative_level == 1, capture_chance(capture_bonus=20)), CaptureChance(u'2 * target level < your level ≤ 4 * target level', relative_level in (2, 3), capture_chance(capture_bonus=40)), CaptureChance(u'4 * target level < your level', relative_level >= 4, capture_chance(capture_bonus=80)), ] c.results[u'Lure Ball'] = [ CaptureChance(u'Hooked on a rod', c.form.terrain.data == 'fishing', capture_chance(capture_bonus=30)), CaptureChance(u'Otherwise', c.form.terrain.data != 'fishing', normal_chance), ] c.results[u'Moon Ball'] = [ CaptureChance(u'Target evolves with a Moon Stone', is_moony, capture_chance(capture_bonus=40)), CaptureChance(u'Otherwise', not is_moony, normal_chance), ] c.results[u'Friend Ball'] = only(normal_chance) c.results[u'Love Ball'] = [ CaptureChance(u'Target is opposite gender of your Pokémon and the same species', c.form.twitterpating.data, capture_chance(capture_bonus=80)), CaptureChance(u'Otherwise', not c.form.twitterpating.data, normal_chance), ] c.results[u'Heavy Ball'] = [ CaptureChance(u'Target weight ≤ 102.4 kg', weight_class == 0, capture_chance(capture_modifier=-20)), CaptureChance(u'102.4 kg < target weight ≤ 204.8 kg', weight_class == 1, capture_chance(capture_modifier=-20)), # sic; game bug CaptureChance(u'204.8 kg < target weight ≤ 307.2 kg', weight_class == 2, capture_chance(capture_modifier=20)), CaptureChance(u'307.2 kg < target weight ≤ 409.6 kg', weight_class == 3, capture_chance(capture_modifier=30)), CaptureChance(u'409.6 kg < target weight', weight_class >= 4, capture_chance(capture_modifier=40)), ] c.results[u'Fast Ball'] = [ CaptureChance(u'Target has base Speed of 100 or more', is_skittish, capture_chance(capture_bonus=40)), CaptureChance(u'Otherwise', not is_skittish, normal_chance), ] c.results[u'Sport Ball'] = only(capture_chance(15)) # Gen III is_nettable = any(_.identifier in ('bug', 'water') for _ in c.pokemon.types) c.results[u'Premier Ball'] = only(normal_chance) c.results[u'Repeat Ball'] = [ CaptureChance(u'Target is already in Pokédex', c.form.caught_before.data, capture_chance(30)), CaptureChance(u'Otherwise', not c.form.caught_before.data, normal_chance), ] # Timer and Nest Balls use a gradient instead of partitions! Keep # the same desc but just inject the right bonus if there's enough # to get the bonus correct. Otherwise, assume the best case c.results[u'Timer Ball'] = [ CaptureChance(u'Better in later turns, caps at turn 30', True, capture_chance(40)), ] if c.form.level.data: c.results[u'Nest Ball'] = [ CaptureChance(u'Better against lower-level targets, worst at level 30+', True, capture_chance(max(10, 40 - c.form.level.data))) ] else: c.results[u'Nest Ball'] = [ CaptureChance(u'Better against lower-level targets, worst at level 30+', False, capture_chance(40)), ] c.results[u'Net Ball'] = [ CaptureChance(u'Target is Water or Bug', is_nettable, capture_chance(30)), CaptureChance(u'Otherwise', not is_nettable, normal_chance), ] c.results[u'Dive Ball'] = [ CaptureChance(u'Currently fishing or surfing', c.form.terrain.data in ('fishing', 'surfing'), capture_chance(35)), CaptureChance(u'Otherwise', c.form.terrain.data == 'land', normal_chance), ] c.results[u'Luxury Ball'] = only(normal_chance) # Gen IV c.results[u'Heal Ball'] = only(normal_chance) c.results[u'Quick Ball'] = [ CaptureChance(u'First turn', True, capture_chance(40)), CaptureChance(u'Otherwise', True, normal_chance), ] c.results[u'Dusk Ball'] = [ CaptureChance(u'During the night and while walking in caves', c.form.is_dark.data, capture_chance(35)), CaptureChance(u'Otherwise', not c.form.is_dark.data, normal_chance), ] c.results[u'Cherish Ball'] = only(normal_chance) c.results[u'Park Ball'] = only(capture_chance(2550)) # Template needs to know how to find expected number of attempts c.capture_chance = capture_chance c.expected_attempts = expected_attempts c.expected_attempts_oh_no = expected_attempts_oh_no # Template also needs real item objects to create links pokeball_query = db.pokedex_session.query(tables.Item) \ .join(tables.ItemCategory, tables.ItemPocket) \ .filter(tables.ItemPocket.identifier == 'pokeballs') c.pokeballs = dict( (item.name, item) for item in pokeball_query ) else: c.results = None return render('/pokedex/gadgets/capture_rate.mako')
def compare_pokemon(self): u"""Pokémon comparison. Takes up to eight Pokémon and shows a page that lists their stats, moves, etc. side-by-side. """ # Note that this gadget doesn't use wtforms at all, since there're only # two fields and the major one is handled very specially. c.did_anything = False # Form controls use version group # We join with VGPMM to filter out version groups which we lack move # data for. *coughxycough* c.version_groups = db.pokedex_session.query(tables.VersionGroup) \ .join(tables.VersionGroupPokemonMoveMethod) \ .order_by(tables.VersionGroup.order.asc()) \ .options(eagerload('versions')) \ .all() # Grab the version to use for moves, defaulting to the most current try: c.version_group = db.pokedex_session.query(tables.VersionGroup) \ .get(request.params['version_group']) except (KeyError, NoResultFound): c.version_group = c.version_groups[-1] # Some manual URL shortening, if necessary... if request.params.get('shorten', False): short_params = self._shorten_compare_pokemon( request.params.getall('pokemon')) redirect(url.current(**short_params)) FoundPokemon = namedtuple('FoundPokemon', ['pokemon', 'form', 'suggestions', 'input']) # The Pokémon themselves go into c.pokemon. This list should always # have eight FoundPokemon elements c.found_pokemon = [None] * self.NUM_COMPARED_POKEMON # Run through the list, ensuring at least 8 Pokémon are entered pokemon_input = request.params.getall('pokemon') \ + [u''] * self.NUM_COMPARED_POKEMON for i in range(self.NUM_COMPARED_POKEMON): raw_pokemon = pokemon_input[i].strip() if not raw_pokemon: # Use a junk placeholder tuple c.found_pokemon[i] = FoundPokemon(pokemon=None, form=None, suggestions=None, input=u'') continue results = db.pokedex_lookup.lookup( raw_pokemon, valid_types=['pokemon_species', 'pokemon_form']) # Two separate things to do here. # 1: Use the first result as the actual Pokémon pokemon = None form = None if results: result = results[0].object c.did_anything = True # 1.5: Deal with form matches if isinstance(result, tables.PokemonForm): pokemon = result.pokemon form = result else: pokemon = result.default_pokemon form = pokemon.default_form # 2: Use the other results as suggestions. Doing this informs the # template that this was a multi-match suggestions = None if len(results) == 1 and results[0].exact: # Don't do anything for exact single matches pass else: # OK, extract options. But no more than, say, three. # Remember both the language and the Pokémon, in the case of # foreign matches suggestions = [(_.name, _.iso3166) for _ in results[1:4]] # Construct a tuple and slap that bitch in there c.found_pokemon[i] = FoundPokemon(pokemon, form, suggestions, raw_pokemon) # There are a lot of links to similar incarnations of this page. # Provide a closure for constructing the links easily def create_comparison_link(target, replace_with=None, move=0): u"""Manipulates the list of Pokémon before creating a link. `target` is the FoundPokemon to be operated upon. It can be either replaced with a new string or moved left/right. """ new_found_pokemon = c.found_pokemon[:] # Do the swapping first if move: idx1 = new_found_pokemon.index(target) idx2 = (idx1 + move) % len(new_found_pokemon) new_found_pokemon[idx1], new_found_pokemon[idx2] = \ new_found_pokemon[idx2], new_found_pokemon[idx1] # Construct a new query query_pokemon = [] for found_pokemon in new_found_pokemon: if found_pokemon is None: # Empty slot query_pokemon.append(u'') elif found_pokemon is target and replace_with != None: # Substitute a new Pokémon query_pokemon.append(replace_with) else: # Keep what we have now query_pokemon.append(found_pokemon.input) short_params = self._shorten_compare_pokemon(query_pokemon) return url.current(**short_params) c.create_comparison_link = create_comparison_link # Setup only done if the page is actually showing if c.did_anything: c.stats = db.pokedex_session.query(tables.Stat) \ .filter(~ tables.Stat.is_battle_only) \ .all() # Relative numbers -- breeding and stats # Construct a nested dictionary of label => pokemon => (value, pct) # `pct` is percentage from the minimum to maximum value c.relatives = dict() # Use the label from the page as the key, because why not relative_things = [ (u'Base EXP', lambda pokemon: pokemon.base_experience), (u'Base happiness', lambda pokemon: pokemon.species.base_happiness), (u'Capture rate', lambda pokemon: pokemon.species.capture_rate), ] def relative_stat_factory(local_stat): return lambda pokemon: pokemon.base_stat(local_stat, 0) for stat in c.stats: relative_things.append( (stat.name, relative_stat_factory(stat))) relative_things.append((u'Base stat total', lambda pokemon: sum( pokemon.base_stat(stat, 0) for stat in c.stats))) # Assemble the data unique_pokemon = set(fp.pokemon for fp in c.found_pokemon if fp.pokemon) for label, getter in relative_things: c.relatives[label] = dict() # Get all the values at once; need to get min and max to figure # out relative position numbers = dict() for pokemon in unique_pokemon: numbers[pokemon] = getter(pokemon) min_number = min(numbers.values()) max_number = max(numbers.values()) # Rig a little function to figure out the percentage, making # sure to avoid division by zero if min_number == max_number: calc = lambda n: 1.0 else: calc = lambda n: 1.0 * (n - min_number) \ / (max_number - min_number) for pokemon in unique_pokemon: c.relatives[label][pokemon] \ = numbers[pokemon], calc(numbers[pokemon]) ### Relative sizes raw_heights = dict( enumerate(fp.pokemon.height if fp and fp.pokemon else 0 for fp in c.found_pokemon)) raw_heights['trainer'] = pokedex_helpers.trainer_height c.heights = pokedex_helpers.scale_sizes(raw_heights) raw_weights = dict( enumerate(fp.pokemon.weight if fp and fp.pokemon else 0 for fp in c.found_pokemon)) raw_weights['trainer'] = pokedex_helpers.trainer_weight c.weights = pokedex_helpers.scale_sizes(raw_weights, dimensions=2) ### Moves # Constructs a table like the pokemon-moves table, except each row # is a move and it indicates which Pokémon learn it. Still broken # up by method. # So, need a dict of method => move => pokemons. c.moves = defaultdict(lambda: defaultdict(set)) # And similarly for level moves, level => pokemon => moves c.level_moves = defaultdict(lambda: defaultdict(list)) q = db.pokedex_session.query(tables.PokemonMove) \ .filter(tables.PokemonMove.version_group == c.version_group) \ .filter(tables.PokemonMove.pokemon_id.in_( _.id for _ in unique_pokemon)) \ .options( eagerload('move'), eagerload('method'), ) for pokemon_move in q: c.moves[pokemon_move.method][pokemon_move.move].add( pokemon_move.pokemon) if pokemon_move.level: c.level_moves[pokemon_move.level] \ [pokemon_move.pokemon].append(pokemon_move.move) # Get TM/HM numbers for display purposes c.machines = dict((machine.move, machine.machine_number) for machine in c.version_group.machines) return render('/pokedex/gadgets/compare_pokemon.mako')
def index(self): """Magicaltastic front page. Plugins can register a hook called 'frontpage_updates_<type>' to add updates to the front page. `<type>` is an arbitrary string indicating the sort of update the plugin knows how to handle; for example, spline-forum has a `frontpage_updates_forum` hook for posting news from a specific forum. Hook handlers should return a list of FrontPageUpdate objects. Standard hook parameters are: `limit`, the maximum number of items that should ever be returned. `max_age`, the number of seconds after which items expire. `title`, a name for the source. `icon`, an icon to show next to its name. `limit` and `max_age` are also global options. Updates are configured in the .ini like so: spline-frontpage.sources.foo = updatetype spline-frontpage.sources.foo.opt1 = val1 spline-frontpage.sources.foo.opt2 = val2 Note that the 'foo' name is completely arbitrary and is only used for grouping options together. This will result in a call to: run_hooks('frontpage_updates_updatetype', opt1=val1, opt2=val2) Plugins may also respond to the `frontpage_extras` hook with other interesting things to put on the front page. There's no way to customize the order of these extras or which appear and which don't, at the moment. Such hooks should return an object with at least a `template` attribute; the template will be called with the object passed in as its `obj` argument. Local plugins can override the fairly simple index.mako template to customize the front page layout. """ updates = [] global_limit = config['spline-frontpage.limit'] global_max_age = max_age_to_datetime( config['spline-frontpage.max_age']) c.sources = config['spline-frontpage.sources'] for source in c.sources: new_updates = source.poll(global_limit, global_max_age) updates.extend(new_updates) # Little optimization: once there are global_limit items, anything # older than the oldest cannot possibly make it onto the list. So, # bump global_max_age to that oldest time if this is ever the case. updates.sort(key=lambda obj: obj.time, reverse=True) del updates[global_limit:] if updates and len(updates) == global_limit: global_max_age = updates[-1].time # Find the oldest unseen item, to draw a divider after it. # If this stays as None, the divider goes at the top c.last_seen_item = None # Could have a timestamp in the stash if this is a user, or in a cookie # if this session has ever been logged out... times = [] for source in (c.user.stash, request.cookies): try: times.append( int(source['frontpage-last-seen-time']) ) except (KeyError, ValueError): pass if times: last_seen_time = datetime.datetime.fromtimestamp(max(times)) for update in updates: if update.time > last_seen_time: c.last_seen_item = update else: break # Save ~now~ as the last-seen time now = datetime.datetime.now().strftime('%s') if c.user: c.user.stash['frontpage-last-seen-time'] = now meta.Session.add(c.user) else: response.set_cookie('frontpage-last-seen-time', now) # Done! Feed to template c.updates = updates # Hook for non-update interesting things to put on the front page. # This hook should return objects with a 'template' attribute, and # whatever else they need c.extras = run_hooks('frontpage_extras') ret = render('/index.mako') # Commit AFTER rendering the template! Committing invalidates # everything in the session, undoing any eagerloading that may have # been done by sources meta.Session.commit() return ret
def stat_calculator(self): """Calculates, well, stats.""" # XXX this form handling is all pretty bad. consider ripping it out # and really thinking about how this ought to work. # possible TODO: # - more better error checking # - track effort gained on the fly (as well as exp for auto level up?) # - track evolutions? # - graphs of potential stats? # - given a pokemon and its genes and effort, graph all stats by level # - given a pokemon and its gene results, graph approximate stats by level...? # - given a pokemon, graph its min and max possible calc'd stats... # - this logic is pretty hairy; use a state object? # Add the stat-based fields stat_query = (db.pokedex_session.query( tables.Stat).filter(tables.Stat.is_battle_only == False)) c.stats = (stat_query.order_by(tables.Stat.id).all()) hidden_power_stats = (stat_query.order_by( tables.Stat.game_index).all()) # Make sure there are the same number of level, stat, and effort # fields. Add an extra one (for more data), as long as we're not about # to shorten num_dupes = c.num_data_points = len(request.GET.getall('level')) if not request.GET.get('shorten', False): num_dupes += 1 class F(StatCalculatorForm): level = DuplicateField( fields.IntegerField( u'Level', default=100, validators=[wtforms.validators.NumberRange(1, 100)]), min_entries=num_dupes, ) stat = DuplicateField( StatField( c.stats, fields.IntegerField(default=0, validators=[ wtforms.validators.NumberRange( min=0, max=700) ])), min_entries=num_dupes, ) effort = DuplicateField( StatField( c.stats, fields.IntegerField(default=0, validators=[ wtforms.validators.NumberRange( min=0, max=255) ])), min_entries=num_dupes, ) ### Parse form and so forth c.form = F(request.GET) c.results = None # XXX shim if not request.GET or not c.form.validate(): return render('/pokedex/gadgets/stat_calculator.mako') if not c.num_data_points: # Zero? How did you manage that? # XXX this doesn't actually appear in the page :D c.form.level.errors.append(u"Please enter at least one level") return render('/pokedex/gadgets/stat_calculator.mako') # Possible shorten and redirect if c.form.needs_shortening: # This is stupid, but update_params doesn't understand unicode kwargs = c.form.short_formdata for key, vals in kwargs.iteritems(): kwargs[key] = [unicode(val).encode('utf8') for val in vals] redirect(h.update_params(url.current(), **kwargs)) def filter_genes(genes, f): """Teeny helper function to only keep possible genes that fit the given lambda. """ genes &= set(gene for gene in genes if f(gene)) # Okay, do some work! # Dumb method for now -- XXX change this to do a binary search. # Run through every possible value for each stat, see if it matches # input, and give the green light if so. pokemon = c.pokemon = c.form.pokemon.data nature = c.form.nature.data if nature and nature.is_neutral: # Neutral nature is equivalent to none at all nature = None # Start with lists of possibly valid genes and cut down from there c.valid_range = defaultdict(dict) # stat => level => (min, max) valid_genes = {} # Stuff for finding the next useful level level_indices = sorted(range(c.num_data_points), key=lambda i: c.form.level[i].data) max_level_index = level_indices[-1] max_given_level = c.form.level[max_level_index].data c.next_useful_level = 100 for stat in c.stats: ### Bunch of setup, per stat # XXX let me stop typing this, christ if stat.identifier == u'hp': func = pokedex.formulae.calculated_hp else: func = pokedex.formulae.calculated_stat base_stat = pokemon.base_stat(stat, 0) if not base_stat: valid_genes[stat] = set(range(32)) continue nature_mod = 1.0 if not nature: pass elif nature.increased_stat == stat: nature_mod = 1.1 elif nature.decreased_stat == stat: nature_mod = 0.9 meta_calculate_stat = functools.partial(func, base_stat=base_stat, nature=nature_mod) # Start out with everything being considered valid valid_genes[stat] = set(range(32)) for i in range(c.num_data_points): stat_in = c.form.stat[i][stat].data effort_in = c.form.effort[i][stat].data level = c.form.level[i].data calculate_stat = functools.partial(meta_calculate_stat, effort=effort_in, level=level) c.valid_range[stat][level] = min_stat, max_stat = \ calculate_stat(iv=0), calculate_stat(iv=31) ### Actual work! # Quick simple check: if the input is totally outside the valid # range, no need to calculate anything if not min_stat <= stat_in <= max_stat: valid_genes[stat] = set() if not valid_genes[stat]: continue # Run through and maybe invalidate each gene filter_genes(valid_genes[stat], lambda gene: calculate_stat(iv=gene) == stat_in) # Find the next "useful" level. This is the lowest level at which # at least two possible genes give different stats, given how much # effort the Pokémon has now. # TODO should this show the *highest* level necessary to get exact? if valid_genes[stat]: min_gene = min(valid_genes[stat]) max_gene = max(valid_genes[stat]) max_effort = c.form.effort[max_level_index][stat].data while level < c.next_useful_level and \ meta_calculate_stat(level=level, effort=max_effort, iv=min_gene) == \ meta_calculate_stat(level=level, effort=max_effort, iv=max_gene): level += 1 c.next_useful_level = level c.form.level[-1].data = c.next_useful_level # Hidden Power type if c.form.hp_type.data: # Shift the type id to make Fighting (id=2) #0 hp_type = c.form.hp_type.data.id - 2 # See below for how this is calculated. # We know x * 15 // 63 == hp_type, and want a range for x. # hp_type * 63 / 15 is the LOWER bound, though you need to ceil() # it to find the lower integral bound. # The same thing for (hp_type + 1) is the lower bound for the next # type, which is one more than our upper bound. Cool. min_x = int(math.ceil(hp_type * 63 / 15)) max_x = int(math.ceil((hp_type + 1) * 63 / 15) - 1) # Now we need to find how many bits from the left will stay the # same throughout this x-range, so we know that those bits must # belong to the corresponding stats. Easy if you note that, if # min_x and max_x have the same leftmost n bits, so will every # integer between them. first_good_bit = None for n in range(6): # Convert "3" to 0b111000 # 3 -> 0b1000 -> 0b111 -> 0b111000 mask = 63 ^ ((1 << n) - 1) if min_x & mask == max_x & mask: first_good_bit = n break if first_good_bit is not None: # OK, cool! Now we know some number of stats are either # definitely odd or definitely even. for stat_id in range(first_good_bit, 6): bit = (min_x >> stat_id) & 1 stat = hidden_power_stats[stat_id] filter_genes(valid_genes[stat], lambda gene: gene & 1 == bit) # Characteristic; needs to be last since it imposes a maximum hint = c.form.hint.data if hint: # Knock out everything that doesn't match its mod-5 filter_genes(valid_genes[hint.stat], lambda gene: gene % 5 == hint.gene_mod_5) # Also, the characteristic is only shown for the highest gene. So, # no other stat can be higher than the new maximum for the hinted # stat. (Need the extra -1 in case there are actually no valid # genes left; max() dies with an empty sequence.) max_gene = max(itertools.chain(valid_genes[hint.stat], (-1, ))) for genes in valid_genes.values(): filter_genes(genes, lambda gene: gene <= max_gene) # Possibly calculate Hidden Power's type and power, if the results are # exact c.exact = all(len(genes) == 1 for genes in valid_genes.values()) if c.exact: # HP uses bit 0 of each gene for the type, and bit 1 for the power. # These bits are used to make new six-bit numbers, where HP goes to # bit 0, Attack to bit 1, etc. type_det = 0 power_det = 0 for i, stat in enumerate(hidden_power_stats): stat_value, = valid_genes[stat] type_det += (stat_value & 0x01) << i power_det += (stat_value & 0x02) >> 1 << i # Our types are also in the correct order, except that we start # from 1 rather than 0, and HP skips Normal c.hidden_power_type = db.pokedex_session.query(tables.Type) \ .get(type_det * 15 // 63 + 2) c.hidden_power_power = power_det * 40 // 63 + 30 # Used for a link c.hidden_power = db.pokedex_session.query(tables.Move) \ .filter_by(identifier='hidden-power').one() # Turn those results into something more readable. # Template still needs valid_genes for drawing the graph c.results = {} c.valid_genes = valid_genes for stat in c.stats: # 1, 2, 3, 5 => "1-3, 5" # Find consecutive ranges of numbers and turn them into strings. # nb: The final dummy iteration with n = None is to more easily add # the last range to the parts list left_endpoint = None parts = [] elements = sorted(valid_genes[stat]) for last_n, n in zip([None] + elements, elements + [None]): if (n is None and left_endpoint is not None) or \ (last_n is not None and last_n + 1 < n): # End of a subrange; break off what we have if left_endpoint == last_n: parts.append(u"{0}".format(last_n)) else: parts.append(u"{0}–{1}".format(left_endpoint, last_n)) if left_endpoint is None or last_n + 1 < n: # Starting a new subrange; remember the new left end left_endpoint = n c.results[stat] = u', '.join(parts) c.stat_graph_chunk_color = stat_graph_chunk_color c.prompt_for_more = (not c.exact and c.next_useful_level > max_given_level) return render('/pokedex/gadgets/stat_calculator.mako')
def pokemon(self, name=None): try: c.pokemon = db.pokemon_query(name, None).one() except NoResultFound: return self._not_found() c.semiform_pokemon = c.pokemon c.pokemon = c.pokemon.species # This Pokémon might exist, but not appear in Conquest if c.pokemon.conquest_order is None: return self._not_found() ### Previous and next for the header c.prev_pokemon, c.next_pokemon = self._prev_next_id( c.pokemon, t.PokemonSpecies, 'conquest_order') ### Type efficacy c.type_efficacies = defaultdict(lambda: 100) for target_type in c.semiform_pokemon.types: for type_efficacy in target_type.target_efficacies: c.type_efficacies[type_efficacy.damage_type] *= \ type_efficacy.damage_factor # The defaultdict starts at 100, and every damage factor is # a percentage. Dividing by 100 with every iteration turns the # damage factor into a decimal percentage taken of the starting # 100, without using floats and regardless of number of types c.type_efficacies[type_efficacy.damage_type] //= 100 ### Evolution # Shamelessly lifted from the main controller and tweaked. # # Format is a matrix as follows: # [ # [ None, Eevee, Vaporeon, None ] # [ None, None, Jolteon, None ] # [ None, None, Flareon, None ] # ... etc ... # ] # That is, each row is a physical row in the resulting table, and each # contains four elements, one per row: Baby, Base, Stage 1, Stage 2. # The Pokémon are actually dictionaries with 'pokemon' and 'span' keys, # where the span is used as the HTML cell's rowspan -- e.g., Eevee has a # total of seven descendents, so it would need to span 7 rows. c.evolution_table = [] # Prefetch the evolution details family = (db.pokedex_session.query(t.PokemonSpecies).filter( t.PokemonSpecies.evolution_chain_id == c.pokemon.evolution_chain_id).options( sqla.orm.subqueryload('conquest_evolution'), sqla.orm.joinedload('conquest_evolution.stat'), sqla.orm.joinedload('conquest_evolution.kingdom'), sqla.orm.joinedload('conquest_evolution.gender'), sqla.orm.joinedload('conquest_evolution.item'), ).all()) # Strategy: build this table going backwards. # Find a leaf, build the path going back up to its root. Remember all # of the nodes seen along the way. Find another leaf not seen so far. # Build its path backwards, sticking it to a seen node if one exists. # Repeat until there are no unseen nodes. seen_nodes = {} while True: # First, find some unseen nodes unseen_leaves = [] for species in family: if species in seen_nodes: continue children = [] # A Pokémon is a leaf if it has no evolutionary children, so... for possible_child in family: if possible_child in seen_nodes: continue if possible_child.parent_species == species: children.append(possible_child) if len(children) == 0: unseen_leaves.append(species) # If there are none, we're done! Bail. # Note that it is impossible to have any unseen non-leaves if there # are no unseen leaves; every leaf's ancestors become seen when we # build a path to it. if len(unseen_leaves) == 0: break unseen_leaves.sort(key=lambda x: x.id) leaf = unseen_leaves[0] # root, parent_n, ... parent2, parent1, leaf current_path = [] # Finally, go back up the tree to the root current_species = leaf while current_species: # The loop bails just after current_species is no longer the # root, so this will give us the root after the loop ends; # we need to know if it's a baby to see whether to indent the # entire table below root_pokemon = current_species if current_species in seen_nodes: current_node = seen_nodes[current_species] # Don't need to repeat this node; the first instance will # have a rowspan current_path.insert(0, None) else: current_node = { 'species': current_species, 'span': 0, } current_path.insert(0, current_node) seen_nodes[current_species] = current_node # This node has one more row to span: our current leaf current_node['span'] += 1 current_species = current_species.parent_species # We want every path to have four nodes: baby, basic, stage 1 and 2. # Every root node is basic, unless it's defined as being a baby. # So first, add an empty baby node at the beginning if this is not # a baby. # We use an empty string to indicate an empty cell, as opposed to a # complete lack of cell due to a tall cell from an earlier row. if not root_pokemon.is_baby: current_path.insert(0, '') # Now pad to four if necessary. while len(current_path) < 4: current_path.append('') c.evolution_table.append(current_path) ### Stats # Conquest has a nonstandard stat, Range, which shouldn't be included # in the total, so we have to do things a bit differently. # XXX actually do things differently instead of just fudging the same # thing to work c.stats = {} # stat => { border, background, percentile } stat_total = 0 total_stat_rows = db.pokedex_session.query(t.ConquestPokemonStat) \ .filter_by(stat=c.pokemon.conquest_stats[0].stat) \ .count() for pokemon_stat in c.pokemon.conquest_stats: stat_info = c.stats[pokemon_stat.stat.identifier] = {} stat_info['value'] = pokemon_stat.base_stat if pokemon_stat.stat.is_base: stat_total += pokemon_stat.base_stat q = db.pokedex_session.query(t.ConquestPokemonStat) \ .filter_by(stat=pokemon_stat.stat) less = q.filter( t.ConquestPokemonStat.base_stat < pokemon_stat.base_stat ).count() equal = q.filter(t.ConquestPokemonStat.base_stat == pokemon_stat.base_stat).count() percentile = (less + equal * 0.5) / total_stat_rows stat_info['percentile'] = percentile # Colors for the stat bars, based on percentile stat_info['background'] = bar_color(percentile, 0.9) stat_info['border'] = bar_color(percentile, 0.8) # Percentile for the total # Need to make a derived table that fakes pokemon_id, total_stats stat_sum_tbl = db.pokedex_session.query( sqla.sql.func.sum(t.ConquestPokemonStat.base_stat) .label('stat_total') ) \ .filter(t.ConquestPokemonStat.conquest_stat_id <= 4) \ .group_by(t.ConquestPokemonStat.pokemon_species_id) \ .subquery() q = db.pokedex_session.query(stat_sum_tbl) less = q.filter(stat_sum_tbl.c.stat_total < stat_total).count() equal = q.filter(stat_sum_tbl.c.stat_total == stat_total).count() percentile = (less + equal * 0.5) / total_stat_rows c.stats['total'] = { 'percentile': percentile, 'value': stat_total, 'background': bar_color(percentile, 0.9), 'border': bar_color(percentile, 0.8), } ### Max links # We only want to show warriors who have a max link above a certain # threshold, because there are 200 warriors and most of them won't # have very good links. default_link = 70 c.link_form = LinkThresholdForm(request.params, link=default_link) if request.params and c.link_form.validate(): link_threshold = c.link_form.link.data else: link_threshold = default_link # However, some warriors will only be above this threshold at later # ranks. In these cases, we may as well show all ranks' links. # No link ever goes down when a warrior ranks up, so we just need to # check their final rank. # First, craft a clause to filter out non-final warrior ranks. ranks_sub = sqla.orm.aliased(t.ConquestWarriorRank) higher_ranks_exist = (sqla.sql.exists([1]).where( sqla.and_(ranks_sub.warrior_id == t.ConquestWarriorRank.warrior_id, ranks_sub.rank > t.ConquestWarriorRank.rank))) # Next, find final-rank warriors with a max link high enough. worthy_warriors = (db.pokedex_session.query(t.ConquestWarrior.id).join( t.ConquestWarriorRank).filter(~higher_ranks_exist).join( t.ConquestMaxLink).filter( t.ConquestMaxLink.pokemon_species_id == c.pokemon.id ).filter(t.ConquestMaxLink.max_link >= link_threshold)) # For Froslass and Gallade, we want to filter out male and female # warriors, respectively. # XXX Eventually we want to figure out all impossible evolutions, and # show them, but sort them to the bottom and grey them out. if (c.pokemon.conquest_evolution is not None and c.pokemon.conquest_evolution.warrior_gender_id is not None): worthy_warriors = worthy_warriors.filter( t.ConquestWarrior.gender_id == c.pokemon.conquest_evolution.warrior_gender_id) # Finally, find ALL the max links for these warriors! links_q = (c.pokemon.conquest_max_links.join(ranks_sub).filter( ranks_sub.warrior_id.in_(worthy_warriors)).options( sqla.orm.joinedload('warrior_rank'), sqla.orm.subqueryload('warrior_rank.stats'), sqla.orm.joinedload('warrior_rank.warrior'), sqla.orm.joinedload('warrior_rank.warrior.archetype'), sqla.orm.subqueryload('warrior_rank.warrior.types'), )) c.max_links = links_q.all() return render('/pokedex/conquest/pokemon.mako')