pyvgc 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvgc-0.0.1/LICENSE +21 -0
- pyvgc-0.0.1/PKG-INFO +50 -0
- pyvgc-0.0.1/README.md +32 -0
- pyvgc-0.0.1/pyproject.toml +38 -0
- pyvgc-0.0.1/setup.cfg +4 -0
- pyvgc-0.0.1/src/pyvgc/__init__.py +45 -0
- pyvgc-0.0.1/src/pyvgc/battle_logic.py +135 -0
- pyvgc-0.0.1/src/pyvgc/constants.py +53 -0
- pyvgc-0.0.1/src/pyvgc/damage.py +183 -0
- pyvgc-0.0.1/src/pyvgc/data/__init__.py +1 -0
- pyvgc-0.0.1/src/pyvgc/data/battle_items.csv +167 -0
- pyvgc-0.0.1/src/pyvgc/data/full_pokemon_roster.csv +404 -0
- pyvgc-0.0.1/src/pyvgc/data/initial_pokemon_natures.csv +26 -0
- pyvgc-0.0.1/src/pyvgc/data/initial_type_chart.csv +19 -0
- pyvgc-0.0.1/src/pyvgc/data/list_of_regulations.csv +4 -0
- pyvgc-0.0.1/src/pyvgc/data/move_templates.csv +513 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/items.csv +120 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/learnsets.csv +333 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/moves.csv +491 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/pokemon.csv +333 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/items.csv +149 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/learnsets.csv +369 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/moves.csv +497 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/pokemon.csv +369 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/items.csv +167 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/learnsets.csv +404 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/moves.csv +508 -0
- pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/pokemon.csv +404 -0
- pyvgc-0.0.1/src/pyvgc/items.py +35 -0
- pyvgc-0.0.1/src/pyvgc/moves.py +112 -0
- pyvgc-0.0.1/src/pyvgc/pokemon.py +268 -0
- pyvgc-0.0.1/src/pyvgc/regulations.py +304 -0
- pyvgc-0.0.1/src/pyvgc/utils.py +115 -0
- pyvgc-0.0.1/src/pyvgc.egg-info/PKG-INFO +50 -0
- pyvgc-0.0.1/src/pyvgc.egg-info/SOURCES.txt +36 -0
- pyvgc-0.0.1/src/pyvgc.egg-info/dependency_links.txt +1 -0
- pyvgc-0.0.1/src/pyvgc.egg-info/top_level.txt +1 -0
- pyvgc-0.0.1/tests/test_package.py +610 -0
pyvgc-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2026 Voozde
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
20
|
+
|
|
21
|
+
- This license is currently under review and is a basic template
|
pyvgc-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvgc
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Pokémon Champions data science package.
|
|
5
|
+
Author: Voozde
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# PYVGC
|
|
20
|
+
|
|
21
|
+
PYVGC is a Python package for basic Pokémon Champions-based data science.
|
|
22
|
+
It is currently meant for Python 3.9 and newer, without runtime dependencies.
|
|
23
|
+
|
|
24
|
+
Regulations scope legal Pokémon, moves,items, and learnsets, while named `BattleLogic` instances preserve historical and future rule sets.
|
|
25
|
+
|
|
26
|
+
Both `Cartridge-Champions-Release-2026` and `Simulator-Champions-Release-2026` are available. Regulation snapshots default to Cartridge rules and can be loaded with `platform="Simulator"` for simulator rules.
|
|
27
|
+
|
|
28
|
+
All current development is meant for early implementation. Until 0.1 this package is on alpha development.
|
|
29
|
+
|
|
30
|
+
Version 0.0.1 provides:
|
|
31
|
+
|
|
32
|
+
- Level-50 Pokémon loading from the bundled roster;
|
|
33
|
+
- Stat training, natures, and stat stages;
|
|
34
|
+
- Pokémon battle type and held-item accessors (no in-battle functionality);
|
|
35
|
+
- normal and critical damage rolls;
|
|
36
|
+
- move, learnset, and Pokémon-for-move lookups;
|
|
37
|
+
- Bundled battle-item reference data with effect descriptions crawled from
|
|
38
|
+
[Serebii's ItemDex](https://www.serebii.net/itemdex/).
|
|
39
|
+
|
|
40
|
+
Item effects and regulation item membership are descriptive data only; item
|
|
41
|
+
mechanics are not implemented in damage calculations. Teams, weather, terrain,
|
|
42
|
+
and KO-target searches are not part of this MVP. Regulation snapshots and the
|
|
43
|
+
single-ability query are available as data utilities; they do not affect
|
|
44
|
+
damage calculations.
|
|
45
|
+
|
|
46
|
+
The package is maintained by Voozde.
|
|
47
|
+
|
|
48
|
+
AI DISCLAIMER
|
|
49
|
+
|
|
50
|
+
Current development is assisted by the VSCode Copilot Plugin, which settled on Gpt-5.6-Luna
|
pyvgc-0.0.1/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# PYVGC
|
|
2
|
+
|
|
3
|
+
PYVGC is a Python package for basic Pokémon Champions-based data science.
|
|
4
|
+
It is currently meant for Python 3.9 and newer, without runtime dependencies.
|
|
5
|
+
|
|
6
|
+
Regulations scope legal Pokémon, moves,items, and learnsets, while named `BattleLogic` instances preserve historical and future rule sets.
|
|
7
|
+
|
|
8
|
+
Both `Cartridge-Champions-Release-2026` and `Simulator-Champions-Release-2026` are available. Regulation snapshots default to Cartridge rules and can be loaded with `platform="Simulator"` for simulator rules.
|
|
9
|
+
|
|
10
|
+
All current development is meant for early implementation. Until 0.1 this package is on alpha development.
|
|
11
|
+
|
|
12
|
+
Version 0.0.1 provides:
|
|
13
|
+
|
|
14
|
+
- Level-50 Pokémon loading from the bundled roster;
|
|
15
|
+
- Stat training, natures, and stat stages;
|
|
16
|
+
- Pokémon battle type and held-item accessors (no in-battle functionality);
|
|
17
|
+
- normal and critical damage rolls;
|
|
18
|
+
- move, learnset, and Pokémon-for-move lookups;
|
|
19
|
+
- Bundled battle-item reference data with effect descriptions crawled from
|
|
20
|
+
[Serebii's ItemDex](https://www.serebii.net/itemdex/).
|
|
21
|
+
|
|
22
|
+
Item effects and regulation item membership are descriptive data only; item
|
|
23
|
+
mechanics are not implemented in damage calculations. Teams, weather, terrain,
|
|
24
|
+
and KO-target searches are not part of this MVP. Regulation snapshots and the
|
|
25
|
+
single-ability query are available as data utilities; they do not affect
|
|
26
|
+
damage calculations.
|
|
27
|
+
|
|
28
|
+
The package is maintained by Voozde.
|
|
29
|
+
|
|
30
|
+
AI DISCLAIMER
|
|
31
|
+
|
|
32
|
+
Current development is assisted by the VSCode Copilot Plugin, which settled on Gpt-5.6-Luna
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyvgc"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A Pokémon Champions data science package."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Voozde"},
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Topic :: Scientific/Engineering",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
pyvgc = [
|
|
35
|
+
"data/*.csv",
|
|
36
|
+
"data/regulations/*/*.csv",
|
|
37
|
+
]
|
|
38
|
+
|
pyvgc-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Public API for the pyvgc package."""
|
|
2
|
+
|
|
3
|
+
from .battle_logic import (
|
|
4
|
+
BattleLogic,
|
|
5
|
+
CARTRIDGE_CHAMPIONS_RELEASE_2026,
|
|
6
|
+
SIMULATOR_CHAMPIONS_RELEASE_2026,
|
|
7
|
+
get_battle_logic,
|
|
8
|
+
)
|
|
9
|
+
from .damage import calculate_damage, damage_rolls
|
|
10
|
+
from .items import Item, get_item
|
|
11
|
+
from .moves import (
|
|
12
|
+
Move,
|
|
13
|
+
get_move,
|
|
14
|
+
moves_for_pokemon,
|
|
15
|
+
pokemon_for_move,
|
|
16
|
+
)
|
|
17
|
+
from .pokemon import Pokemon
|
|
18
|
+
from .regulations import load_regulation
|
|
19
|
+
from .utils import (
|
|
20
|
+
pokemon_with_ability,
|
|
21
|
+
pokemon_with_single_ability,
|
|
22
|
+
regulation_description,
|
|
23
|
+
signature_moves,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BattleLogic",
|
|
28
|
+
"CARTRIDGE_CHAMPIONS_RELEASE_2026",
|
|
29
|
+
"SIMULATOR_CHAMPIONS_RELEASE_2026",
|
|
30
|
+
"calculate_damage",
|
|
31
|
+
"damage_rolls",
|
|
32
|
+
"get_battle_logic",
|
|
33
|
+
"get_item",
|
|
34
|
+
"get_move",
|
|
35
|
+
"Item",
|
|
36
|
+
"Move",
|
|
37
|
+
"moves_for_pokemon",
|
|
38
|
+
"Pokemon",
|
|
39
|
+
"pokemon_with_ability",
|
|
40
|
+
"pokemon_with_single_ability",
|
|
41
|
+
"pokemon_for_move",
|
|
42
|
+
"regulation_description",
|
|
43
|
+
"load_regulation",
|
|
44
|
+
"signature_moves",
|
|
45
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Named battle-rule configurations used by regulations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
from fractions import Fraction
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import TYPE_CHECKING, Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from .constants import (
|
|
13
|
+
BASE_HP_INCREASE,
|
|
14
|
+
BASE_STAT_INCREASE,
|
|
15
|
+
CRITICAL_MULTIPLIER,
|
|
16
|
+
DAMAGE_DIVISOR,
|
|
17
|
+
DAMAGE_LEVEL_DIVISOR,
|
|
18
|
+
LEVEL_DOUBLES,
|
|
19
|
+
MAX_TOTAL_TRAINING,
|
|
20
|
+
MAX_TRAINING_PER_STAT,
|
|
21
|
+
MAX_STAT_STAGE,
|
|
22
|
+
MIN_STAT_STAGE,
|
|
23
|
+
RANDOM_PERCENTAGES,
|
|
24
|
+
STAB_MULTIPLIER,
|
|
25
|
+
STAT_STAGE_MULTIPLIERS,
|
|
26
|
+
TYPES,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .damage import DamageRolls
|
|
31
|
+
from .pokemon import Pokemon
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class BattleLogic:
|
|
36
|
+
"""Immutable rules and constants for one game-logic version."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
level: int = LEVEL_DOUBLES
|
|
40
|
+
base_hp_increase: int = BASE_HP_INCREASE
|
|
41
|
+
base_stat_increase: int = BASE_STAT_INCREASE
|
|
42
|
+
max_training_per_stat: int = MAX_TRAINING_PER_STAT
|
|
43
|
+
max_total_training: int = MAX_TOTAL_TRAINING
|
|
44
|
+
min_stat_stage: int = MIN_STAT_STAGE
|
|
45
|
+
max_stat_stage: int = MAX_STAT_STAGE
|
|
46
|
+
stat_stage_multipliers: Mapping[int, tuple[int, int]] = field(
|
|
47
|
+
default_factory=lambda: MappingProxyType(dict(STAT_STAGE_MULTIPLIERS))
|
|
48
|
+
)
|
|
49
|
+
damage_level_divisor: int = DAMAGE_LEVEL_DIVISOR
|
|
50
|
+
damage_divisor: int = DAMAGE_DIVISOR
|
|
51
|
+
random_percentages: range = RANDOM_PERCENTAGES
|
|
52
|
+
stab_multiplier: Fraction = STAB_MULTIPLIER
|
|
53
|
+
critical_multiplier: Fraction = CRITICAL_MULTIPLIER
|
|
54
|
+
type_chart: Optional[Mapping[str, Mapping[str, Fraction]]] = None
|
|
55
|
+
|
|
56
|
+
def with_type_chart(
|
|
57
|
+
self, type_chart: Mapping[str, Mapping[str, Fraction]]
|
|
58
|
+
) -> "BattleLogic":
|
|
59
|
+
"""Return this logic with an immutable type chart."""
|
|
60
|
+
immutable_chart = MappingProxyType({
|
|
61
|
+
attack: MappingProxyType(dict(defending))
|
|
62
|
+
for attack, defending in type_chart.items()
|
|
63
|
+
})
|
|
64
|
+
return replace(self, type_chart=immutable_chart)
|
|
65
|
+
|
|
66
|
+
def calculate_damage(
|
|
67
|
+
self,
|
|
68
|
+
attacker: "Pokemon",
|
|
69
|
+
defender: "Pokemon",
|
|
70
|
+
power: int,
|
|
71
|
+
move_type: str,
|
|
72
|
+
*,
|
|
73
|
+
special: bool = False,
|
|
74
|
+
percentage: bool = True,
|
|
75
|
+
) -> "DamageRolls":
|
|
76
|
+
"""Calculate damage using this battle logic and two Pokémon.
|
|
77
|
+
|
|
78
|
+
Both Pokémon must use this exact logic instance. This makes the
|
|
79
|
+
selected rule set explicit at the calculation call site.
|
|
80
|
+
"""
|
|
81
|
+
from .damage import calculate_damage
|
|
82
|
+
|
|
83
|
+
if attacker.battle_logic is not self or defender.battle_logic is not self:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"attacker and defender must use this battle logic"
|
|
86
|
+
)
|
|
87
|
+
return calculate_damage(
|
|
88
|
+
attacker,
|
|
89
|
+
defender,
|
|
90
|
+
power,
|
|
91
|
+
move_type,
|
|
92
|
+
special=special,
|
|
93
|
+
percentage=percentage,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_type_chart() -> Mapping[str, Mapping[str, Fraction]]:
|
|
98
|
+
"""Load the initial packaged type chart used by current battle logic."""
|
|
99
|
+
path = resources.files("pyvgc.data").joinpath("initial_type_chart.csv")
|
|
100
|
+
with path.open(newline="", encoding="utf-8") as file:
|
|
101
|
+
return MappingProxyType({
|
|
102
|
+
row["AttackingType"]: MappingProxyType({
|
|
103
|
+
defending: Fraction(row[defending]) for defending in TYPES
|
|
104
|
+
})
|
|
105
|
+
for row in csv.DictReader(file)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
CARTRIDGE_CHAMPIONS_RELEASE_2026 = BattleLogic(
|
|
110
|
+
name="Cartridge-Champions-Release-2026",
|
|
111
|
+
type_chart=_load_type_chart(),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
SIMULATOR_CHAMPIONS_RELEASE_2026 = replace(
|
|
115
|
+
CARTRIDGE_CHAMPIONS_RELEASE_2026,
|
|
116
|
+
name="Simulator-Champions-Release-2026",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
BATTLE_LOGICS = MappingProxyType(
|
|
120
|
+
{
|
|
121
|
+
"Cartridge-Champions-Release-2026": CARTRIDGE_CHAMPIONS_RELEASE_2026,
|
|
122
|
+
"Simulator-Champions-Release-2026": SIMULATOR_CHAMPIONS_RELEASE_2026,
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_battle_logic(name: str) -> BattleLogic:
|
|
128
|
+
"""Return a registered battle logic by case-insensitive name."""
|
|
129
|
+
if not isinstance(name, str) or not name.strip():
|
|
130
|
+
raise ValueError("battle logic name must be a non-empty string")
|
|
131
|
+
key = name.strip().casefold()
|
|
132
|
+
for logic_name, logic in BATTLE_LOGICS.items():
|
|
133
|
+
if logic_name.casefold() == key:
|
|
134
|
+
return logic
|
|
135
|
+
raise KeyError(f"Unknown battle logic: {name!r}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shared constants and packaged data paths for pyvgc."""
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
|
|
5
|
+
LEVEL_DOUBLES = 50
|
|
6
|
+
|
|
7
|
+
BASE_HP_INCREASE = 75
|
|
8
|
+
BASE_STAT_INCREASE = 20
|
|
9
|
+
|
|
10
|
+
MAX_TRAINING_PER_STAT = 32
|
|
11
|
+
MAX_TOTAL_TRAINING = 66
|
|
12
|
+
MIN_STAT_STAGE = -6
|
|
13
|
+
MAX_STAT_STAGE = 6
|
|
14
|
+
|
|
15
|
+
STAT_STAGE_MULTIPLIERS = {
|
|
16
|
+
stage: (2 + stage, 2) if stage >= 0 else (2, 2 - stage)
|
|
17
|
+
for stage in range(MIN_STAT_STAGE, MAX_STAT_STAGE + 1)
|
|
18
|
+
}
|
|
19
|
+
DAMAGE_LEVEL_DIVISOR = 5
|
|
20
|
+
DAMAGE_DIVISOR = 50
|
|
21
|
+
|
|
22
|
+
RANDOM_PERCENTAGES = range(85, 101)
|
|
23
|
+
STAB_MULTIPLIER = Fraction(3, 2)
|
|
24
|
+
CRITICAL_MULTIPLIER = Fraction(3, 2)
|
|
25
|
+
STAT_NAMES = (
|
|
26
|
+
"hp", "attack", "defense", "special_attack", "special_defense", "speed"
|
|
27
|
+
)
|
|
28
|
+
TYPES = frozenset((
|
|
29
|
+
"Normal", "Fire", "Water", "Electric", "Grass", "Ice", "Fighting",
|
|
30
|
+
"Poison", "Ground", "Flying", "Psychic", "Bug", "Rock", "Ghost",
|
|
31
|
+
"Dragon", "Dark", "Steel", "Fairy",
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
ALWAYS_HIT_VALUE = "always-hit"
|
|
35
|
+
|
|
36
|
+
NATURES = {
|
|
37
|
+
"Hardy": ("attack", "attack"), "Lonely": ("attack", "defense"),
|
|
38
|
+
"Brave": ("attack", "speed"), "Adamant": ("attack", "special_attack"),
|
|
39
|
+
"Naughty": ("attack", "special_defense"),
|
|
40
|
+
"Bold": ("defense", "attack"), "Docile": ("defense", "defense"),
|
|
41
|
+
"Impish": ("defense", "special_attack"), "Lax": ("defense", "special_defense"),
|
|
42
|
+
"Relaxed": ("defense", "speed"),
|
|
43
|
+
"Modest": ("special_attack", "attack"), "Mild": ("special_attack", "defense"),
|
|
44
|
+
"Bashful": ("special_attack", "special_attack"),
|
|
45
|
+
"Rash": ("special_attack", "special_defense"), "Quiet": ("special_attack", "speed"),
|
|
46
|
+
"Calm": ("special_defense", "attack"), "Gentle": ("special_defense", "defense"),
|
|
47
|
+
"Careful": ("special_defense", "special_attack"),
|
|
48
|
+
"Quirky": ("special_defense", "special_defense"),
|
|
49
|
+
"Sassy": ("special_defense", "speed"),
|
|
50
|
+
"Timid": ("speed", "attack"), "Hasty": ("speed", "defense"),
|
|
51
|
+
"Jolly": ("speed", "special_attack"), "Naive": ("speed", "special_defense"),
|
|
52
|
+
"Serious": ("speed", "speed"),
|
|
53
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Pokémon Champions damage calculation for level-50 Pokémon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, TypedDict
|
|
7
|
+
|
|
8
|
+
from .battle_logic import BattleLogic, CARTRIDGE_CHAMPIONS_RELEASE_2026
|
|
9
|
+
from .constants import TYPES
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .pokemon import Pokemon
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DamageRolls(TypedDict, total=False):
|
|
16
|
+
"""Mapping returned by damage calculations."""
|
|
17
|
+
|
|
18
|
+
normal: list[int]
|
|
19
|
+
critical: list[int]
|
|
20
|
+
normal_percentage: list[float]
|
|
21
|
+
critical_percentage: list[float]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_type(type_name: str) -> str:
|
|
25
|
+
"""Validate and canonicalize one Pokémon type name."""
|
|
26
|
+
if not isinstance(type_name, str):
|
|
27
|
+
raise ValueError(f"type names must be strings, got {type(type_name).__name__}")
|
|
28
|
+
normalized = type_name.strip().title()
|
|
29
|
+
if normalized not in TYPES:
|
|
30
|
+
raise ValueError(f"unknown Pokémon type: {type_name!r}")
|
|
31
|
+
return normalized
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _type_multiplier_for_types(
|
|
35
|
+
move_type: str, defender_types: tuple[str, ...], logic: BattleLogic
|
|
36
|
+
) -> Fraction:
|
|
37
|
+
"""Return effectiveness for already-normalized defender types."""
|
|
38
|
+
if logic.type_chart is None:
|
|
39
|
+
raise ValueError(f"battle logic {logic.name!r} has no type chart")
|
|
40
|
+
multiplier = Fraction(1)
|
|
41
|
+
for defender_type in defender_types:
|
|
42
|
+
multiplier *= logic.type_chart[move_type][defender_type]
|
|
43
|
+
return multiplier
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _type_multiplier(
|
|
47
|
+
move_type: str, defender_types: Iterable[str], logic: BattleLogic
|
|
48
|
+
) -> Fraction:
|
|
49
|
+
"""Normalize types and return their combined effectiveness multiplier."""
|
|
50
|
+
normalized_move = _normalize_type(move_type)
|
|
51
|
+
normalized_defenders = tuple(
|
|
52
|
+
_normalize_type(defender_type) for defender_type in defender_types
|
|
53
|
+
)
|
|
54
|
+
return _type_multiplier_for_types(
|
|
55
|
+
normalized_move, normalized_defenders, logic
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def damage_rolls(
|
|
60
|
+
power: int,
|
|
61
|
+
attack: int,
|
|
62
|
+
defense: int,
|
|
63
|
+
move_type: str,
|
|
64
|
+
attacker_types: Iterable[str],
|
|
65
|
+
defender_types: Iterable[str],
|
|
66
|
+
*,
|
|
67
|
+
logic: BattleLogic = CARTRIDGE_CHAMPIONS_RELEASE_2026,
|
|
68
|
+
) -> DamageRolls:
|
|
69
|
+
"""Return all 16 normal and 16 critical damage rolls.
|
|
70
|
+
|
|
71
|
+
``attack`` and ``defense`` are the already-calculated battle stats.
|
|
72
|
+
Damage is floored at every formula step, as in the games. Critical hits
|
|
73
|
+
use the selected logic's critical multiplier and ignore the relevant stat
|
|
74
|
+
stages.
|
|
75
|
+
"""
|
|
76
|
+
if not all(
|
|
77
|
+
type(value) is int and value > 0
|
|
78
|
+
for value in (power, attack, defense)
|
|
79
|
+
):
|
|
80
|
+
raise ValueError("power, attack, and defense must be positive integers")
|
|
81
|
+
move_type = _normalize_type(move_type)
|
|
82
|
+
attacker_types = tuple(
|
|
83
|
+
_normalize_type(type_name) for type_name in attacker_types
|
|
84
|
+
)
|
|
85
|
+
defender_types = tuple(
|
|
86
|
+
_normalize_type(type_name) for type_name in defender_types
|
|
87
|
+
)
|
|
88
|
+
if not attacker_types or not defender_types:
|
|
89
|
+
raise ValueError("attacker_types and defender_types must not be empty")
|
|
90
|
+
if len(attacker_types) > 2 or len(defender_types) > 2:
|
|
91
|
+
raise ValueError("a Pokémon can have at most two types")
|
|
92
|
+
stab = logic.stab_multiplier if move_type in attacker_types else Fraction(1)
|
|
93
|
+
effectiveness = _type_multiplier(move_type, defender_types, logic)
|
|
94
|
+
|
|
95
|
+
# (((((2L/5)+2) * Power * A / D) / 50) + 2), with integer floors.
|
|
96
|
+
base = (
|
|
97
|
+
(
|
|
98
|
+
(2 * logic.level // logic.damage_level_divisor + 2)
|
|
99
|
+
* power
|
|
100
|
+
* attack
|
|
101
|
+
// defense
|
|
102
|
+
)
|
|
103
|
+
// logic.damage_divisor
|
|
104
|
+
) + 2
|
|
105
|
+
|
|
106
|
+
def rolls(critical: bool) -> list[int]:
|
|
107
|
+
"""Calculate all 16 random outcomes for one hit category."""
|
|
108
|
+
result: list[int] = []
|
|
109
|
+
critical_damage = int(base * logic.critical_multiplier) if critical else base
|
|
110
|
+
for random_percent in logic.random_percentages:
|
|
111
|
+
fixed = critical_damage * random_percent // 100
|
|
112
|
+
fixed = int(fixed * stab)
|
|
113
|
+
fixed = int(fixed * effectiveness)
|
|
114
|
+
result.append(fixed)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
return {"normal": rolls(False), "critical": rolls(True)}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def calculate_damage(
|
|
121
|
+
attacker: "Pokemon",
|
|
122
|
+
defender: "Pokemon",
|
|
123
|
+
power: int,
|
|
124
|
+
move_type: str,
|
|
125
|
+
*,
|
|
126
|
+
special: bool = False,
|
|
127
|
+
percentage: bool = True,
|
|
128
|
+
) -> DamageRolls:
|
|
129
|
+
"""Calculate damage rolls using two Pokémon and their current battle state.
|
|
130
|
+
|
|
131
|
+
The Pokémon models provide state only. This function owns the damage
|
|
132
|
+
calculation, including critical-hit stat-stage handling and percentages.
|
|
133
|
+
"""
|
|
134
|
+
from .pokemon import Pokemon
|
|
135
|
+
|
|
136
|
+
if not isinstance(attacker, Pokemon) or not isinstance(defender, Pokemon):
|
|
137
|
+
raise TypeError("attacker and defender must be Pokemon instances")
|
|
138
|
+
if attacker.battle_logic != defender.battle_logic:
|
|
139
|
+
raise ValueError("attacker and defender must use the same battle logic")
|
|
140
|
+
|
|
141
|
+
attack_stat = "special_attack" if special else "attack"
|
|
142
|
+
defense_stat = "special_defense" if special else "defense"
|
|
143
|
+
attack = getattr(attacker, attack_stat)
|
|
144
|
+
defense = getattr(defender, defense_stat)
|
|
145
|
+
rolls = damage_rolls(
|
|
146
|
+
power,
|
|
147
|
+
attack,
|
|
148
|
+
defense,
|
|
149
|
+
move_type,
|
|
150
|
+
attacker.types,
|
|
151
|
+
defender.types,
|
|
152
|
+
logic=attacker.battle_logic,
|
|
153
|
+
)
|
|
154
|
+
critical_attack = (
|
|
155
|
+
attacker._pre_stage_stats[attack_stat]
|
|
156
|
+
if attacker._stat_stages.get(attack_stat, 0) < 0
|
|
157
|
+
else attack
|
|
158
|
+
)
|
|
159
|
+
critical_defense = (
|
|
160
|
+
defender._pre_stage_stats[defense_stat]
|
|
161
|
+
if defender._stat_stages.get(defense_stat, 0) > 0
|
|
162
|
+
else defense
|
|
163
|
+
)
|
|
164
|
+
if critical_attack != attack or critical_defense != defense:
|
|
165
|
+
rolls["critical"] = damage_rolls(
|
|
166
|
+
power,
|
|
167
|
+
critical_attack,
|
|
168
|
+
critical_defense,
|
|
169
|
+
move_type,
|
|
170
|
+
attacker.types,
|
|
171
|
+
defender.types,
|
|
172
|
+
logic=attacker.battle_logic,
|
|
173
|
+
)["critical"]
|
|
174
|
+
if percentage:
|
|
175
|
+
rolls["normal_percentage"] = [
|
|
176
|
+
round(damage / defender.hp * 100, 2)
|
|
177
|
+
for damage in rolls["normal"]
|
|
178
|
+
]
|
|
179
|
+
rolls["critical_percentage"] = [
|
|
180
|
+
round(damage / defender.hp * 100, 2)
|
|
181
|
+
for damage in rolls["critical"]
|
|
182
|
+
]
|
|
183
|
+
return rolls
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled data files for pyvgc."""
|