type effectiveness per generation
This commit is contained in:
@@ -89,6 +89,7 @@ class TypeEffectiveness:
|
||||
attacking_type: str
|
||||
defending_type: str
|
||||
damage_factor: float # 0.0, 0.5, 1.0, 2.0
|
||||
generation: str # generation name (e.g., "generation-i", "generation-ii")
|
||||
|
||||
|
||||
class PokemonDownloader:
|
||||
@@ -235,59 +236,159 @@ class PokemonDownloader:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download move {move_id}: {e}")
|
||||
return None
|
||||
|
||||
def download_type_effectiveness(self) -> List[TypeEffectiveness]:
|
||||
|
||||
def download_type_effectiveness(self, target_generation: str = "generation-i") -> List[TypeEffectiveness]:
|
||||
"""
|
||||
Download type effectiveness data.
|
||||
|
||||
Download type effectiveness data for a specific generation.
|
||||
|
||||
Args:
|
||||
target_generation: Generation to get effectiveness for (default: "generation-i")
|
||||
|
||||
Returns:
|
||||
List of TypeEffectiveness objects
|
||||
List of TypeEffectiveness objects for the specified generation
|
||||
"""
|
||||
effectiveness_data = []
|
||||
|
||||
|
||||
try:
|
||||
# Get all types (Gen 1 has 15 types)
|
||||
gen1_types = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon'
|
||||
# First, build a cache of type generations to avoid repeated API calls
|
||||
console.print("🔍 Building type generation cache...")
|
||||
type_generations = self._build_type_generation_cache()
|
||||
|
||||
# Filter types that exist in the target generation
|
||||
target_gen_index = self._generation_order.index(target_generation)
|
||||
valid_types = [
|
||||
type_name for type_name, gen_name in type_generations.items()
|
||||
if self._generation_order.index(gen_name) <= target_gen_index
|
||||
]
|
||||
|
||||
for type_name in gen1_types:
|
||||
type_data = self._safe_api_call(pb.type_, type_name)
|
||||
|
||||
# Double damage to
|
||||
for relation in type_data.damage_relations.double_damage_to:
|
||||
if relation.name in gen1_types:
|
||||
|
||||
console.print(f"📊 Processing {len(valid_types)} types for {target_generation}")
|
||||
|
||||
for type_name in valid_types:
|
||||
try:
|
||||
type_data = self._safe_api_call(pb.type_, type_name)
|
||||
|
||||
# Process current damage relations for the target generation
|
||||
current_relations = self._get_damage_relations_for_generation(
|
||||
type_data, target_generation, type_generations
|
||||
)
|
||||
|
||||
# Add current generation effectiveness data
|
||||
for defending_type, damage_factor in current_relations.items():
|
||||
effectiveness_data.append(TypeEffectiveness(
|
||||
attacking_type=type_name,
|
||||
defending_type=relation.name,
|
||||
damage_factor=2.0
|
||||
defending_type=defending_type,
|
||||
damage_factor=damage_factor,
|
||||
generation=target_generation
|
||||
))
|
||||
|
||||
# Half damage to
|
||||
for relation in type_data.damage_relations.half_damage_to:
|
||||
if relation.name in gen1_types:
|
||||
effectiveness_data.append(TypeEffectiveness(
|
||||
attacking_type=type_name,
|
||||
defending_type=relation.name,
|
||||
damage_factor=0.5
|
||||
))
|
||||
|
||||
# No damage to
|
||||
for relation in type_data.damage_relations.no_damage_to:
|
||||
if relation.name in gen1_types:
|
||||
effectiveness_data.append(TypeEffectiveness(
|
||||
attacking_type=type_name,
|
||||
defending_type=relation.name,
|
||||
damage_factor=0.0
|
||||
))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process type {type_name}: {e}")
|
||||
continue
|
||||
|
||||
console.print(f"✅ Processed {len(effectiveness_data)} type effectiveness entries")
|
||||
return effectiveness_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download type effectiveness: {e}")
|
||||
return []
|
||||
|
||||
def _build_type_generation_cache(self) -> Dict[str, str]:
|
||||
"""
|
||||
Build a cache of type names to their generation.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping type name to generation name
|
||||
"""
|
||||
type_generations = {}
|
||||
|
||||
# List of all known Pokemon types across all generations
|
||||
all_types = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel',
|
||||
'fairy'
|
||||
]
|
||||
|
||||
for type_name in all_types:
|
||||
try:
|
||||
type_data = self._safe_api_call(pb.type_, type_name)
|
||||
type_generations[type_name] = type_data.generation.name
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get generation for type {type_name}: {e}")
|
||||
continue
|
||||
|
||||
return type_generations
|
||||
|
||||
@property
|
||||
def _generation_order(self) -> List[str]:
|
||||
"""Get the order of generations for comparison."""
|
||||
return [
|
||||
'generation-i', 'generation-ii', 'generation-iii', 'generation-iv',
|
||||
'generation-v', 'generation-vi', 'generation-vii', 'generation-viii',
|
||||
'generation-ix'
|
||||
]
|
||||
|
||||
def _get_damage_relations_for_generation(self, type_data, target_generation: str, type_generations: Dict[str, str]) -> Dict[str, float]:
|
||||
"""
|
||||
Extract damage relations for a specific generation from type data.
|
||||
|
||||
Args:
|
||||
type_data: Type data from PokeAPI
|
||||
target_generation: Target generation name
|
||||
type_generations: Cache of type names to their generation
|
||||
|
||||
Returns:
|
||||
Dictionary mapping defending type to damage factor
|
||||
"""
|
||||
relations = {}
|
||||
|
||||
# Check if we need historical data
|
||||
if type_data.generation.name == target_generation:
|
||||
# Current generation - use current damage relations
|
||||
damage_relations = type_data.damage_relations
|
||||
else:
|
||||
# Look for historical data
|
||||
damage_relations = None
|
||||
for past_relation in type_data.past_damage_relations:
|
||||
if past_relation.generation.name == target_generation:
|
||||
damage_relations = past_relation.damage_relations
|
||||
break
|
||||
|
||||
# If no historical data found, use current relations
|
||||
if damage_relations is None:
|
||||
damage_relations = type_data.damage_relations
|
||||
|
||||
# Extract damage factors, filtering by generation
|
||||
target_gen_index = self._generation_order.index(target_generation)
|
||||
|
||||
if hasattr(damage_relations, 'double_damage_to'):
|
||||
for relation in damage_relations.double_damage_to:
|
||||
# Only include types that exist in the target generation
|
||||
defending_gen = type_generations.get(relation.name)
|
||||
if defending_gen:
|
||||
defending_gen_index = self._generation_order.index(defending_gen)
|
||||
if defending_gen_index <= target_gen_index:
|
||||
relations[relation.name] = 2.0
|
||||
|
||||
if hasattr(damage_relations, 'half_damage_to'):
|
||||
for relation in damage_relations.half_damage_to:
|
||||
# Only include types that exist in the target generation
|
||||
defending_gen = type_generations.get(relation.name)
|
||||
if defending_gen:
|
||||
defending_gen_index = self._generation_order.index(defending_gen)
|
||||
if defending_gen_index <= target_gen_index:
|
||||
relations[relation.name] = 0.5
|
||||
|
||||
if hasattr(damage_relations, 'no_damage_to'):
|
||||
for relation in damage_relations.no_damage_to:
|
||||
# Only include types that exist in the target generation
|
||||
defending_gen = type_generations.get(relation.name)
|
||||
if defending_gen:
|
||||
defending_gen_index = self._generation_order.index(defending_gen)
|
||||
if defending_gen_index <= target_gen_index:
|
||||
relations[relation.name] = 0.0
|
||||
|
||||
return relations
|
||||
|
||||
def download_pokemon_batch(self, start_id: int, end_id: int, max_workers: int = 5) -> Dict[int, PokemonData]:
|
||||
"""
|
||||
@@ -572,65 +673,101 @@ def download_moves(ctx, move_ids, workers):
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--generation', default='generation-i', help='Target generation for type effectiveness')
|
||||
@click.pass_context
|
||||
def download_types(ctx):
|
||||
def download_types(ctx, generation):
|
||||
"""Download type effectiveness data."""
|
||||
downloader = ctx.obj['downloader']
|
||||
|
||||
|
||||
console.print(Panel.fit(
|
||||
"🔽 Downloading type effectiveness data",
|
||||
f"🔽 Downloading type effectiveness data for {generation}",
|
||||
style="bold blue"
|
||||
))
|
||||
|
||||
effectiveness_data = downloader.download_type_effectiveness()
|
||||
|
||||
|
||||
effectiveness_data = downloader.download_type_effectiveness(generation)
|
||||
|
||||
if effectiveness_data:
|
||||
downloader.save_type_effectiveness(effectiveness_data)
|
||||
filename = f"type_effectiveness_{generation}.json"
|
||||
downloader.save_type_effectiveness(effectiveness_data, filename)
|
||||
console.print(f"✅ Successfully downloaded {len(effectiveness_data)} type effectiveness entries")
|
||||
else:
|
||||
console.print("❌ Failed to download type effectiveness data")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--generations', default='generation-i,generation-ii,generation-iii,generation-iv,generation-v,generation-vi,generation-vii,generation-viii,generation-ix',
|
||||
help='Comma-separated list of generations to download')
|
||||
@click.option('--workers', default=3, help='Number of concurrent workers')
|
||||
@click.pass_context
|
||||
def download_types_multi(ctx, generations, workers):
|
||||
"""Download type effectiveness data for multiple generations."""
|
||||
downloader = ctx.obj['downloader']
|
||||
|
||||
generation_list = [gen.strip() for gen in generations.split(',')]
|
||||
console.print(Panel.fit(
|
||||
f"🔽 Downloading type effectiveness for {len(generation_list)} generations",
|
||||
style="bold blue"
|
||||
))
|
||||
|
||||
total_entries = 0
|
||||
for generation in generation_list:
|
||||
console.print(f"\n📊 Processing {generation}...")
|
||||
|
||||
effectiveness_data = downloader.download_type_effectiveness(generation)
|
||||
|
||||
if effectiveness_data:
|
||||
filename = f"type_effectiveness_{generation}.json"
|
||||
downloader.save_type_effectiveness(effectiveness_data, filename)
|
||||
total_entries += len(effectiveness_data)
|
||||
console.print(f"✅ Saved {len(effectiveness_data)} entries for {generation}")
|
||||
else:
|
||||
console.print(f"❌ Failed to download data for {generation}")
|
||||
|
||||
console.print(f"\n🎉 Downloaded type effectiveness for {len(generation_list)} generations ({total_entries} total entries)")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--start', default=1, help='Starting Pokemon ID')
|
||||
@click.option('--end', default=151, help='Ending Pokemon ID (151 for Gen 1)')
|
||||
@click.option('--workers', default=5, help='Number of concurrent workers')
|
||||
@click.option('--generation', default='generation-i', help='Target generation for type effectiveness')
|
||||
@click.pass_context
|
||||
def download_complete(ctx, start, end, workers):
|
||||
def download_complete(ctx, start, end, workers, generation):
|
||||
"""Download complete dataset (Pokemon, moves, and type effectiveness)."""
|
||||
downloader = ctx.obj['downloader']
|
||||
|
||||
|
||||
console.print(Panel.fit(
|
||||
f"🔽 Downloading complete Pokemon dataset ({start}-{end})",
|
||||
f"🔽 Downloading complete Pokemon dataset ({start}-{end}) for {generation}",
|
||||
style="bold blue"
|
||||
))
|
||||
|
||||
|
||||
# Download Pokemon
|
||||
pokemon_data = downloader.download_pokemon_batch(start, end, workers)
|
||||
|
||||
|
||||
if pokemon_data:
|
||||
downloader.save_pokemon_data(pokemon_data, f"pokemon_complete_{start}_{end}.json")
|
||||
|
||||
|
||||
# Get all unique moves
|
||||
all_move_ids = set()
|
||||
for pokemon in pokemon_data.values():
|
||||
all_move_ids.update(pokemon.moves)
|
||||
|
||||
|
||||
# Download moves
|
||||
if all_move_ids:
|
||||
moves_data = downloader.download_moves_batch(list(all_move_ids), workers)
|
||||
if moves_data:
|
||||
downloader.save_moves_data(moves_data, f"moves_complete_{start}_{end}.json")
|
||||
|
||||
# Download type effectiveness
|
||||
effectiveness_data = downloader.download_type_effectiveness()
|
||||
|
||||
# Download type effectiveness for specified generation
|
||||
effectiveness_data = downloader.download_type_effectiveness(generation)
|
||||
if effectiveness_data:
|
||||
downloader.save_type_effectiveness(effectiveness_data, "type_effectiveness_complete.json")
|
||||
|
||||
filename = f"type_effectiveness_complete_{generation}.json"
|
||||
downloader.save_type_effectiveness(effectiveness_data, filename)
|
||||
|
||||
# Show final summary
|
||||
summary_table = downloader.get_stats_summary(pokemon_data)
|
||||
console.print(summary_table)
|
||||
|
||||
|
||||
console.print("🎉 Complete dataset download finished!")
|
||||
else:
|
||||
console.print("❌ Failed to download Pokemon data")
|
||||
|
||||
@@ -111,9 +111,10 @@ TYPE_EFFECTIVENESS_ENTRY_SCHEMA = {
|
||||
"damage_factor": {
|
||||
"type": "number",
|
||||
"enum": [0.0, 0.5, 1.0, 2.0] # Only valid damage multipliers
|
||||
}
|
||||
},
|
||||
"generation": {"type": "string", "minLength": 1} # Generation name (e.g., "generation-i")
|
||||
},
|
||||
"required": ["attacking_type", "defending_type", "damage_factor"],
|
||||
"required": ["attacking_type", "defending_type", "damage_factor", "generation"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
|
||||
@@ -124,13 +125,58 @@ TYPE_EFFECTIVENESS_SCHEMA = {
|
||||
"uniqueItems": True
|
||||
}
|
||||
|
||||
# Valid Generation 1 types for additional validation
|
||||
GEN1_TYPES = {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon'
|
||||
# Valid types by generation for additional validation
|
||||
GENERATION_TYPES = {
|
||||
'generation-i': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon'
|
||||
},
|
||||
'generation-ii': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel'
|
||||
},
|
||||
'generation-iii': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel'
|
||||
},
|
||||
'generation-iv': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel'
|
||||
},
|
||||
'generation-v': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel'
|
||||
},
|
||||
'generation-vi': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
|
||||
},
|
||||
'generation-vii': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
|
||||
},
|
||||
'generation-viii': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
|
||||
},
|
||||
'generation-ix': {
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
|
||||
'fighting', 'poison', 'ground', 'flying', 'psychic',
|
||||
'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
|
||||
}
|
||||
}
|
||||
|
||||
# Backward compatibility
|
||||
GEN1_TYPES = GENERATION_TYPES['generation-i']
|
||||
|
||||
|
||||
class DataValidator:
|
||||
"""Validator class for Pokemon data using JSON schemas."""
|
||||
@@ -260,27 +306,30 @@ class DataValidator:
|
||||
def validate_type_effectiveness(self, effectiveness_data: List[Dict[str, Any]]) -> List[str]:
|
||||
"""
|
||||
Validate type effectiveness data.
|
||||
|
||||
|
||||
Args:
|
||||
effectiveness_data: List of type effectiveness entries
|
||||
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
|
||||
# Schema validation
|
||||
for error in self.type_effectiveness_validator.iter_errors(effectiveness_data):
|
||||
errors.append(f"Schema error: {error.message}")
|
||||
|
||||
# Additional validation
|
||||
|
||||
# Additional validation using generation-specific types
|
||||
for i, entry in enumerate(effectiveness_data):
|
||||
if 'attacking_type' in entry and entry['attacking_type'] not in GEN1_TYPES:
|
||||
errors.append(f"Entry {i}: Invalid attacking type '{entry['attacking_type']}'")
|
||||
|
||||
if 'defending_type' in entry and entry['defending_type'] not in GEN1_TYPES:
|
||||
errors.append(f"Entry {i}: Invalid defending type '{entry['defending_type']}'")
|
||||
|
||||
generation = entry.get('generation', 'generation-i')
|
||||
valid_types = GENERATION_TYPES.get(generation, GEN1_TYPES)
|
||||
|
||||
if 'attacking_type' in entry and entry['attacking_type'] not in valid_types:
|
||||
errors.append(f"Entry {i}: Invalid attacking type '{entry['attacking_type']}' for {generation}")
|
||||
|
||||
if 'defending_type' in entry and entry['defending_type'] not in valid_types:
|
||||
errors.append(f"Entry {i}: Invalid defending type '{entry['defending_type']}' for {generation}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_file(self, file_path: Path, data_type: str) -> List[str]:
|
||||
|
||||
Reference in New Issue
Block a user