- Derive image_path from card ID instead of raw data image_file field - Use simplified CDN paths: /<set>/<card>.webp (e.g., a1/033-charmander.webp) - Energy cards use basic/<type>.webp paths - Fix undefined variable bug in transform_trainer_card - Update tests to match new path format - Regenerate all 382 card definitions with correct image_url fields
489 lines
15 KiB
Python
489 lines
15 KiB
Python
"""Tests for the card conversion script.
|
|
|
|
These tests verify the transformation logic that converts raw scraped card data
|
|
into the CardDefinition format used by the game engine.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Add scripts directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts"))
|
|
|
|
from convert_cards import (
|
|
generate_energy_cards,
|
|
transform_attack,
|
|
transform_card,
|
|
transform_pokemon_card,
|
|
transform_trainer_card,
|
|
transform_weakness_resistance,
|
|
validate_card,
|
|
)
|
|
|
|
|
|
class TestTransformAttack:
|
|
"""Tests for the transform_attack function."""
|
|
|
|
def test_basic_attack_with_damage(self):
|
|
"""Test transforming a simple attack with base damage only.
|
|
|
|
Verifies that an attack with just damage and no modifier
|
|
produces a damage_display of just the number.
|
|
"""
|
|
raw_attack = {
|
|
"name": "Tackle",
|
|
"cost": ["colorless"],
|
|
"damage": 20,
|
|
"damage_modifier": None,
|
|
"effect_text": None,
|
|
"effect_id": None,
|
|
}
|
|
|
|
result = transform_attack(raw_attack)
|
|
|
|
assert result["name"] == "Tackle"
|
|
assert result["cost"] == ["colorless"]
|
|
assert result["damage"] == 20
|
|
assert result["damage_display"] == "20"
|
|
assert "effect_description" not in result
|
|
assert "effect_params" not in result
|
|
|
|
def test_attack_with_plus_modifier(self):
|
|
"""Test transforming an attack with '+' damage modifier.
|
|
|
|
Attacks like '50+' deal base damage plus additional damage
|
|
based on some condition (e.g., coin flips).
|
|
"""
|
|
raw_attack = {
|
|
"name": "Double Horn",
|
|
"cost": ["grass", "grass"],
|
|
"damage": 50,
|
|
"damage_modifier": "+",
|
|
"effect_text": "Flip 2 coins. This attack does 50 damage for each heads.",
|
|
"effect_id": None,
|
|
}
|
|
|
|
result = transform_attack(raw_attack)
|
|
|
|
assert result["damage"] == 50
|
|
assert result["damage_display"] == "50+"
|
|
assert result["effect_params"]["damage_modifier"] == "+"
|
|
assert "Flip 2 coins" in result["effect_description"]
|
|
|
|
def test_attack_with_x_modifier(self):
|
|
"""Test transforming an attack with 'x' damage modifier.
|
|
|
|
Attacks like '50x' deal damage multiplied by some factor
|
|
(e.g., number of coin flip heads).
|
|
"""
|
|
raw_attack = {
|
|
"name": "Psychic",
|
|
"cost": ["psychic", "colorless"],
|
|
"damage": 20,
|
|
"damage_modifier": "x",
|
|
"effect_text": "Does 20 damage times the number of Energy attached to your opponent's Active Pokemon.",
|
|
"effect_id": None,
|
|
}
|
|
|
|
result = transform_attack(raw_attack)
|
|
|
|
assert result["damage"] == 20
|
|
assert result["damage_display"] == "20x"
|
|
assert result["effect_params"]["damage_modifier"] == "x"
|
|
|
|
def test_attack_no_damage_effect_only(self):
|
|
"""Test transforming an attack that deals no damage.
|
|
|
|
Some attacks only have effects (like status conditions)
|
|
and don't deal direct damage.
|
|
"""
|
|
raw_attack = {
|
|
"name": "Confuse Ray",
|
|
"cost": ["psychic"],
|
|
"damage": 0,
|
|
"damage_modifier": None,
|
|
"effect_text": "The Defending Pokemon is now Confused.",
|
|
"effect_id": None,
|
|
}
|
|
|
|
result = transform_attack(raw_attack)
|
|
|
|
assert result["damage"] == 0
|
|
assert "damage_display" not in result # No display for 0 damage
|
|
assert result["effect_description"] == "The Defending Pokemon is now Confused."
|
|
|
|
def test_attack_with_effect_id(self):
|
|
"""Test that existing effect_id is preserved.
|
|
|
|
Some scraped cards may already have effect IDs mapped.
|
|
"""
|
|
raw_attack = {
|
|
"name": "Thunder Shock",
|
|
"cost": ["lightning"],
|
|
"damage": 20,
|
|
"damage_modifier": None,
|
|
"effect_text": "Flip a coin. If heads, the Defending Pokemon is now Paralyzed.",
|
|
"effect_id": "may_paralyze",
|
|
}
|
|
|
|
result = transform_attack(raw_attack)
|
|
|
|
assert result["effect_id"] == "may_paralyze"
|
|
|
|
|
|
class TestTransformWeaknessResistance:
|
|
"""Tests for the transform_weakness_resistance function."""
|
|
|
|
def test_transform_weakness(self):
|
|
"""Test transforming a typical weakness.
|
|
|
|
Weaknesses have a type and a value (damage modifier).
|
|
"""
|
|
raw_weakness = {"type": "fire", "value": 20}
|
|
|
|
result = transform_weakness_resistance(raw_weakness)
|
|
|
|
assert result["energy_type"] == "fire"
|
|
assert result["value"] == 20
|
|
|
|
def test_transform_resistance(self):
|
|
"""Test transforming a typical resistance.
|
|
|
|
Resistances reduce damage taken from a specific type.
|
|
"""
|
|
raw_resistance = {"type": "fighting", "value": -30}
|
|
|
|
result = transform_weakness_resistance(raw_resistance)
|
|
|
|
assert result["energy_type"] == "fighting"
|
|
assert result["value"] == -30
|
|
|
|
def test_transform_none(self):
|
|
"""Test that None input returns None.
|
|
|
|
Not all Pokemon have weakness or resistance.
|
|
"""
|
|
result = transform_weakness_resistance(None)
|
|
assert result is None
|
|
|
|
|
|
class TestTransformPokemonCard:
|
|
"""Tests for transforming Pokemon cards."""
|
|
|
|
def test_basic_pokemon(self):
|
|
"""Test transforming a basic Pokemon card.
|
|
|
|
Verifies all fields are correctly mapped from raw to definition format.
|
|
"""
|
|
raw_card = {
|
|
"id": "a1-001-bulbasaur",
|
|
"name": "Bulbasaur",
|
|
"set_code": "a1",
|
|
"set_name": "Genetic Apex",
|
|
"card_number": 1,
|
|
"rarity": "Common",
|
|
"card_type": "pokemon",
|
|
"image_url": "https://example.com/bulbasaur.webp",
|
|
"image_file": "a1/001-bulbasaur.webp",
|
|
"hp": 70,
|
|
"pokemon_type": "grass",
|
|
"stage": "basic",
|
|
"evolves_from": None,
|
|
"is_ex": False,
|
|
"abilities": [],
|
|
"attacks": [
|
|
{
|
|
"name": "Vine Whip",
|
|
"cost": ["grass", "colorless"],
|
|
"damage": 40,
|
|
"damage_modifier": None,
|
|
"effect_text": None,
|
|
"effect_id": None,
|
|
}
|
|
],
|
|
"weakness": {"type": "fire", "value": 20},
|
|
"resistance": None,
|
|
"retreat_cost": 1,
|
|
"flavor_text": None,
|
|
"illustrator": "Narumi Sato",
|
|
}
|
|
|
|
result = transform_pokemon_card(raw_card)
|
|
|
|
assert result["id"] == "a1-001-bulbasaur"
|
|
assert result["name"] == "Bulbasaur"
|
|
assert result["card_type"] == "pokemon"
|
|
assert result["hp"] == 70
|
|
assert result["pokemon_type"] == "grass"
|
|
assert result["stage"] == "basic"
|
|
assert result["variant"] == "normal"
|
|
assert result["set_id"] == "a1"
|
|
assert result["rarity"] == "common"
|
|
assert result["retreat_cost"] == 1
|
|
assert result["weakness"]["energy_type"] == "fire"
|
|
assert len(result["attacks"]) == 1
|
|
assert result["illustrator"] == "Narumi Sato"
|
|
assert result["image_path"] == "a1/001-bulbasaur.webp"
|
|
assert "mantipocket.s3" in result["image_url"]
|
|
|
|
def test_ex_pokemon(self):
|
|
"""Test transforming an EX Pokemon card.
|
|
|
|
EX Pokemon have is_ex=True in raw data, which maps to variant='ex'.
|
|
"""
|
|
raw_card = {
|
|
"id": "a1-004-venusaur-ex",
|
|
"name": "Venusaur ex",
|
|
"set_code": "a1",
|
|
"card_type": "pokemon",
|
|
"image_file": "a1/004-venusaur-ex.webp",
|
|
"hp": 190,
|
|
"pokemon_type": "grass",
|
|
"stage": "stage_2",
|
|
"evolves_from": "Ivysaur",
|
|
"is_ex": True,
|
|
"abilities": [],
|
|
"attacks": [],
|
|
"weakness": {"type": "fire", "value": 20},
|
|
"resistance": None,
|
|
"retreat_cost": 3,
|
|
"rarity": "Double Rare",
|
|
"illustrator": "PLANETA CG Works",
|
|
}
|
|
|
|
result = transform_pokemon_card(raw_card)
|
|
|
|
assert result["variant"] == "ex"
|
|
assert result["evolves_from"] == "Ivysaur"
|
|
assert result["rarity"] == "double rare"
|
|
|
|
def test_evolution_pokemon(self):
|
|
"""Test transforming an evolution Pokemon.
|
|
|
|
Stage 1 and Stage 2 Pokemon must have evolves_from set.
|
|
"""
|
|
raw_card = {
|
|
"id": "a1-002-ivysaur",
|
|
"name": "Ivysaur",
|
|
"set_code": "a1",
|
|
"card_type": "pokemon",
|
|
"image_file": None,
|
|
"hp": 90,
|
|
"pokemon_type": "grass",
|
|
"stage": "stage_1",
|
|
"evolves_from": "Bulbasaur",
|
|
"is_ex": False,
|
|
"abilities": [],
|
|
"attacks": [],
|
|
"weakness": {"type": "fire", "value": 20},
|
|
"resistance": None,
|
|
"retreat_cost": 2,
|
|
"rarity": "Uncommon",
|
|
}
|
|
|
|
result = transform_pokemon_card(raw_card)
|
|
|
|
assert result["stage"] == "stage_1"
|
|
assert result["evolves_from"] == "Bulbasaur"
|
|
# Image path is now derived from card ID, not raw data
|
|
assert result["image_path"] == "a1/002-ivysaur.webp"
|
|
assert "mantipocket.s3" in result["image_url"]
|
|
|
|
|
|
class TestTransformTrainerCard:
|
|
"""Tests for transforming Trainer cards."""
|
|
|
|
def test_supporter_card(self):
|
|
"""Test transforming a Supporter trainer card.
|
|
|
|
Supporters are trainer cards limited to one per turn.
|
|
"""
|
|
raw_card = {
|
|
"id": "a1-220-misty",
|
|
"name": "Misty",
|
|
"set_code": "a1",
|
|
"card_type": "trainer",
|
|
"trainer_type": "supporter",
|
|
"rarity": "Uncommon",
|
|
"effect_text": "Choose 1 of your Pokemon...",
|
|
"illustrator": "Sanosuke Sakuma",
|
|
"image_file": "a1/220-misty.webp",
|
|
}
|
|
|
|
result = transform_trainer_card(raw_card)
|
|
|
|
assert result["id"] == "a1-220-misty"
|
|
assert result["name"] == "Misty"
|
|
assert result["card_type"] == "trainer"
|
|
assert result["trainer_type"] == "supporter"
|
|
assert result["set_id"] == "a1"
|
|
assert result["effect_description"] == "Choose 1 of your Pokemon..."
|
|
assert result["image_path"] == "a1/220-misty.webp"
|
|
|
|
|
|
class TestTransformCard:
|
|
"""Tests for the main transform_card dispatcher."""
|
|
|
|
def test_dispatches_to_pokemon(self):
|
|
"""Test that Pokemon cards are routed to transform_pokemon_card."""
|
|
raw_card = {
|
|
"id": "test-pokemon",
|
|
"name": "Test Pokemon",
|
|
"set_code": "test",
|
|
"card_type": "pokemon",
|
|
"hp": 60,
|
|
"pokemon_type": "fire",
|
|
"stage": "basic",
|
|
"is_ex": False,
|
|
"attacks": [],
|
|
"abilities": [],
|
|
"weakness": None,
|
|
"resistance": None,
|
|
"retreat_cost": 1,
|
|
"rarity": "Common",
|
|
}
|
|
|
|
result = transform_card(raw_card)
|
|
|
|
assert result["card_type"] == "pokemon"
|
|
|
|
def test_dispatches_to_trainer(self):
|
|
"""Test that Trainer cards are routed to transform_trainer_card."""
|
|
raw_card = {
|
|
"id": "test-trainer",
|
|
"name": "Test Trainer",
|
|
"set_code": "test",
|
|
"card_type": "trainer",
|
|
"trainer_type": "item",
|
|
"rarity": "Common",
|
|
}
|
|
|
|
result = transform_card(raw_card)
|
|
|
|
assert result["card_type"] == "trainer"
|
|
|
|
def test_unsupported_card_type_raises(self):
|
|
"""Test that unsupported card types raise ValueError."""
|
|
raw_card = {
|
|
"id": "test-unknown",
|
|
"card_type": "unknown",
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="Unsupported card type"):
|
|
transform_card(raw_card)
|
|
|
|
|
|
class TestValidateCard:
|
|
"""Tests for card validation against the CardDefinition model."""
|
|
|
|
def test_valid_pokemon_card(self):
|
|
"""Test that a valid Pokemon card passes validation."""
|
|
card_dict = {
|
|
"id": "test-pokemon",
|
|
"name": "Test Pokemon",
|
|
"card_type": "pokemon",
|
|
"hp": 60,
|
|
"pokemon_type": "fire",
|
|
"stage": "basic",
|
|
"variant": "normal",
|
|
"set_id": "test",
|
|
"rarity": "common",
|
|
"retreat_cost": 1,
|
|
}
|
|
|
|
result = validate_card(card_dict)
|
|
|
|
assert result.id == "test-pokemon"
|
|
assert result.hp == 60
|
|
|
|
def test_valid_energy_card(self):
|
|
"""Test that a valid Energy card passes validation."""
|
|
card_dict = {
|
|
"id": "energy-basic-fire",
|
|
"name": "Fire Energy",
|
|
"card_type": "energy",
|
|
"energy_type": "fire",
|
|
"energy_provides": ["fire"],
|
|
"set_id": "basic",
|
|
"rarity": "common",
|
|
}
|
|
|
|
result = validate_card(card_dict)
|
|
|
|
assert result.id == "energy-basic-fire"
|
|
assert result.energy_type.value == "fire"
|
|
|
|
def test_invalid_pokemon_missing_hp(self):
|
|
"""Test that Pokemon without HP fails validation."""
|
|
card_dict = {
|
|
"id": "invalid-pokemon",
|
|
"name": "Invalid Pokemon",
|
|
"card_type": "pokemon",
|
|
"hp": None,
|
|
"pokemon_type": "fire",
|
|
"stage": "basic",
|
|
}
|
|
|
|
with pytest.raises(ValueError):
|
|
validate_card(card_dict)
|
|
|
|
|
|
class TestGenerateEnergyCards:
|
|
"""Tests for basic energy card generation."""
|
|
|
|
def test_generates_all_energy_types(self):
|
|
"""Test that all 10 basic energy types are generated.
|
|
|
|
The game uses 10 energy types: colorless, darkness, dragon,
|
|
fighting, fire, grass, lightning, metal, psychic, water.
|
|
"""
|
|
cards = generate_energy_cards()
|
|
|
|
assert len(cards) == 10
|
|
|
|
# Check all types are present
|
|
energy_types = {card["energy_type"] for card in cards}
|
|
expected_types = {
|
|
"colorless",
|
|
"darkness",
|
|
"dragon",
|
|
"fighting",
|
|
"fire",
|
|
"grass",
|
|
"lightning",
|
|
"metal",
|
|
"psychic",
|
|
"water",
|
|
}
|
|
assert energy_types == expected_types
|
|
|
|
def test_energy_card_structure(self):
|
|
"""Test that generated energy cards have correct structure.
|
|
|
|
Each energy card should have id, name, card_type, energy_type,
|
|
energy_provides, rarity, set_id, and image paths.
|
|
"""
|
|
cards = generate_energy_cards()
|
|
grass_energy = next(c for c in cards if c["energy_type"] == "grass")
|
|
|
|
assert grass_energy["id"] == "energy-basic-grass"
|
|
assert grass_energy["name"] == "Grass Energy"
|
|
assert grass_energy["card_type"] == "energy"
|
|
assert grass_energy["energy_provides"] == ["grass"]
|
|
assert grass_energy["rarity"] == "common"
|
|
assert grass_energy["set_id"] == "basic"
|
|
assert grass_energy["image_path"] == "basic/grass.webp"
|
|
|
|
def test_energy_cards_validate(self):
|
|
"""Test that all generated energy cards pass validation.
|
|
|
|
Each card should be valid against the CardDefinition model.
|
|
"""
|
|
cards = generate_energy_cards()
|
|
|
|
for card in cards:
|
|
result = validate_card(card)
|
|
assert result.card_type.value == "energy"
|