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.
Files changed (38) hide show
  1. pyvgc-0.0.1/LICENSE +21 -0
  2. pyvgc-0.0.1/PKG-INFO +50 -0
  3. pyvgc-0.0.1/README.md +32 -0
  4. pyvgc-0.0.1/pyproject.toml +38 -0
  5. pyvgc-0.0.1/setup.cfg +4 -0
  6. pyvgc-0.0.1/src/pyvgc/__init__.py +45 -0
  7. pyvgc-0.0.1/src/pyvgc/battle_logic.py +135 -0
  8. pyvgc-0.0.1/src/pyvgc/constants.py +53 -0
  9. pyvgc-0.0.1/src/pyvgc/damage.py +183 -0
  10. pyvgc-0.0.1/src/pyvgc/data/__init__.py +1 -0
  11. pyvgc-0.0.1/src/pyvgc/data/battle_items.csv +167 -0
  12. pyvgc-0.0.1/src/pyvgc/data/full_pokemon_roster.csv +404 -0
  13. pyvgc-0.0.1/src/pyvgc/data/initial_pokemon_natures.csv +26 -0
  14. pyvgc-0.0.1/src/pyvgc/data/initial_type_chart.csv +19 -0
  15. pyvgc-0.0.1/src/pyvgc/data/list_of_regulations.csv +4 -0
  16. pyvgc-0.0.1/src/pyvgc/data/move_templates.csv +513 -0
  17. pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/items.csv +120 -0
  18. pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/learnsets.csv +333 -0
  19. pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/moves.csv +491 -0
  20. pyvgc-0.0.1/src/pyvgc/data/regulations/m_a/pokemon.csv +333 -0
  21. pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/items.csv +149 -0
  22. pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/learnsets.csv +369 -0
  23. pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/moves.csv +497 -0
  24. pyvgc-0.0.1/src/pyvgc/data/regulations/m_b/pokemon.csv +369 -0
  25. pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/items.csv +167 -0
  26. pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/learnsets.csv +404 -0
  27. pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/moves.csv +508 -0
  28. pyvgc-0.0.1/src/pyvgc/data/regulations/m_c/pokemon.csv +404 -0
  29. pyvgc-0.0.1/src/pyvgc/items.py +35 -0
  30. pyvgc-0.0.1/src/pyvgc/moves.py +112 -0
  31. pyvgc-0.0.1/src/pyvgc/pokemon.py +268 -0
  32. pyvgc-0.0.1/src/pyvgc/regulations.py +304 -0
  33. pyvgc-0.0.1/src/pyvgc/utils.py +115 -0
  34. pyvgc-0.0.1/src/pyvgc.egg-info/PKG-INFO +50 -0
  35. pyvgc-0.0.1/src/pyvgc.egg-info/SOURCES.txt +36 -0
  36. pyvgc-0.0.1/src/pyvgc.egg-info/dependency_links.txt +1 -0
  37. pyvgc-0.0.1/src/pyvgc.egg-info/top_level.txt +1 -0
  38. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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."""