Merge pull request #634 from phrasmotica/feature/past-type-efficacy

Add past type damage relations data
This commit is contained in:
Alessandro Pezzè 2021-07-24 15:45:50 +02:00 committed by GitHub
commit 2be93d816c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 548 additions and 117 deletions

View file

@ -594,6 +594,16 @@ def _build_types():
build_generic((TypeEfficacy,), "type_efficacy.csv", csv_record_to_objects)
def csv_record_to_objects(info):
yield TypeEfficacyPast(
damage_type_id=int(info[0]),
target_type_id=int(info[1]),
damage_factor=int(info[2]),
generation_id=int(info[3]),
)
build_generic((TypeEfficacyPast,), "type_efficacy_past.csv", csv_record_to_objects)
#############
# CONTEST #

View file

@ -0,0 +1,7 @@
damage_type_id,target_type_id,damage_factor,generation_id
4,7,200,1
7,4,200,1
8,14,0,1
15,10,100,1
8,9,50,5
17,9,50,5
1 damage_type_id target_type_id damage_factor generation_id
2 4 7 200 1
3 7 4 200 1
4 8 14 0 1
5 15 10 100 1
6 8 9 50 5
7 17 9 50 5

View file

@ -2864,67 +2864,133 @@ Types are properties for Pokémon and their moves. Each type has three propertie
```json
{
"id": 5,
"name": "ground",
"damage_relations": {
"no_damage_to": [{
"name": "flying",
"url": "http://pokeapi.co/api/v2/type/3/"
}],
"half_damage_to": [{
"name": "bug",
"url": "http://pokeapi.co/api/v2/type/7/"
}],
"double_damage_to": [{
"name": "poison",
"url": "http://pokeapi.co/api/v2/type/4/"
}],
"no_damage_from": [{
"name": "electric",
"url": "http://pokeapi.co/api/v2/type/13/"
}],
"half_damage_from": [{
"name": "poison",
"url": "http://pokeapi.co/api/v2/type/4/"
}],
"double_damage_from": [{
"name": "water",
"url": "http://pokeapi.co/api/v2/type/11/"
}]
},
"game_indices": [{
"game_index": 4,
"generation": {
"name": "generation-i",
"url": "http://pokeapi.co/api/v2/generation/1/"
}
}],
"generation": {
"name": "generation-i",
"url": "http://pokeapi.co/api/v2/generation/1/"
},
"move_damage_class": {
"name": "physical",
"url": "http://pokeapi.co/api/v2/move-damage-class/2/"
},
"names": [{
"name": "じめん",
"language": {
"name": "ja",
"url": "http://pokeapi.co/api/v2/language/1/"
}
}],
"pokemon": [{
"slot": 1,
"pokemon": {
"name": "sandshrew",
"url": "http://pokeapi.co/api/v2/pokemon/27/"
}
}],
"moves": [{
"name": "sand-attack",
"url": "http://pokeapi.co/api/v2/move/28/"
}]
"id": 8,
"name": "ghost",
"damage_relations": {
"no_damage_to": [
{
"name": "normal",
"url": "https://pokeapi.co/api/v2/type/1/"
}
],
"half_damage_to": [
{
"name": "dark",
"url": "https://pokeapi.co/api/v2/type/17/"
}
],
"double_damage_to": [
{
"name": "ghost",
"url": "https://pokeapi.co/api/v2/type/8/"
}
],
"no_damage_from": [
{
"name": "normal",
"url": "https://pokeapi.co/api/v2/type/1/"
}
],
"half_damage_from": [
{
"name": "poison",
"url": "https://pokeapi.co/api/v2/type/4/"
}
],
"double_damage_from": [
{
"name": "ghost",
"url": "https://pokeapi.co/api/v2/type/8/"
}
]
},
"past_damage_relations": [
{
"generation": {
"name": "generation-v",
"url": "https://pokeapi.co/api/v2/generation/5/"
},
"damage_relations": {
"no_damage_to": [
{
"name": "normal",
"url": "https://pokeapi.co/api/v2/type/1/"
}
],
"half_damage_to": [
{
"name": "steel",
"url": "https://pokeapi.co/api/v2/type/9/"
}
],
"double_damage_to": [
{
"name": "ghost",
"url": "https://pokeapi.co/api/v2/type/8/"
}
],
"no_damage_from": [
{
"name": "normal",
"url": "https://pokeapi.co/api/v2/type/1/"
}
],
"half_damage_from": [
{
"name": "poison",
"url": "https://pokeapi.co/api/v2/type/4/"
}
],
"double_damage_from": [
{
"name": "ghost",
"url": "https://pokeapi.co/api/v2/type/8/"
}
]
}
}
],
"game_indices": [
{
"game_index": 8,
"generation": {
"name": "generation-i",
"url": "https://pokeapi.co/api/v2/generation/1/"
}
}
],
"generation": {
"name": "generation-i",
"url": "https://pokeapi.co/api/v2/generation/1/"
},
"move_damage_class": {
"name": "physical",
"url": "https://pokeapi.co/api/v2/move-damage-class/2/"
},
"names": [
{
"name": "ゴースト",
"language": {
"name": "ja-Hrkt",
"url": "https://pokeapi.co/api/v2/language/1/"
}
}
],
"pokemon": [
{
"slot": 1,
"pokemon": {
"name": "gastly",
"url": "https://pokeapi.co/api/v2/pokemon/92/"
}
}
],
"moves": [
{
"name": "night-shade",
"url": "https://pokeapi.co/api/v2/move/101/"
}
]
}
```
@ -2934,15 +3000,16 @@ Types are properties for Pokémon and their moves. Each type has three propertie
| Name | Description | Data Type |
| ---- | ----------- | --------- |
| id | The identifier for this type resource | integer |
| name | The name for this type resource | string |
| damage_relations | A detail of how effective this type is toward others and vice versa | [TypeRelations](#typerelations) |
| game_indices | A list of game indices relevent to this item by generation | list [GenerationGameIndex](#generationgameindex) |
| generation | The generation this type was introduced in | [NamedAPIResource](#namedapiresource) ([Generation](#generations)) |
| move_damage_class | The class of damage inflicted by this type | [NamedAPIResource](#namedapiresource) ([MoveDamageClass](#move-damage-classes)) |
| names | The name of this type listed in different languages | list [Name](#resourcename) |
| pokemon | A list of details of pokemon that have this type | [TypePokemon](#typepokemon) |
| moves | A list of moves that have this type | list [NamedAPIResource](#namedapiresource) ([Move](#moves)) |
| id | The identifier for this type resource | integer |
| name | The name for this type resource | string |
| damage_relations | A detail of how effective this type is toward others and vice versa | [TypeRelations](#typerelations) |
| past_damage_relations | A list of details of how effective this type was toward others and vice versa in previous generations | list [TypeRelationsPast](#typerelationspast) |
| game_indices | A list of game indices relevent to this item by generation | list [GenerationGameIndex](#generationgameindex) |
| generation | The generation this type was introduced in | [NamedAPIResource](#namedapiresource) ([Generation](#generations)) |
| move_damage_class | The class of damage inflicted by this type | [NamedAPIResource](#namedapiresource) ([MoveDamageClass](#move-damage-classes)) |
| names | The name of this type listed in different languages | list [Name](#resourcename) |
| pokemon | A list of details of pokemon that have this type | [TypePokemon](#typepokemon) |
| moves | A list of moves that have this type | list [NamedAPIResource](#namedapiresource) ([Move](#moves)) |
#### TypePokemon
@ -2962,6 +3029,13 @@ Types are properties for Pokémon and their moves. Each type has three propertie
| half_damage_from | A list of types that are not very effective against this type | list [NamedAPIResource](#namedapiresource) ([Type](#types)) |
| double_damage_from | A list of types that are very effective against this type | list [NamedAPIResource](#namedapiresource) ([Type](#types)) |
#### TypeRelationsPast
| Name | Description | Data Type |
| ---- | ----------- | --------- |
| generation | The last generation in which the referenced type had the listed damage relations | [NamedAPIResource](#namedapiresource) ([Generation](#generations)) |
| damage_relations | The damage relations the referenced type had up to and including the listed generation | list [TypeRelations](#typerelations) |
<h1 id="utility-section">Utility</h1>

View file

@ -0,0 +1,84 @@
# Generated by Django 2.1.11 on 2021-02-24 13:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("pokemon_v2", "0010_pokemonformtype"),
]
operations = [
migrations.CreateModel(
name="TypeEfficacyPast",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("damage_factor", models.IntegerField()),
(
"damage_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="typeefficacypast_damage_type",
to="pokemon_v2.Type",
),
),
(
"generation",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="typeefficacypast",
to="pokemon_v2.Generation",
),
),
(
"target_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="typeefficacypast_target_type",
to="pokemon_v2.Type",
),
),
],
options={
"abstract": False,
},
),
migrations.AlterField(
model_name="typeefficacy",
name="damage_type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="typeefficacy_damage_type",
to="pokemon_v2.Type",
),
),
migrations.AlterField(
model_name="typeefficacy",
name="target_type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="typeefficacy_target_type",
to="pokemon_v2.Type",
),
),
]

View file

@ -614,6 +614,30 @@ class HasType(models.Model):
abstract = True
class HasTypeEfficacy(models.Model):
damage_type = models.ForeignKey(
"Type",
blank=True,
null=True,
related_name="%(class)s_damage_type",
on_delete=models.CASCADE,
)
target_type = models.ForeignKey(
"Type",
blank=True,
null=True,
related_name="%(class)s_target_type",
on_delete=models.CASCADE,
)
damage_factor = models.IntegerField()
class Meta:
abstract = True
class HasVersion(models.Model):
version = models.ForeignKey(
@ -795,25 +819,13 @@ class TypeGameIndex(HasType, HasGeneration, HasGameIndex):
pass
class TypeEfficacy(models.Model):
class TypeEfficacy(HasTypeEfficacy):
pass
damage_type = models.ForeignKey(
"Type",
blank=True,
null=True,
related_name="damage_type",
on_delete=models.CASCADE,
)
target_type = models.ForeignKey(
"Type",
blank=True,
null=True,
related_name="target_type",
on_delete=models.CASCADE,
)
damage_factor = models.IntegerField()
# model for a type's efficacy that was used until a given generation
class TypeEfficacyPast(HasTypeEfficacy, HasGeneration):
pass
#################

View file

@ -1793,6 +1793,15 @@ class TypeEfficacySerializer(serializers.ModelSerializer):
fields = "__all__"
class TypeEfficacyPastSerializer(serializers.ModelSerializer):
generation = GenerationSummarySerializer()
class Meta:
model = TypeEfficacyPast
fields = ("target_type", "damage_type", "damage_factor", "generation")
class TypeGameIndexSerializer(serializers.ModelSerializer):
generation = GenerationSummarySerializer()
@ -1823,6 +1832,9 @@ class TypeDetailSerializer(serializers.ModelSerializer):
)
move_damage_class = MoveDamageClassSummarySerializer()
damage_relations = serializers.SerializerMethodField("get_type_relationships")
past_damage_relations = serializers.SerializerMethodField(
"get_type_past_relationships"
)
pokemon = serializers.SerializerMethodField("get_type_pokemon")
moves = MoveSummarySerializer(many=True, read_only=True, source="move")
@ -1832,6 +1844,7 @@ class TypeDetailSerializer(serializers.ModelSerializer):
"id",
"name",
"damage_relations",
"past_damage_relations",
"game_indices",
"generation",
"move_damage_class",
@ -1840,6 +1853,22 @@ class TypeDetailSerializer(serializers.ModelSerializer):
"moves",
)
# adds an entry for the given type with the given damage
# factor in the given direction to the set of relations
def add_type_entry(self, relations, type, damage_factor, direction="_damage_to"):
if damage_factor == 200:
relations["double" + direction].append(
TypeSummarySerializer(type, context=self.context).data
)
elif damage_factor == 50:
relations["half" + direction].append(
TypeSummarySerializer(type, context=self.context).data
)
elif damage_factor == 0:
relations["no" + direction].append(
TypeSummarySerializer(type, context=self.context).data
)
def get_type_relationships(self, obj):
relations = OrderedDict()
@ -1857,18 +1886,8 @@ class TypeDetailSerializer(serializers.ModelSerializer):
for relation in serializer.data:
type = Type.objects.get(pk=relation["target_type"])
if relation["damage_factor"] == 200:
relations["double_damage_to"].append(
TypeSummarySerializer(type, context=self.context).data
)
elif relation["damage_factor"] == 50:
relations["half_damage_to"].append(
TypeSummarySerializer(type, context=self.context).data
)
elif relation["damage_factor"] == 0:
relations["no_damage_to"].append(
TypeSummarySerializer(type, context=self.context).data
)
damage_factor = relation["damage_factor"]
self.add_type_entry(relations, type, damage_factor, direction="_damage_to")
# Damage From
results = TypeEfficacy.objects.filter(target_type=obj)
@ -1876,21 +1895,143 @@ class TypeDetailSerializer(serializers.ModelSerializer):
for relation in serializer.data:
type = Type.objects.get(pk=relation["damage_type"])
if relation["damage_factor"] == 200:
relations["double_damage_from"].append(
TypeSummarySerializer(type, context=self.context).data
)
elif relation["damage_factor"] == 50:
relations["half_damage_from"].append(
TypeSummarySerializer(type, context=self.context).data
)
elif relation["damage_factor"] == 0:
relations["no_damage_from"].append(
TypeSummarySerializer(type, context=self.context).data
)
damage_factor = relation["damage_factor"]
self.add_type_entry(
relations, type, damage_factor, direction="_damage_from"
)
return relations
# takes a list of past type relations by generation and
# returns a list of lists where each list has the entries
# for a single generation
def group_relations_by_generation(self, serializer_data):
data_by_gen = []
current_generation = ""
generation_data = []
for relation in serializer_data:
gen_name = relation["generation"]["name"]
if gen_name != current_generation:
# first item for this generation so create its list
current_generation = gen_name
generation_data = [relation]
data_by_gen.append(generation_data)
else:
# add to this generation's list
generation_data.append(relation)
return data_by_gen
# removes the entry for the given type in
# the given direction from the set of relations
def remove_type_entry(self, relations, type, direction="_damage_to"):
for k in ["double", "half", "no"]:
rel_list = relations[k + direction]
for i, o in enumerate(rel_list):
if o["name"] == type.name:
del rel_list[i]
return
# returns past type relationships for the given type object
def get_type_past_relationships(self, obj):
# collect data from DB
damage_type_results = list(TypeEfficacyPast.objects.filter(damage_type=obj))
target_type_results = list(TypeEfficacyPast.objects.filter(target_type=obj))
serializer = TypeEfficacyPastSerializer(
damage_type_results + target_type_results, many=True, context=self.context
)
# group data by generation
data_by_gen = self.group_relations_by_generation(serializer.data)
# process each generation's data in turn
final_data = []
past_relations = {}
for gen_data in data_by_gen:
# create past relations object for this generation
past_relations = OrderedDict()
# set generation
past_relations["generation"] = gen_data[0]["generation"]
# use current damage relations object
past_relations["damage_relations"] = self.get_type_relationships(obj)
relations = past_relations["damage_relations"]
current_gen = Generation.objects.get(name=gen_data[0]["generation"]["name"])
# remove types not yet introduced
# e.g. Poison has no effect on Steel, but Steel was not present in generation I
# so it should be absent from the list
relations["no_damage_to"] = self.remove_newer_types(
relations["no_damage_to"], current_gen
)
relations["half_damage_to"] = self.remove_newer_types(
relations["half_damage_to"], current_gen
)
relations["double_damage_to"] = self.remove_newer_types(
relations["double_damage_to"], current_gen
)
relations["no_damage_from"] = self.remove_newer_types(
relations["no_damage_from"], current_gen
)
relations["half_damage_from"] = self.remove_newer_types(
relations["half_damage_from"], current_gen
)
relations["double_damage_from"] = self.remove_newer_types(
relations["double_damage_from"], current_gen
)
# populate offensive relations
results = list(filter(lambda x: x["damage_type"] == obj.id, gen_data))
for relation in results:
type = Type.objects.get(pk=relation["target_type"])
# remove conflicting entry if it exists
self.remove_type_entry(relations, type, direction="_damage_to")
# add entry
damage_factor = relation["damage_factor"]
self.add_type_entry(
relations, type, damage_factor, direction="_damage_to"
)
del relation["generation"]
# populate defensive relations
results = list(filter(lambda x: x["target_type"] == obj.id, gen_data))
for relation in results:
type = Type.objects.get(pk=relation["damage_type"])
# remove conflicting entry if it exists
self.remove_type_entry(relations, type, direction="_damage_from")
# add entry
damage_factor = relation["damage_factor"]
self.add_type_entry(
relations, type, damage_factor, direction="_damage_from"
)
del relation["generation"]
# add to final list
final_data.append(past_relations)
return final_data
def remove_newer_types(self, relations, current_gen):
return list(filter(lambda x: self.type_is_present(x, current_gen), relations))
def type_is_present(self, type, current_gen):
type_obj = Type.objects.get(name=type["name"])
gen_introduced = Generation.objects.get(pk=type_obj.generation.id)
return gen_introduced.id <= current_gen.id
def get_type_pokemon(self, obj):
poke_type_objects = PokemonType.objects.filter(type=obj)

View file

@ -3420,12 +3420,30 @@ class APITests(APIData, APITestCase):
pokemon = self.setup_pokemon_data(name="pkmn for base tp")
pokemon_type = self.setup_pokemon_type_data(pokemon=pokemon, type=type)
no_damage_to = self.setup_type_data(name="no damage to tp")
half_damage_to = self.setup_type_data(name="half damage to tp")
double_damage_to = self.setup_type_data(name="double damage to tp")
no_damage_from = self.setup_type_data(name="no damage from tp")
half_damage_from = self.setup_type_data(name="half damage from tp")
double_damage_from = self.setup_type_data(name="double damage from tp")
generation = self.setup_generation_data(name="past gen")
no_damage_to = self.setup_type_data(
name="no damage to tp", generation=generation
)
half_damage_to = self.setup_type_data(
name="half damage to tp", generation=generation
)
double_damage_to = self.setup_type_data(
name="double damage to tp", generation=generation
)
no_damage_from = self.setup_type_data(
name="no damage from tp", generation=generation
)
half_damage_from = self.setup_type_data(
name="half damage from tp", generation=generation
)
double_damage_from = self.setup_type_data(
name="double damage from tp", generation=generation
)
newer_generation = self.setup_generation_data(name="newer_generation")
newer_type = self.setup_type_data(name="newer tp", generation=newer_generation)
# type relations
no_damage_to_relation = TypeEfficacy(
@ -3458,6 +3476,22 @@ class APITests(APIData, APITestCase):
)
double_damage_from_type_relation.save()
double_damage_from_newer_type_relation = TypeEfficacy(
damage_type=newer_type, target_type=type, damage_factor=200
)
double_damage_from_newer_type_relation.save()
# past type relations
# type used to deal half damage rather than no damage
past_no_damage_to_relation = TypeEfficacyPast(
damage_type=type,
target_type=no_damage_to,
damage_factor=50,
generation=generation,
)
past_no_damage_to_relation.save()
response = self.client.get("{}/type/{}/".format(API_V2, type.pk))
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -3552,6 +3586,75 @@ class APITests(APIData, APITestCase):
response.data["damage_relations"]["double_damage_from"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, double_damage_from.pk),
)
# past damage relations params
# generation
past_damage_relations = response.data["past_damage_relations"]
gen_data = past_damage_relations[0]["generation"]
self.assertEqual(gen_data["name"], generation.name)
self.assertEqual(
gen_data["url"],
"{}{}/generation/{}/".format(
TEST_HOST, API_V2, past_no_damage_to_relation.generation.pk
),
)
# relations
gen_relations = past_damage_relations[0]["damage_relations"]
# type that currently receives no damage used to receive half damage, so is no longer in
# this list...
self.assertEqual(len(gen_relations["no_damage_to"]), 0)
self.assertEqual(
gen_relations["half_damage_to"][0]["name"], half_damage_to.name
)
self.assertEqual(
gen_relations["half_damage_to"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, half_damage_to.pk),
)
# ...it's in this list instead
self.assertEqual(gen_relations["half_damage_to"][1]["name"], no_damage_to.name)
self.assertEqual(
gen_relations["half_damage_to"][1]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, no_damage_to.pk),
)
self.assertEqual(
gen_relations["double_damage_to"][0]["name"], double_damage_to.name
)
self.assertEqual(
gen_relations["double_damage_to"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, double_damage_to.pk),
)
self.assertEqual(
gen_relations["no_damage_from"][0]["name"], no_damage_from.name
)
self.assertEqual(
gen_relations["no_damage_from"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, no_damage_from.pk),
)
self.assertEqual(
gen_relations["half_damage_from"][0]["name"], half_damage_from.name
)
self.assertEqual(
gen_relations["half_damage_from"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, half_damage_from.pk),
)
self.assertEqual(
gen_relations["double_damage_from"][0]["name"], double_damage_from.name
)
self.assertEqual(
gen_relations["double_damage_from"][0]["url"],
"{}{}/type/{}/".format(TEST_HOST, API_V2, double_damage_from.pk),
)
# second double-damage-from type is absent because it's from a newer generation than the
# generation of this set of relations
self.assertEqual(len(gen_relations["double_damage_from"]), 1)
# game indices params
self.assertEqual(
response.data["game_indices"][0]["game_index"], type_game_index.game_index