type effectiveness per generation

This commit is contained in:
cdemeyer-teachx
2025-08-14 18:50:14 +09:00
parent 058efbd673
commit 943d016e29
16 changed files with 6258 additions and 862 deletions

View File

@@ -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")

View File

@@ -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]: