Cards - emkarcinos/WMIAdventure GitHub Wiki

Moduł przeznaczony na karty zlokalizowany w folderze cards. Nie ma tutaj logiki biznesowej (na dzień dzisiejszy), jest tylko model bazy danych i ORM w Django.

Karty to "przedmioty", których gracze używają do pojedynków między sobą. Istnieją też karty kolekcjonerskie (TODO) służące jako achievementy w aplikacji. Tworzone są w specjalnym Edytorze Kart. Wszystkie requesty dotyczące kart trafiają do klas widoków właśnie w tym module.

Diagram bazy

CardInfo

Encja przechowująca podstawowe informacje o karcie w prostej formie.

  • name - Wyświetlana nazwa karty
  • tooltip - Krótki opis karty
  • subject - Przedmiot którego dotyczy karta - Na razie jest to String, w przyszłości powinien się tu pojawić klucz obcy na faktyczną encję z przedmiotami, które obecnie nie są zaimplementowane
  • image - obraz w karcie - na razie nie używane nigdzie w systemie.

CardEffect

Ogólne efekty kart. Każdy wpis w tabeli reprezentuje jeden typ efektu. Każda karta może mieć wiele takich efektów jednocześnie. Modyfikacja ID lub nazwy efektu nie powinna być wykonywana w bazie czy aktualizowana za pomocą requestów, w models.py/CardEffect zdefiniowany jest specjalny enumerate przeznaczony na wiązanie nazwy efektów z jego identyfikatorami. Pomaga to przy pracach programistycznych.

  • name - Nazwa efektu
  • tooltip - Krótki opis efektu
  • has_modifier - Pole boolowskie mówiące o tym, czy dany efekt może być modyfikowany przez mnożniki. Przykładem efektu, który będzie miał to pole jako true może być efekt zadający obrażenia, a false znajdziemy przy efekcie pomijania następnej karty.

CardLevel

Lista dostępnych poziomów dla kart. Właściwie jest to po prostu tabela słownikowa - poziomy są konkretnie nazwane, jeżeli chcemy je modyfikować to właśnie tutaj. Podobnie jak w CardEffects, w models/CardLevel zdefiniowany jest specjalny enumerate i najlepiej tam wprowadzać zmiany.

  • level - numer poziomu
  • name - wyświetlana nazwa

Card

Zrozumienie idei tej encji może być nieco skomplikowane. Każda karta którą stworzymy będzie miała jeden opis, wiele poziomów i na każdym z tych poziomów wiele efektów. Widzimy, że Card ma pola level i effects. Połączenie wielu poziomów z wieloma efektami jest skomplikowane i zostało rozwiązane w ten sposób, że dla każdego jednego poziomu w jednej karcie tworzymy nowy wpis w tej tabeli. Zatem tworząc kartę, która ma trzy poziomy, tworzymy jeden CardInfo, i dla każdego poziomu tworzymy Card z odnośnikiem do CardInfo i numerem poziomu. Efekty zostaną dodane w innej encji - CardLevelEffects

  • info - klucz obcy na CardInfo
  • level - klucz obcy na CardLevel
  • next_level_cost - koszt ulepszenia karty na kolejny poziom jako *int

CardLevelEffects

Ta encja może być rozumiana jako rozszerzone-wiele-do-wielu, tj. wiążemy efekty z encją Card i nadajemy im jakieś parametry. Mówi nam zatem o tym jakie efekty występują na konkretnym poziomie danej karty i o ich parametrach.

  • card - klucz obcy na Card
  • card_effect - klucz obcy na CardEffect
  • target - cel, na który ten efekt zadziała - baza jest ograniczona na dwie wartości - 1, jako cel na gracza i 2 jako cel na przeciwnika.
  • power - moc efektu
  • range - 'losowość' efektu - ostateczna wartość efektu będzie z przedziału (power - range), (power + range) - więcej w Battle - Wyliczanie mocy efektu

Szczegóły implementacji niektórych modeli w Django

Niektóre modele zostały stworzone przy użyciu dość nietypowego mechanizmu tworzenia abstrakcyjnego modelu w osobnej funkcji. Motywacją do użycia tego mechanizmu jest ponowne użycie kodu, wykorzystane w w module proposed_content.

BaseCardInfo

