Skip to content

Commit

Permalink
Merge pull request #37 from ldeluigi/feature/mana-identity
Browse files Browse the repository at this point in the history
Add support for color identity of cards and combos
  • Loading branch information
ldeluigi authored Oct 2, 2022
2 parents d8d5879 + d58a79d commit c0f9a2c
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 29 deletions.
14 changes: 7 additions & 7 deletions backend/spellbook/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
class CardAdmin(admin.ModelAdmin):
fieldsets = [
('Spellbook', {'fields': ['name', 'features']}),
('Scryfall', {'fields': ['oracle_id']}),
('Scryfall', {'fields': ['oracle_id', 'identity']}),
]
# inlines = [FeatureInline]
search_fields = ['name', 'features__name']
autocomplete_fields = ['features']
list_display = ['name', 'id']
list_display = ['name', 'identity', 'id']


class CardInline(admin.StackedInline):
Expand Down Expand Up @@ -72,9 +72,9 @@ def clean_mana_needed(self):
@admin.register(Variant)
class VariantAdmin(admin.ModelAdmin):
form = VariantForm
readonly_fields = ['uses', 'produces', 'of', 'includes', 'unique_id']
readonly_fields = ['uses', 'produces', 'of', 'includes', 'unique_id', 'identity']
fieldsets = [
('Generated', {'fields': ['unique_id', 'uses', 'produces', 'of', 'includes']}),
('Generated', {'fields': ['unique_id', 'uses', 'produces', 'of', 'includes', 'identity']}),
('Editable', {'fields': [
'status',
'zone_locations',
Expand All @@ -84,9 +84,9 @@ class VariantAdmin(admin.ModelAdmin):
'description',
'frozen']})
]
list_filter = ['status']
list_display = ['__str__', 'status', 'id']
search_fields = ['id', 'uses__name', 'produces__name', 'unique_id']
list_filter = ['status', 'identity']
list_display = ['__str__', 'status', 'id', 'identity']
search_fields = ['id', 'uses__name', 'produces__name', 'unique_id', 'identity']
actions = [set_restore, set_draft, set_new, set_not_working]

def generate(self, request):
Expand Down
18 changes: 12 additions & 6 deletions backend/spellbook/management/commands/import_combos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from django.core.management.base import BaseCommand
from spellbook.variants import unique_id_from_cards_ids
from spellbook.variants import unique_id_from_cards_ids, merge_identities
from spellbook.models import Feature, Card, Job, Variant
from django.utils import timezone
from django.db.models import Count, Q
Expand Down Expand Up @@ -89,25 +89,30 @@ def handle(self, *args, **options):
except Card.DoesNotExist:
q = Card.objects.filter(name=data['name'])
if q.exists():
q.update(oracle_id=data['oracle_id'])
q.update(oracle_id=data['oracle_id'], identity=merge_identities(data['color_identity']))
else:
Card.objects.create(name=data['name'], oracle_id=data['oracle_id'])
Card.objects.create(name=data['name'], oracle_id=data['oracle_id'], identity=merge_identities(data['color_identity']))
self.stdout.write('Done fetching cards')
self.stdout.write('Importing combos...')
for i, (id, _cards, produced, prerequisite, description) in enumerate(x):
self.stdout.write(f'{i+1}/{len(x)}')
cards = [Card.objects.get(oracle_id=scryfall_db[card.lower()]['oracle_id']) for card in _cards]
already_present = Variant.objects.annotate(
total_cards=Count('includes'),
matching_cards=Count('includes', filter=Q(includes__in=cards)),
total_cards=Count('uses'),
matching_cards=Count('uses', filter=Q(uses__in=cards)),
).filter(
total_cards=len(cards),
matching_cards=len(cards),
)
if already_present.exists():
self.stdout.write(f'Skipping combo [{id}] {cards}: already present in variants')
continue
combo = Variant(other_prerequisites=prerequisite, description=description, frozen=True, status=Variant.Status.OK, unique_id=unique_id_from_cards_ids([c.id for c in cards]))
combo = Variant(other_prerequisites=prerequisite,
description=description,
frozen=True,
status=Variant.Status.OK,
unique_id=unique_id_from_cards_ids([c.id for c in cards]),
identity=merge_identities([c.identity for c in cards]))
combo.save()
combo.uses.set(cards)
for p in produced:
Expand All @@ -126,4 +131,5 @@ def handle(self, *args, **options):
job.status = Job.Status.FAILURE
job.message = f'Failed to import combos: {e}'
job.save()
print(e)
raise e
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.1 on 2022-10-01 22:24

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('spellbook', '0002_alter_combo_cards_state_alter_combo_description_and_more'),
]

