0
\$\begingroup\$

I have currently problem with discord bot, what is in Python programming language. This commands loading take so long, and I need help with optimization of this code.

from fractions import Fraction
import discord
from discord.ext import commands
import asyncio
import asyncpg
import datetime
from aiohttp import web
import random
import json
import math

class Info(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    with open('./poke/resources/pokemon_list.json') as pokemon_list_file:
        global pokemon_list
        pokemon_list = json.load(pokemon_list_file)

    with open('./poke/resources/natures.json') as natures_file:
        global natures
        natures1 = json.load(natures_file)

    global pokes
    pokes = {}
    for pokesm in pokemon_list['pokemons']:
        name = pokesm['name']
        pokes[name] = {}
        pokes[name]['image_url'] = pokesm['image']
        pokes[name]['hp'] = pokesm['hp']
        pokes[name]['attack'] = pokesm['attack']
        pokes[name]['defense'] = pokesm['defense']
        pokes[name]['special_attack'] = pokesm['special_attack']
        pokes[name]['special_defense'] = pokesm['special_defense']
        pokes[name]['speed'] = pokesm['speed']

    global natures
    natures = {}
    for naturesm in natures1['natures']:
        nature = naturesm['name']
        natures[nature] = {}
        natures[nature]['boost'] = naturesm['boost']
        natures[nature]['remove'] = naturesm['remove']



    @commands.command(name="info", aliases=['i'])
    async def info(self, ctx, poke):
        con = await self.bot.pool.acquire()
        fetch_id = await con.fetchval("SELECT pokes[{0}] FROM users WHERE u_id = {1}".format(poke, ctx.message.author.id))
        name = await con.fetchval("SELECT name FROM pokes WHERE id = {0}".format(fetch_id))
        hpiv = await con.fetchval("SELECT hpiv FROM pokes WHERE id = {0}".format(fetch_id))
        atkiv = await con.fetchval("SELECT atkiv FROM pokes WHERE id = {0}".format(fetch_id))
        defiv = await con.fetchval("SELECT defiv FROM pokes WHERE id = {0}".format(fetch_id))
        spatkiv = await con.fetchval("SELECT spatkiv FROM pokes WHERE id = {0}".format(fetch_id))
        spdefiv = await con.fetchval("SELECT spdefiv FROM pokes WHERE id = {0}".format(fetch_id))
        speediv = await con.fetchval("SELECT speediv FROM pokes WHERE id = {0}".format(fetch_id))
        hpev = await con.fetchval("SELECT hpev FROM pokes WHERE id = {0}".format(fetch_id))
        atkev = await con.fetchval("SELECT atkev FROM pokes WHERE id = {0}".format(fetch_id))
        defev = await con.fetchval("SELECT defev FROM pokes WHERE id = {0}".format(fetch_id))
        spatkev = await con.fetchval("SELECT spatkev FROM pokes WHERE id = {0}".format(fetch_id))
        spdefev = await con.fetchval("SELECT spdefev FROM pokes WHERE id = {0}".format(fetch_id))
        speedev = await con.fetchval("SELECT speedev FROM pokes WHERE id = {0}".format(fetch_id))
        lvl = await con.fetchval("SELECT lvl FROM pokes WHERE id = {0}".format(fetch_id))
        exp = await con.fetchval("SELECT exp FROM pokes WHERE id = {0}".format(fetch_id))
        nature = await con.fetchval("SELECT nature FROM pokes WHERE id = {0}".format(fetch_id))
        ability = await con.fetchval("SELECT ability FROM pokes WHERE id = {0}".format(fetch_id))

        hp_base = pokes[name]['hp']
        atk_base = pokes[name]['attack']
        def_base = pokes[name]['defense']
        spatk_base = pokes[name]['special_attack']
        spdef_base = pokes[name]['special_defense']
        speed_base = pokes[name]['speed']

        hp = math.floor((int((2 * hp_base + hpiv + int(hpev / 4)) * lvl) / 100) + lvl + 10)
        self.attack = int((int((2 * atk_base + atkiv + int(atkev / 4)) * lvl) / 100) + 5)
        self.defense = int((int((2 * def_base + defiv + int(defev / 4)) * lvl) / 100) + 5)
        self.special_attack = int((int((2 * spatk_base + spatkiv + int(spatkev / 4)) * lvl) / 100) + 5)
        self.special_defense = int((int((2 * spdef_base + spdefiv + int(spdefev / 4)) * lvl) / 100) + 5)
        self.speed = int((int((2 * speed_base + speediv + int(speedev / 4)) * lvl) / 100) + 5)

        total = int(hpiv) + int(atkiv) + int(defiv) + int(spatkiv) + int(spdefiv) + int(speediv)

        iv_percentage = 100 * (total/186)

        if natures[nature]['boost'] == "Attack":
            attack_initial = self.attack
            self.attack = int(attack_initial) * 1.1
        elif natures[nature]['boost'] == "Defense":
            defense_initial = self.defense
            self.defense = int(defense_initial) * 1.1
        elif natures[nature]['boost'] == "Special Attack":
            special_attack_initial = self.special_attack
            self.special_attack = int(special_attack_initial) * 1.1
        elif natures[nature]['boost'] == "Special Defense":
            special_defense_initial = self.special_defense
            self.special_defense = int(special_defense_initial) * 1.1
        elif natures[nature]['boost'] == "Speed":
            speed_initial = self.speed
            self.speed = int(speed_initial) * 1.1

        if natures[nature]['remove'] == "Attack":
            attack_initial = self.attack
            self.attack = int(attack_initial) * 0.9
        elif natures[nature]['remove'] == "Defense":
            defense_initial = self.defense
            self.defense = int(defense_initial) * 0.9
        elif natures[nature]['remove'] == "Special Attack":
            special_attack_initial = self.special_attack
            self.special_attack = int(special_attack_initial) * 0.9
        elif natures[nature]['remove'] == "Special Defense":
            special_defense_initial = self.special_defense
            self.special_defense = int(special_defense_initial) * 0.9
        elif natures[nature]['remove'] == "Speed":
            speed_initial = self.speed
            self.speed = int(speed_initial) * 0.9

        embed = discord.Embed(title="Level {0} {1}".format(lvl, name), description="""
**Ability**: {0}
**Exp**: {1}
**Nature**: {2}

**HP**: {3} | {4} IVs | {5} EVs
**Attack**: {6} | {7} IVs | {8} EVs
**Defense**: {9} | {10} IVs | {11} EVs
**Special Attack**: {12} | {13} IVs | {14} EVs
**Special Defense**: {15} | {16} IVs | {17} EVs
**Speed**: {18} | {19} IVs | {20} EVs
**IV %**: {21}""".format(ability, exp, nature, hp, hpiv, hpev, int(self.attack), atkiv, atkev, int(self.defense), defiv, defev, int(self.special_attack), spatkiv, spatkev, int(self.special_defense), spdefiv, spdefev, int(self.speed), speediv, speedev, round(iv_percentage, 2)))
        embed.set_image(url=pokes[name]['image_url'])
        await ctx.send(embed=embed)
        await self.bot.pool.release(con)

def setup(bot):
    bot.add_cog(Info(bot))

Just I can't release beta, when this responses take 20 sec sometimes. I want this respond in max 2 sec.

\$\endgroup\$
  • \$\begingroup\$ Just a hint: Why are you doing almost 20 separate database queries when it could be done with one? \$\endgroup\$ – RoToRa Sep 30 '19 at 7:48
3
\$\begingroup\$

Almost certainly the largest slow-down is that you are getting each individual value on its own. Instead get all information at once:

poke_query = """
SELECT
hpiv, atkiv, defiv, spatkiv, spdefiv, speediv, 
hpev, atkev, defev, spatkev, spdefev, speedev,
lvl, exp, nature
FROM pokes
WHERE id = {}"""

poke_info = await con.execute(poke_query.format(fetch_id)).fetchone()

Note that the parameters you pass to str.format are put into the placeholders in the order you pass them, so e.g. "{0} {1} {2}".format(1, 2, 3) is equivalent to "{} {} {}".format(1, 2, 3).

You should be able to access the individual columns with their names (e.g. poke_info.hpiv) or like a tuple (e.g. poke_info[0]).

In addition, most SQL packages allow using parameters. This prevents SQL injections, which you should be vary of, because the user of the bot controls the value of poke. I think in your case it would be as easy as

poke_query = """
SELECT
hpiv, atkiv, defiv, spatkiv, spdefiv,
speediv, hpev, atkev, defev, speedev,
lvl, exp, nature
FROM pokes
WHERE id = ?"""

poke_info = await con.execute(poke_query, fetch_id).fetchone()

For the poke ID it is similarly:

fetch_id = await con.fetchval("SELECT pokes[?] FROM users WHERE u_id = ?",
                              poke, ctx.message.author.id)

These are assuming you are using pyodbc, but there are equivalent ways to achieve the same thing in other packages.

For example with asyncpg it would be $1 instead of ?:

poke_query = """
SELECT
hpiv, atkiv, defiv, spatkiv, spdefiv,
speediv, hpev, atkev, defev, speedev,
lvl, exp, nature
FROM pokes
WHERE id = $1"""

poke_info = await con.fetchrow(poke_query, fetch_id)

Where each value can be accessed as poke_info['hpiv'] or poke_inf[0].


There is no need for pokes and natures to be global values. The way you currently define them they are class variables and you can access them both with Info.pokes and self.pokes. Beware that these are initialized at class definition time. So if you change the config files you need to restart the script/bot/interpreter. If you put that code into the constructor __init__ it would be read everytime a new instance of Info is created and pokes becomes an attribute so can only be accessed via self.pokes.

You can also use dictionary expressions to make this a bit easier.

class Info(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

        with open('./poke/resources/pokemon_list.json') as pokemon_list_file:
            pokemon_list = json.load(pokemon_list_file)
            self.pokes = {p['name']: {'image_url': p['image'],
                                      'hp': p['hp'],
                                      'attack': p['attack'],
                                      'defense': p['defense'],
                                      'special_attack': p['special_attack'],
                                      'special_defense': p['special_defense'],
                                      'speed': p['speed']}
                          for p in json.load(pokemon_list_file)}

        with open('./poke/resources/natures.json') as natures_file:
            self.natures = {n['name']: {'boost': n['boost'],
                                        'remove': n['remove']}
                            for n in json.load(natures_file)}

Things get even easier if you don't care that the inner dictionaries also contain the name (and any additional keys) and that the name of the image url changes:

class Info(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

        with open('./poke/resources/pokemon_list.json') as pokemon_list_file:
            pokemon_list = json.load(pokemon_list_file)
            self.pokes = {p['name']: p for p in json.load(pokemon_list_file)}

        with open('./poke/resources/natures.json') as natures_file:
            self.natures = {n['name']: n for n in json.load(natures_file)}

I can't say I completely understand all the calculations of the actual stats from the IVs, EVs and base stats. But try to save each stat as its natural type. If a thing is an int, save it as an int from the beginning and don't continuously cast it to an int. This will not save a lot of runtime but make it a lot easier to read.

Another thing you could to for this is put the calculations into their own functions, which you can give a name and docstring explaining what the function does. It also makes it immediately clear that the actual calculation is the same in all cases.

def calc(base, iv, ev, lvl):
    """Calculate the actual stat from the base value, IV, EV and current level."""
    return int((int((2 * base + iv + int(ev / 4)) * lvl) / 100) + 5)

self.attack = calc(atk_base, atk_iv, atk_ev, lvl)
self.defense = calc(def_base, def_iv, def_ev, lvl)
...

For the boosts and removes, you can shorten it a bit using a mapping from the name to the internal name and in-place operations:

mapping = {"Attack": "attack", "Defense": "defense",
           "Special Attack": "special_attack", "Special Defense": "special_defense"}

nature = self.natures[nature]
self.__dict__[mapping[nature['boost']]] *= 1.1
self.__dict__[mapping[nature['remove']]] *= 0.9

Instead of this dict you can also use the fact that there is an easy way to convert from one to the other:

def nature_to_attr(name):
    return name.lower().replace(" ", "_")

nature = self.natures[nature]
self.__dict__[nature_to_attr(nature['boost'])] *= 1.1
self.__dict__[nature_to_attr(nature['remove'])] *= 0.9
\$\endgroup\$
  • \$\begingroup\$ One note: Asyncpg doesn't have fetchone feature, and when you use fetch, you get result like this: [<Record ability=None>, <Record ability=None>] \$\endgroup\$ – ks1001 Sep 29 '19 at 14:23
  • \$\begingroup\$ @ks1001 It does have fetchrow, though, which is what I used in the answer. \$\endgroup\$ – Graipher Sep 29 '19 at 14:25
  • \$\begingroup\$ I am not really familiar with asyncpg, so you might have to call it slightly differently (obviously I could not test the code). Maybe it is actually con.cursor(poke_query, fetch_id).fetchrow() or something similar. \$\endgroup\$ – Graipher Sep 29 '19 at 14:29
  • \$\begingroup\$ I'll try to move psycopg2, but only when I can get pool over the Cogs (different files) \$\endgroup\$ – ks1001 Sep 29 '19 at 14:34

Not the answer you're looking for? Browse other questions tagged or ask your own question.