def base_card_info_factory(upload_images_to: str):
    class BaseCardInfo(models.Model):

        class Meta:
            abstract = True

        name = models.CharField(max_length=50, help_text="Displayed card's name.")
        tooltip = models.TextField(help_text="Card's description. Gets displayed together with the card as a tooltip.")
        image = models.ImageField(upload_to=upload_images_to, null=True, blank=True,
                                  help_text="An image. We don't really"
                                            "know what should that be.")
        subject = models.CharField(max_length=50, null=True,
                                   help_text="Subject name. In the future this field will be an"
                                             " id pointing to Subject object.")

    return BaseCardInfo

Abstrakcyjny model BaseCardInfo jest tworzony w funkcji. Przechowuje on podstawowe informacje o danej karcie, jak nazwa, opis, itp.

Dlaczego po prostu nie zrobić modelu BaseCardInfo, bez żadnych funkcji? Tworzenie tego modelu w funkcji umożliwia przekazanie parametru upload_images_to, który ustala gdzie przechowywać obrazy kart.

Przechowywanie obrazów dla kart proponowanych powinno być w innym miejscu niż kart, które są zaakceptowane i używane w systemie, dzięki przekazywaniu parametru upload_images_to do funkcji jest to możliwe.

CardInfo

class CardInfo(base_card_info_factory('cards/images/'))

Model CardInfo przechowuje podstawowe informacje o danej karcie.

Dziedziczy wszystkie pola z abstrakcyjnego modelu BaseCardInfo tworzonego w funkcji base_card_info_factory.

BaseCard

def base_card_factory(related_card_info_class: type):
    class BaseCard(models.Model):

        info = models.ForeignKey(related_card_info_class, related_name='levels', unique=False, on_delete=models.CASCADE)
        level = models.ForeignKey(CardLevel, unique=False, on_delete=models.CASCADE)
        next_level_cost = models.IntegerField(null=True, validators=[MinValueValidator(0),
                                                                     MaxValueValidator(100)])

        class Meta:
            abstract = True

            """
            This makes (info, level) unique
            """
            constraints = [
                models.UniqueConstraint(fields=['info', 'level'],
                                        name=f'unique_{related_card_info_class.__name__}_level')
            ]

    return BaseCard

Abstrakcyjny model BaseCard przechowuje informacje o konkretnym poziomie danej karty.

Tworzony w funkcji base_card_factory. Tworzenie go w funkcji umożliwia przekazanie parametru related_card_info_class, który ustala z jakim typem card info powiązany jest dany model card.

(typy card info: CardInfo, ProposedCardInfo)

Po co przekazywać related_card_info_class?

  • Model Card powinien być powiązany z CardInfo
  • Model ProposedCard być powiązany z modelem ProposedCardInfo, nie z CardInfo. Ustawianie odpowiednich powiązań jest możliwe dzięki related_card_info_class.

Card

class Card(base_card_factory(CardInfo))

Model Card przechowuje informacje o konkretnym poziomie danej karty

Card dziedziczy wszystkie pola z abstrakcyjnego modelu BaseCard tworzonego w funkcji base_card_factory.

BaseCardLevelEffects

def base_card_level_effects_factory(foreignkey_card_cls: type):
    class BaseCardLevelEffects(models.Model):

        class Meta:
            abstract = True

        class Target(models.IntegerChoices):
            """
            Possible targets.
            """
            PLAYER = 1
            OPPONENT = 2

        card = models.ForeignKey(foreignkey_card_cls, related_name='effects', unique=False, on_delete=models.CASCADE)
        card_effect = models.ForeignKey(CardEffect, unique=False, on_delete=models.CASCADE)
        # This isn't unique even as a pair with card, as a single card on a given level '
        # may have multiple of the same effect.
        target = models.IntegerField(choices=Target.choices, default=Target.OPPONENT)
        power = models.IntegerField(null=True, validators=[MinValueValidator(0),
                                                           MaxValueValidator(100)])
        # Range defines how the power attribute will vary in card logic.
        # So an actual power will be randomized from range (power - range, power + range)
        range = models.FloatField(null=True, validators=[MinValueValidator(0),
                                                         MaxValueValidator(100)])

    return BaseCardLevelEffects

Abstrakcyjny model BaseCardLevelEffects definiuje pola przechowujące informacje o efektach na danym poziomie karty.

Jest tworzony w funkcji base_card_level_effects_factory. Tworzenie go w funkcji umożliwia przekazanie parametru foreignkey_card_cls, który ustala z jakim typem card powiązany jest dany model card level effects.

(typy card: Card, ProposedCard)

Po co przekazywać foreignkey_card_cls?

  • Model CardLevelEffects powinien być powiązany z Card
  • Model ProposedCardLevelEffects być powiązany z modelem ProposedCard. Ustawianie odpowiednich powiązań jest możliwe dzięki related_card_info_class.

