Initial cut as a Python package
This commit is contained in:
commit
f870361242
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
dist/
|
||||
*.db
|
||||
*.json
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.12
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Zoé Cassiopée Gauthier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
29
README.md
Normal file
29
README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Pokémon GO Average Scaled Estimators
|
||||
|
||||
## Installation
|
||||
|
||||
Once downloaded, this package can be installed locally in development mode:
|
||||
|
||||
```console
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To run the simulations, choose your parameters such as attacker type, attacker level, and wether Party Power is active.
|
||||
Try to fill the stored results database with any of the following:
|
||||
|
||||
```console
|
||||
ase-cli --refresh POKEMON_TYPE_GRASS # Grass attackers. Defaults to level 40 and no Party Power.
|
||||
ase-cli --refresh --level 30 POKEMON_TYPE_GRASS # Level 30 Grass attackers, no Party Power.
|
||||
ase-cli --refresh --party 2 POKEMON_TYPE_GRASS # Level 40 Grass attackers, Party Power with two trainers.
|
||||
ase-cli --refresh POKEMON_TYPE_DARK POKEMON_TYPE_GHOST # Combined Dark and Ghost attackers.
|
||||
```
|
||||
|
||||
Once the database contains the desired simulation results, display the scaled estimators for attackers of thec given
|
||||
type:
|
||||
|
||||
```console
|
||||
ase-cli POKEMON_TYPE_GRASS
|
||||
ase-cli --level 30 POKEMON_TYPE_GRASS
|
||||
```
|
101
pyproject.toml
Normal file
101
pyproject.toml
Normal file
@ -0,0 +1,101 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "pogo-scaled-estimators"
|
||||
version = "1.0a1"
|
||||
dependencies = [
|
||||
"requests",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
authors = [
|
||||
{ name = "Zoé Cassiopée Gauthier", email = "zoe.gauthier@blorp.dev" },
|
||||
]
|
||||
description = "Calculates scaled difficulty estimators from Pokebattler simulations."
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
keywords = []
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Source" = "https://git.blorp.dev/zo/pogo-scaled-estimators"
|
||||
|
||||
[project.scripts]
|
||||
ase-cli = "pogo_scaled_estimators:main_cli"
|
||||
|
||||
[tool.hatch.envs.lint]
|
||||
detached = true
|
||||
dependencies = [
|
||||
"pyright",
|
||||
"ruff",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.lint.scripts]
|
||||
all = ["style", "typing"]
|
||||
format = ["ruff format --fix {args:.}"]
|
||||
style = ["ruff check {args:.}"]
|
||||
typing = ["pyright"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
# Note: these are ordered to match https://beta.ruff.rs/docs/rules/
|
||||
"F", # PyFlakes
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
"ASYNC", # flake8-async
|
||||
"BLE", # flake8-blind-except
|
||||
"FBT", # flake8-boolean-trap
|
||||
"B", # flake8-bugbear
|
||||
"A", # flake8-builtins
|
||||
"COM", # flake8-commas
|
||||
"C4", # flake8-comprehensions
|
||||
"DTZ", # flake8-datetimez
|
||||
"T10", # flake8-debugger
|
||||
"EM", # flake8-errmsg
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
"PIE", # flake8-pie
|
||||
"PT", # flake8-pytest-style
|
||||
"Q", # flake8-quotes
|
||||
"RET", # flake8-return
|
||||
"SLOT", # flake8-slots
|
||||
"SIM", # flake8-simplify
|
||||
"TID", # flake8-tidy-imports
|
||||
"PTH", # flake8-pathlib
|
||||
"TD", # flake8-todo
|
||||
"PL", # PyLint
|
||||
"TRY", # tryceratops
|
||||
"NPY", # NumPy
|
||||
"RUF", # Ruff
|
||||
]
|
||||
ignore = [
|
||||
#----- Rules recommended by https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
||||
"W191", # tab-identation
|
||||
"E111", # indentation-with-invalid-multiple
|
||||
"E114", # indentation-with-invalid-multiple-comment
|
||||
"E117", # over-indented
|
||||
"E501", # line-too-long
|
||||
"D206", # indent-with-spaces
|
||||
"D300", # triple-single-quotes
|
||||
"Q000", # bad-quotes-inline-string
|
||||
"Q001", # bad-quotes-multiline-string
|
||||
"Q002", # bad-quotes-docstring
|
||||
"Q003", # avoidable-escaped-quote
|
||||
"COM812", # missing-trailing-comma
|
||||
"COM819", # prohibited-trailing-comma
|
||||
"ISC001", # single-line-implicit-string-concatenation
|
||||
"ISC002", # multi-line-implicit-string-concatenation
|
||||
]
|
13
src/pogo_scaled_estimators/__init__.py
Normal file
13
src/pogo_scaled_estimators/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2024 Zoé Cassiopée Gauthier.
|
||||
#
|
||||
# Use of this source code is governed by an MIT-style
|
||||
# license that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
from pogo_scaled_estimators.calculator import Calculator
|
||||
from pogo_scaled_estimators.cli import main_cli
|
||||
|
||||
__all__ = [
|
||||
"main_cli",
|
||||
"Calculator",
|
||||
]
|
231
src/pogo_scaled_estimators/calculator.py
Normal file
231
src/pogo_scaled_estimators/calculator.py
Normal file
@ -0,0 +1,231 @@
|
||||
# Copyright 2024 Zoé Cassiopée Gauthier.
|
||||
#
|
||||
# Use of this source code is governed by an MIT-style
|
||||
# license that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import json
|
||||
import operator
|
||||
import sqlite3
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
WEAKNESS = 1.6
|
||||
DOUBLE_WEAKNESS = WEAKNESS * WEAKNESS
|
||||
|
||||
|
||||
class Calculator:
|
||||
def __init__(self, attacker_types):
|
||||
self.db = sqlite3.connect("ase.db")
|
||||
# db.set_trace_callback(print)
|
||||
|
||||
with contextlib.suppress(sqlite3.OperationalError):
|
||||
self.db.execute(
|
||||
"CREATE TABLE estimators(defender, raid_tier, attacker, level, quick_move, charged_move, estimator, party)"
|
||||
)
|
||||
|
||||
self.raids = self._pokebattler_resource("raids")
|
||||
self.pokemon = self._pokebattler_resource("pokemon")
|
||||
self.resists = self._pokebattler_resource("resists")
|
||||
self.moves = self._pokebattler_resource("moves")
|
||||
|
||||
self.attacker_types = attacker_types
|
||||
|
||||
def _pokebattler_resource(self, name):
|
||||
p = Path(f"./{name}.json")
|
||||
if not p.exists():
|
||||
response = requests.get(f"https://fight.pokebattler.com/{name}")
|
||||
with p.open(mode="wb") as fp:
|
||||
fp.write(response.content)
|
||||
|
||||
with p.open() as fp:
|
||||
return json.load(fp)
|
||||
|
||||
def calculate(self, level=40, party=1):
|
||||
raid_bosses = self._raid_bosses()
|
||||
charged_moves = [
|
||||
move["moveId"] for move in self.moves["move"] if "type" in move and move["type"] in self.attacker_types
|
||||
]
|
||||
res = self.db.execute(
|
||||
f"""SELECT DISTINCT(attacker) FROM estimators WHERE charged_move IN ("{'","'.join(charged_moves)}")"""
|
||||
)
|
||||
attackers = {row[0]: {"RAID_LEVEL_3": [], "RAID_LEVEL_5": [], "RAID_LEVEL_MEGA": []} for row in res.fetchall()}
|
||||
|
||||
defenders = functools.reduce(operator.iconcat, raid_bosses.values(), [])
|
||||
res = self.db.execute(
|
||||
f"""
|
||||
SELECT e.raid_tier, e.defender, e.attacker, MIN(e.estimator) / m.min_estimator
|
||||
FROM estimators e
|
||||
INNER JOIN (
|
||||
SELECT defender, MIN(estimator) AS min_estimator
|
||||
FROM estimators
|
||||
WHERE party = ? AND level = 40
|
||||
AND attacker IN ("{'","'.join(attackers.keys())}")
|
||||
AND defender IN ("{'","'.join(defenders)}")
|
||||
GROUP BY defender
|
||||
) AS m ON e.defender = m.defender
|
||||
WHERE party = ? AND level = ? AND attacker IN ("{'","'.join(attackers.keys())}")
|
||||
GROUP BY e.defender, e.attacker
|
||||
""",
|
||||
(party, party, level),
|
||||
)
|
||||
for raid_tier, _defender, attacker, estimator in res.fetchall():
|
||||
if raid_tier == "RAID_LEVEL_MEGA_5":
|
||||
simplified_raid_tier = "RAID_LEVEL_MEGA"
|
||||
elif raid_tier == "RAID_LEVEL_ULTRA_BEAST":
|
||||
simplified_raid_tier = "RAID_LEVEL_5"
|
||||
else:
|
||||
simplified_raid_tier = raid_tier
|
||||
attackers[attacker][simplified_raid_tier].append(estimator)
|
||||
|
||||
ase = {}
|
||||
for attacker, estimators in attackers.items():
|
||||
if not estimators["RAID_LEVEL_3"] or not estimators["RAID_LEVEL_5"] or not estimators["RAID_LEVEL_MEGA"]:
|
||||
continue
|
||||
ase = (
|
||||
0.15 * sum(estimators["RAID_LEVEL_3"]) / len(estimators["RAID_LEVEL_3"])
|
||||
+ 0.50 * sum(estimators["RAID_LEVEL_5"]) / len(estimators["RAID_LEVEL_5"])
|
||||
+ 0.35 * sum(estimators["RAID_LEVEL_MEGA"]) / len(estimators["RAID_LEVEL_MEGA"])
|
||||
)
|
||||
print(f"{attacker},{ase}")
|
||||
|
||||
def _raid_bosses(self):
|
||||
raid_tiers = []
|
||||
raid_bosses = {}
|
||||
|
||||
for raid_level in ["3", "5", "MEGA", "MEGA_5", "ULTRA_BEAST"]:
|
||||
tier = f"RAID_LEVEL_{raid_level}"
|
||||
raid_tiers.extend(
|
||||
[
|
||||
tier,
|
||||
f"{tier}_LEGACY",
|
||||
f"{tier}_FUTURE",
|
||||
]
|
||||
)
|
||||
raid_bosses[tier] = []
|
||||
|
||||
for tier in filter(lambda tier: tier["tier"] in raid_tiers, self.raids["tiers"]):
|
||||
for boss in (raid["pokemon"] for raid in tier["raids"]):
|
||||
if boss.endswith("_FORM"):
|
||||
continue
|
||||
boss_pokemon = next(filter(lambda mon: mon["pokemonId"] == boss, self.pokemon["pokemon"]))
|
||||
if ("candyToEvolve" in boss_pokemon or boss in ["SEADRA", "SEALEO"]) and boss not in [
|
||||
"KELDEO",
|
||||
"LUMINEON",
|
||||
"MANAPHY",
|
||||
"PHIONE",
|
||||
"STUNFISK",
|
||||
"TERRAKION",
|
||||
]:
|
||||
continue
|
||||
boss_types = (
|
||||
boss_pokemon["type"],
|
||||
boss_pokemon.get("type2", "POKEMON_TYPE_NONE"),
|
||||
)
|
||||
if any(self._is_weak(attacker_type, boss_types) for attacker_type in self.attacker_types):
|
||||
raid_bosses[tier["info"]["guessTier"]].append(boss)
|
||||
|
||||
return raid_bosses
|
||||
|
||||
def _is_weak(self, attacker_type, defender_types):
|
||||
pokemon_types = list(self.resists.keys())
|
||||
defender_type_indices = (
|
||||
pokemon_types.index(defender_types[0]),
|
||||
pokemon_types.index(defender_types[1]),
|
||||
)
|
||||
attack_resist = (
|
||||
self.resists[attacker_type][defender_type_indices[0]]
|
||||
* self.resists[attacker_type][defender_type_indices[1]]
|
||||
)
|
||||
|
||||
# Check for double weakness.
|
||||
if attack_resist >= DOUBLE_WEAKNESS:
|
||||
return True
|
||||
|
||||
# Checkout for single weakness if not double weak to anything else.
|
||||
any_double_weaknesses = any(
|
||||
r[defender_type_indices[0]] * r[defender_type_indices[1]] >= DOUBLE_WEAKNESS for r in self.resists.values()
|
||||
)
|
||||
if not any_double_weaknesses and attack_resist >= WEAKNESS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def load_estimators(self, tier, defender, level, party):
|
||||
base_url = "https://fight.pokebattler.com"
|
||||
query_string = {
|
||||
"sort": "ESTIMATOR",
|
||||
"weatherCondition": "NO_WEATHER",
|
||||
"dodgeStrategy": "DODGE_REACTION_TIME",
|
||||
"aggregation": "AVERAGE",
|
||||
"includeLegendary": "true",
|
||||
"includeShadow": "true",
|
||||
"includeMegas": "true",
|
||||
"attackerTypes": self.attacker_types,
|
||||
"primalAssistants": "",
|
||||
"numParty": str(party),
|
||||
}
|
||||
if tier != "RAID_LEVEL_3":
|
||||
query_string["friendshipLevel"] = "FRIENDSHIP_LEVEL_4"
|
||||
url = f"{base_url}/raids/defenders/{defender}/levels/{tier}/attackers/levels/{level}/strategies/CINEMATIC_ATTACK_WHEN_POSSIBLE/DEFENSE_RANDOM_MC?{urllib.parse.urlencode(query_string, doseq=True)}"
|
||||
response = requests.get(url)
|
||||
for attacker in response.json()["attackers"][0]["randomMove"]["defenders"]:
|
||||
for attacker_moves in attacker["byMove"]:
|
||||
res = self.db.execute(
|
||||
"SELECT estimator FROM estimators WHERE defender=? AND attacker=? AND level=? AND quick_move=? AND charged_move=? AND party=?",
|
||||
(
|
||||
defender,
|
||||
attacker["pokemonId"],
|
||||
level,
|
||||
attacker_moves["move1"],
|
||||
attacker_moves["move2"],
|
||||
party,
|
||||
),
|
||||
)
|
||||
estimator = res.fetchone()
|
||||
if estimator is not None:
|
||||
print(
|
||||
defender,
|
||||
attacker["pokemonId"],
|
||||
attacker_moves["move1"],
|
||||
attacker_moves["move2"],
|
||||
estimator[0],
|
||||
)
|
||||
continue
|
||||
|
||||
self.db.execute(
|
||||
"INSERT INTO estimators(defender, raid_tier, attacker, level, quick_move, charged_move, estimator, party) VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
defender,
|
||||
tier,
|
||||
attacker["pokemonId"],
|
||||
level,
|
||||
attacker_moves["move1"],
|
||||
attacker_moves["move2"],
|
||||
attacker_moves["result"]["estimator"],
|
||||
party,
|
||||
),
|
||||
)
|
||||
print(
|
||||
defender,
|
||||
attacker["pokemonId"],
|
||||
attacker_moves["move1"],
|
||||
attacker_moves["move2"],
|
||||
attacker_moves["result"]["estimator"],
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
def refresh(self, level=40, party=1):
|
||||
for tier, defenders in self._raid_bosses().items():
|
||||
for defender in defenders:
|
||||
try:
|
||||
self.load_estimators(tier, defender, level, party)
|
||||
except json.decoder.JSONDecodeError:
|
||||
time.sleep(30)
|
||||
self.load_estimators(tier, defender, level, party)
|
||||
time.sleep(30)
|
30
src/pogo_scaled_estimators/cli.py
Normal file
30
src/pogo_scaled_estimators/cli.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright 2024 Zoé Cassiopée Gauthier.
|
||||
#
|
||||
# Use of this source code is governed by an MIT-style
|
||||
# license that can be found in the LICENSE file or at
|
||||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import pogo_scaled_estimators.calculator
|
||||
|
||||
|
||||
def main_cli():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("type", nargs="+", help="an attacker type")
|
||||
parser.add_argument("--level", type=int, default=40)
|
||||
parser.add_argument("--party", type=int, default=1)
|
||||
parser.add_argument("--refresh", action="store_true", default=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
calculator = pogo_scaled_estimators.calculator.Calculator(args.type)
|
||||
if args.refresh:
|
||||
calculator.refresh(level=args.level, party=args.party)
|
||||
else:
|
||||
calculator.calculate(level=args.level, party=args.party)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main_cli())
|
Loading…
Reference in New Issue
Block a user