operations = [
migrations.AddField(
model_name='card',
name='identity',
field=models.CharField(blank=True, help_text='Card mana identity', max_length=5, validators=[django.core.validators.RegexValidator(message='Can be any combination of zero or more letters in [W,U,B,R,G], in order.', regex='^W?U?B?R?G?$')], verbose_name='mana identity of card'),
),
migrations.AddField(
model_name='variant',
name='identity',
field=models.CharField(blank=True, editable=False, help_text='Mana identity', max_length=5, validators=[django.core.validators.RegexValidator(message='Can be any combination of zero or more letters in [W,U,B,R,G], in order.', regex='^W?U?B?R?G?$')], verbose_name='mana identity'),
),
migrations.AlterField(
model_name='variant',
name='description',
field=models.TextField(blank=True, help_text='Long description, in steps', validators=[django.core.validators.RegexValidator(message='Unpaired double square brackets are not allowed.', regex='^(?:[^\\[]*(?:\\[(?!\\[)|\\[{2}[^\\[]+\\]{2}|\\[{3,}))*[^\\[]*$'), django.core.validators.RegexValidator(message='Symbols must be in the {1}{W}{U}{B}{R}{G}{B/P}{A}{E}{T}{Q}... format.', regex='^(?:[^\\{]*\\{(?:[0-9WUBRGCPXYZSTQEA½∞]|PW|CHAOS|TK|[1-9][0-9]{1,2}|H[WUBRG]|(?:2\\/[WUBRG]|W\\/U|W\\/B|B\\/R|B\\/G|U\\/B|U\\/R|R\\/G|R\\/W|G\\/W|G\\/U)(?:\\/P)?)\\})*[^\\{]*$')]),
),
]
6 changes: 4 additions & 2 deletions backend/spellbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.db import OperationalError, models, transaction
from sortedm2m.fields import SortedManyToManyField
from django.contrib.auth.models import User
from .symbols import MANA_VALIDATOR, TEXT_VALIDATORS
from .symbols import MANA_VALIDATOR, TEXT_VALIDATORS, IDENTITY_VALIDATOR


class Feature(models.Model):
Expand All @@ -29,6 +29,7 @@ class Card(models.Model):
related_name='cards',
help_text='Features provided by this single card effects or characteristics',
blank=True)
identity = models.CharField(max_length=5, blank=True, help_text='Card mana identity', verbose_name='mana identity of card', validators=[IDENTITY_VALIDATOR])
added = models.DateTimeField(auto_now_add=True, editable=False)
updated = models.DateTimeField(auto_now=True, editable=False)

Expand Down Expand Up @@ -123,11 +124,12 @@ class Status(models.TextChoices):
cards_state = models.TextField(blank=True, default='', help_text='State of cards in their starting locations.', validators=TEXT_VALIDATORS, verbose_name='starting cards state')
mana_needed = models.CharField(blank=True, max_length=200, default='', help_text='Mana needed for this combo. Use the {1}{W}{U}{B}{R}{G}{B/P}... format.', validators=[MANA_VALIDATOR])
other_prerequisites = models.TextField(blank=True, default='', help_text='Other prerequisites for this variant.', validators=TEXT_VALIDATORS)
description = models.TextField(blank=True, help_text='Long description of the variant, in steps', validators=TEXT_VALIDATORS)
description = models.TextField(blank=True, help_text='Long description, in steps', validators=TEXT_VALIDATORS)
created = models.DateTimeField(auto_now_add=True, editable=False)
updated = models.DateTimeField(auto_now=True, editable=False)
unique_id = models.CharField(max_length=128, unique=True, blank=False, help_text='Unique ID for this variant', editable=False)
frozen = models.BooleanField(default=False, blank=False, help_text='Is this variant undeletable?', verbose_name='is frozen')
identity = models.CharField(max_length=5, blank=True, help_text='Mana identity', verbose_name='mana identity', editable=False, validators=[IDENTITY_VALIDATOR])

class Meta:
ordering = ['-status', '-created']
Expand Down
5 changes: 3 additions & 2 deletions backend/spellbook/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
class CardSerializer(serializers.ModelSerializer):
class Meta:
model = Card
fields = ['id', 'name', 'oracle_id']
fields = ['id', 'name', 'oracle_id', 'identity']


class FeatureSerializer(serializers.ModelSerializer):
Expand All @@ -19,7 +19,7 @@ class CardDetailSerializer(serializers.ModelSerializer):

class Meta:
model = Card
fields = ['id', 'name', 'oracle_id', 'features']
fields = ['id', 'name', 'oracle_id', 'identity', 'features']


class ComboSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -56,6 +56,7 @@ class Meta:
'produces',
'of',
'includes',
'identity',
'zone_locations',
'cards_state',
'mana_needed',
Expand Down
2 changes: 2 additions & 0 deletions backend/spellbook/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
SYMBOLS_TEXT_REGEX = r'^(?:[^\{]*\{(?:[0-9WUBRGCPXYZSTQEA½∞]|PW|CHAOS|TK|[1-9][0-9]{1,2}|H[WUBRG]|(?:2\/[WUBRG]|W\/U|W\/B|B\/R|B\/G|U\/B|U\/R|R\/G|R\/W|G\/W|G\/U)(?:\/P)?)\})*[^\{]*$'
SYMBOLS_TEXT_VALIDATOR = RegexValidator(regex=SYMBOLS_TEXT_REGEX, message='Symbols must be in the {1}{W}{U}{B}{R}{G}{B/P}{A}{E}{T}{Q}... format.')
TEXT_VALIDATORS = [DOUBLE_SQUARE_BRACKET_TEXT_VALIDATOR, SYMBOLS_TEXT_VALIDATOR]
IDENTITY_REGEX = r'^W?U?B?R?G?$'
IDENTITY_VALIDATOR = RegexValidator(regex=IDENTITY_REGEX, message='Can be any combination of zero or more letters in [W,U,B,R,G], in order.')
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
success: function(data) {
let oracle_id = data.oracle_id;
$('#id_oracle_id').val(oracle_id);
let color_identity = Array.from(new Set(Array.from(data.color_identity).map(i => i.toUpperCase()))).sort().join('');
$('#id_identity').val(color_identity);
}
})
},
Expand Down
25 changes: 14 additions & 11 deletions backend/spellbook/variants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import hashlib
import logging
from typing import Iterable
import pyomo.environ as pyo
from dataclasses import dataclass
from itertools import starmap
Expand Down Expand Up @@ -61,6 +62,11 @@ def removed_features(variant: Variant, features: set[int]) -> set[int]:
return features - set(variant.includes.values_list('removes__id', flat=True))


def merge_identities(identities: Iterable[str]):
i = set(''.join(identities).upper())
return ''.join([color for color in 'WUBRG' if color in i])


def update_variant(
data: Data,
unique_id: str,
Expand All @@ -73,18 +79,14 @@ def update_variant(
variant.of.set(combos_that_generated)
variant.includes.set(combos_included)
variant.produces.set(removed_features(variant, features) - data.utility_features_ids)
variant.identity = merge_identities(variant.uses.values_list('identity', flat=True))
if restore:
combos = data.combos.filter(id__in=combos_included)
zone_locations = '\n'.join(c.zone_locations for c in combos if len(c.zone_locations) > 0)
cards_state = '\n'.join(c.cards_state for c in combos if len(c.cards_state) > 0)
other_prerequisites = '\n'.join(c.other_prerequisites for c in combos if len(c.other_prerequisites) > 0)
mana_needed = ' '.join(c.mana_needed for c in combos if len(c.mana_needed) > 0)
description = '\n'.join(c.description for c in combos if len(c.description) > 0)
variant.zone_locations = zone_locations
variant.cards_state = cards_state
variant.other_prerequisites = other_prerequisites
variant.mana_needed = mana_needed
variant.description = description
variant.zone_locations = '\n'.join(c.zone_locations for c in combos if len(c.zone_locations) > 0)
variant.cards_state = '\n'.join(c.cards_state for c in combos if len(c.cards_state) > 0)
variant.other_prerequisites = '\n'.join(c.other_prerequisites for c in combos if len(c.other_prerequisites) > 0)
variant.mana_needed = ' '.join(c.mana_needed for c in combos if len(c.mana_needed) > 0)
variant.description = '\n'.join(c.description for c in combos if len(c.description) > 0)
variant.status = Variant.Status.NEW if ok else Variant.Status.NOT_WORKING
if not ok:
variant.status = Variant.Status.NOT_WORKING
Expand Down Expand Up @@ -113,7 +115,8 @@ def create_variant(
cards_state=cards_state,
other_prerequisites=other_prerequisites,
mana_needed=mana_needed,
description=description)
description=description,
identity=merge_identities(data.cards.filter(id__in=cards).values_list('identity', flat=True)))
if not ok:
variant.status = Variant.Status.NOT_WORKING
variant.save()
Expand Down
3 changes: 2 additions & 1 deletion backend/spellbook/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class VariantViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Variant.objects.filter(status=Variant.Status.OK)
serializer_class = VariantSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['unique_id', 'uses__id', 'includes__id', 'produces__id', 'of__id']
filterset_fields = ['unique_id', 'uses__id', 'includes__id', 'produces__id', 'of__id', 'identity']
search_fields = ['uses__name', 'produces__name']
ordering_fields = ['created', 'updated', 'unique_id']

Expand Down Expand Up @@ -40,6 +40,7 @@ class ComboViewSet(viewsets.ReadOnlyModelViewSet):
class CardViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Card.objects.all()
serializer_class = CardDetailSerializer
filterset_fields = ['oracle_id', 'identity']


card_list = CardViewSet.as_view({'get': 'list'})
Expand Down

0 comments on commit c0f9a2c

Please sign in to comment.