CardLevelEffects

class CardLevelEffects(base_card_level_effects_factory(Card))

Model CardLevelEffects przechowuje informacje o efektach na danym poziomie karty.

CardLevelEffects dziedziczy wszystkie pola z abstrakcyjnego modelu BaseCardLevelEffects tworzonego w funkcji base_card_level_effects_factory.

Szczegóły implementacji niektórych serializerów

Analogicznie do niektórych modeli część serializerów dziedziczy z bazowych serializerów zagnieżdżonych w funkcji. Motywacją do użycia tego mechanizmu tworzenia bazowych serializerów jest ponowne użycie kodu, wykorzystane w w module proposed_content.

By lepiej zrozumieć ten mechanizm warto przeczytać tę sekcję.

BaseSimpleCardLevelEffectsSerializer

def base_simple_card_lvl_efcts_ser_factory(card_level_effects_model: type):

    class BaseSimpleCardLevelEffectsSerializer(serializers.ModelSerializer):
        class Meta:
            model = card_level_effects_model
            fields = ['card_effect', 'target', 'power', 'range']

    return BaseSimpleCardLevelEffectsSerializer

Parametr card_level_effects_model określa, jakiego typu będzie serializowany obiekt. (Możliwe typy: CardLevelEffects, ProposedCardLevelEffects)

SimpleCardLevelEffectsSerializer

class SimpleCardLevelEffectsSerializer(base_simple_card_lvl_efcts_ser_factory(CardLevelEffects)):

Dziedziczy wszystkie reguły serializacji z BaseSimpleCardLevelEffectsSerializer.

BaseSimpleCardSerializer

def base_simple_card_serializer_factory(card_model: type, simple_card_level_effects_ser: type):
    class BaseSimpleCardSerializer(serializers.ModelSerializer):
        effects = simple_card_level_effects_ser(many=True)

        class Meta:
            model = card_model
            fields = ['level', 'next_level_cost', 'effects']

    return BaseSimpleCardSerializer

Parametr card_model określa, jakiego typu będzie serializowany obiekt.

(Możliwe typy: Card, ProposedCard)

Parametr simple_card_level_effects_ser podaje typ serializera serializującego konkretne obiekty card level effects.

(Możliwe typy serializerów: SimpleCardLevelEffectsSerializer, SimpleProposedCardLevelEffectsSerializer)

SimpleCardSerializer

class SimpleCardSerializer(base_simple_card_serializer_factory(Card, SimpleCardLevelEffectsSerializer)):
    """
    Managing simple serialization of given card model.

    See: BaseSimpleCardSerializer which is inner class in base_simple_card_serializer_factory function.
    """

Dziedziczy wszystkie reguły serializacji z BaseSimpleCardSerializer.

BaseWholeCardSerializer

def base_whole_card_serializer_factory(card_info_cls: type, simple_card_ser: type):
    class BaseWholeCardSerializer(serializers.ModelSerializer):
        """
        (De)Serializes Card as a whole, packs all the information scattered across many models in one serializer.
        Information like:
        - card name
        - card image
        - card tooltip
        - all possible card's levels
        - all effects on given card's level
        etc.
        """

        levels = simple_card_ser(many=True, required=False, help_text="An array of levels objects.")
        subject = serializers.CharField(max_length=50, allow_null=True,
                                        help_text="Subject name. In the future this field"
                                                  "will be an id pointing to Subject "
                                                  "object.")

        class Meta:
            model = card_info_cls
            fields = ['id', 'name', 'subject', 'image', 'tooltip', 'levels']

        def validate(self, attrs):
            ...

        def create(self, validated_data):
            ...

        def update(self, instance, validated_data):
            ...

        def _validate_levels_provided(self, validated_data):
            ...

        def _before_create_validation(self, validated_data):
            ...

    return BaseWholeCardSerializer

Określa reguły (de)serializacji karty jako całości - agreguje wszystkie informacje rozsiane po wielu modelach.

Parametry funkcji base_whole_card_serializer_factory:

WholeCardSerializer

class WholeCardSerializer(base_whole_card_serializer_factory(CardInfo, SimpleCardSerializer)):
    """
    (De)Serializes Card as a whole, packs all the information scattered across many models in one serializer.
    Information like:
    - card name
    - card image
    - card tooltip
    - all possible card's levels
    - all effects on given card's level
    etc.

    See: base_whole_card_serializer_factory
    """

Dziedziczy wszystkie reguły serializacji z BaseWholeCardSerializer.