Initial cut as a Python package
This commit is contained in:
commit
35cfb1efa7
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 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