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')
Exemple #2
0
    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')
Exemple #3
0
    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')
Exemple #4
0
    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,
        )
Exemple #5
0
    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')
Exemple #7
0
    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')
Exemple #8
0
    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')
Exemple #9
0
    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 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')
Exemple #11
0
    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')
Exemple #12
0
    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')
Exemple #14
0
    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')
Exemple #15
0
    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')
Exemple #18
0
    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')
Exemple #20
0
    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')
Exemple #21
0
    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')
Exemple #22
0
    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')
Exemple #24
0
    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')
Exemple #25
0
    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')
Exemple #26
0
    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')
Exemple #27
0
    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')
Exemple #28
0
    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')
Exemple #30
0
    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')
Exemple #31
0
    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')
Exemple #32
0
    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')
Exemple #33
0
    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,
        )
Exemple #34
0
    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 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')
Exemple #37
0
    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')
Exemple #38
0
    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')
Exemple #39
0
    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,
        )
Exemple #40
0
    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')
Exemple #41
0
    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')
Exemple #42
0
    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,
        )
Exemple #43
0
    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,
        )
Exemple #44
0
    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')
Exemple #45
0
    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')
Exemple #46
0
    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')
Exemple #50
0
    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')
Exemple #56
0
    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')
Exemple #58
0
    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')