diff --git a/src/apps/epic/migrations/0013_seed_minchiate_fiorentine_1860_1890.py b/src/apps/epic/migrations/0013_seed_minchiate_fiorentine_1860_1890.py new file mode 100644 index 0000000..8fe8deb --- /dev/null +++ b/src/apps/epic/migrations/0013_seed_minchiate_fiorentine_1860_1890.py @@ -0,0 +1,137 @@ +"""Sprint A.1 — seed the Minchiate Fiorentine (1860-1890) deck. + +97 cards: 40 numbered trumps + Il Matto (rank 0, unnumbered Fool) + 56 minors +(4 suits × 14 cards: pip 1-10, page 11, knight 12, queen 13, king 14). + +Names are stored in Italian-display form (Italian for trumps; English-rank + +Italian-suit hybrid for minors — "Page of Batons", "King of Coins"). The +canonical SUIT enum stays Earthman vocab (BRANDS/GRAILS/BLADES/CROWNS) per the +2026-05-25 lock; Sprint A.2's `display_suit_name` property handles the +canonical→display translation. + +Correspondences cite the RWS Tarot equivalent where one exists (16 trumps map +cleanly; the 5 popes, 4 theological+cardinal virtues, 4 elements, and 12 +zodiac trumps don't have RWS parallels — left blank). + +Keywords intentionally left empty for now; admin form (Sprint B) will enrich. +""" +from django.db import migrations + + +TRUMPS = [ + # (number, name, slug, correspondence) + (0, "Il Matto", "il-matto", "The Fool"), + (1, "Papa Uno", "papa-uno", ""), + (2, "Papa Due", "papa-due", ""), + (3, "Papa Tre", "papa-tre", ""), + (4, "Papa Quattro", "papa-quattro", ""), + (5, "Papa Cinque", "papa-cinque", ""), + (6, "La Temperanza", "la-temperanza", "Temperance"), + (7, "La Forza", "la-forza", "Strength"), + (8, "La Giustizia", "la-giustizia", "Justice"), + (9, "La Ruota della Fortuna", "la-ruota-della-fortuna", "Wheel of Fortune"), + (10, "Il Carro", "il-carro", "The Chariot"), + (11, "Il Gobbo", "il-gobbo", "The Hermit"), + (12, "L'Impiccato", "l-impiccato", "The Hanged Man"), + (13, "La Morte", "la-morte", "Death"), + (14, "Il Diavolo", "il-diavolo", "The Devil"), + (15, "La Casa del Diavolo", "la-casa-del-diavolo", "The Tower"), + (16, "La Speranza", "la-speranza", ""), # Hope — theological virtue + (17, "La Prudenza", "la-prudenza", ""), # Prudence — cardinal virtue + (18, "La Fede", "la-fede", ""), # Faith + (19, "La Carita", "la-carita", ""), # Charity + (20, "Il Fuoco", "il-fuoco", ""), # Fire — element + (21, "L'Acqua", "l-acqua", ""), # Water + (22, "La Terra", "la-terra", ""), # Earth + (23, "L'Aria", "l-aria", ""), # Air + (24, "La Bilancia", "la-bilancia", ""), # Libra — zodiac + (25, "La Vergine", "la-vergine", ""), # Virgo + (26, "Il Scorpione", "il-scorpione", ""), # Scorpio + (27, "L'Ariete", "l-ariete", ""), # Aries + (28, "Il Capricorno", "il-capricorno", ""), # Capricorn + (29, "Il Sagittario", "il-sagittario", ""), # Sagittarius + (30, "Il Cancro", "il-cancro", ""), # Cancer + (31, "I Pesci", "i-pesci", ""), # Pisces + (32, "L'Acquario", "l-acquario", ""), # Aquarius + (33, "Il Leone", "il-leone", ""), # Leo + (34, "Il Toro", "il-toro", ""), # Taurus + (35, "I Gemelli", "i-gemelli", ""), # Gemini + (36, "La Stella", "la-stella", "The Star"), + (37, "La Luna", "la-luna", "The Moon"), + (38, "Il Sole", "il-sole", "The Sun"), + (39, "Il Mondo", "il-mondo", "The World"), + (40, "Le Trombe", "le-trombe", "Judgement"), +] + +# Canonical Earthman suit enum → Italian-family display name (also used as slug stem). +SUIT_DISPLAY = { + "BRANDS": "Batons", + "GRAILS": "Cups", + "BLADES": "Swords", + "CROWNS": "Coins", +} +PIP_NAMES = {1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five", + 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten"} +COURT_NAMES = {11: "Page", 12: "Knight", 13: "Queen", 14: "King"} + + +def forward(apps, schema_editor): + DeckVariant = apps.get_model("epic", "DeckVariant") + TarotCard = apps.get_model("epic", "TarotCard") + + deck = DeckVariant.objects.create( + name="Minchiate Fiorentine (1860–1890)", + slug="minchiate-fiorentine-1860-1890", + card_count=97, + is_default=False, + family="italian", + has_card_images=True, + is_polarized=False, + description=( + "97-card Minchiate Fiorentine deck from the Baragioli-era 1860-1890 " + "Florence lithograph series. Five popes, four theological/cardinal " + "virtues, four elements, twelve zodiac signs, plus the standard " + "trump iconography and Il Matto." + ), + ) + + # 41 trumps (incl. Il Matto at rank 0). + for number, name, slug, correspondence in TRUMPS: + TarotCard.objects.create( + deck_variant=deck, + arcana="MAJOR", + suit=None, + number=number, + name=name, + slug=slug, + correspondence=correspondence, + ) + + # 56 minors: 4 suits × 14 cards. + for canonical_suit, display in SUIT_DISPLAY.items(): + for n in range(1, 15): + rank_word = PIP_NAMES.get(n) or COURT_NAMES[n] + TarotCard.objects.create( + deck_variant=deck, + arcana="MINOR", + suit=canonical_suit, + number=n, + name=f"{rank_word} of {display}", + slug=f"{rank_word.lower()}-of-{display.lower()}", + ) + + +def backward(apps, schema_editor): + DeckVariant = apps.get_model("epic", "DeckVariant") + TarotCard = apps.get_model("epic", "TarotCard") + deck = DeckVariant.objects.filter(slug="minchiate-fiorentine-1860-1890").first() + if deck: + TarotCard.objects.filter(deck_variant=deck).delete() + deck.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("epic", "0012_rws_rename_and_suit_revocab"), + ] + operations = [migrations.RunPython(forward, backward)] diff --git a/src/apps/epic/tests/integrated/test_models.py b/src/apps/epic/tests/integrated/test_models.py index 7068370..6145325 100644 --- a/src/apps/epic/tests/integrated/test_models.py +++ b/src/apps/epic/tests/integrated/test_models.py @@ -1037,3 +1037,115 @@ class DeckSchemaA0Test(TestCase): self.assertNotIn("CUPS", choice_values) self.assertNotIn("SWORDS", choice_values) self.assertNotIn("PENTACLES", choice_values) + + +class MinchiateFiorentine1860SeedTest(TestCase): + """Sprint A.1 — seed the actual Minchiate Fiorentine 1860-1890 deck. + 97 cards: 40 numbered trumps + Il Matto (rank 0) + 56 minors (4 suits × 14).""" + + DECK_SLUG = "minchiate-fiorentine-1860-1890" + + def setUp(self): + self.deck = DeckVariant.objects.get(slug=self.DECK_SLUG) + + def test_deck_exists_w_canonical_name(self): + self.assertEqual(self.deck.name, "Minchiate Fiorentine (1860–1890)") + + def test_deck_family_is_italian(self): + self.assertEqual(self.deck.family, "italian") + + def test_deck_has_card_images_true(self): + self.assertTrue(self.deck.has_card_images) + + def test_deck_is_polarized_false(self): + self.assertFalse(self.deck.is_polarized) + + def test_deck_card_count_97(self): + self.assertEqual(self.deck.card_count, 97) + + def test_total_card_rows_match_declared_count(self): + self.assertEqual( + TarotCard.objects.filter(deck_variant=self.deck).count(), 97 + ) + + def test_trump_count_is_41(self): + """40 numbered (1-40) + Il Matto (rank 0) = 41 trumps.""" + self.assertEqual( + TarotCard.objects.filter(deck_variant=self.deck, arcana="MAJOR").count(), + 41, + ) + + def test_il_matto_at_rank_0(self): + il_matto = TarotCard.objects.get(deck_variant=self.deck, slug="il-matto") + self.assertEqual(il_matto.arcana, "MAJOR") + self.assertEqual(il_matto.number, 0) + self.assertEqual(il_matto.name, "Il Matto") + + def test_il_gobbo_at_rank_11_w_hermit_correspondence(self): + il_gobbo = TarotCard.objects.get(deck_variant=self.deck, slug="il-gobbo") + self.assertEqual(il_gobbo.number, 11) + self.assertEqual(il_gobbo.correspondence, "The Hermit") + + def test_trump_40_is_le_trombe(self): + le_trombe = TarotCard.objects.get( + deck_variant=self.deck, arcana="MAJOR", number=40, + ) + self.assertEqual(le_trombe.name, "Le Trombe") + + def test_papa_uno_through_cinque_are_majors_1_through_5(self): + papas = list( + TarotCard.objects.filter(deck_variant=self.deck, arcana="MAJOR") + .filter(number__in=[1, 2, 3, 4, 5]) + .order_by("number") + .values_list("name", flat=True) + ) + self.assertEqual(papas, ["Papa Uno", "Papa Due", "Papa Tre", "Papa Quattro", "Papa Cinque"]) + + def test_minors_count_56_w_canonical_suits_only(self): + minors_per_suit = { + suit: TarotCard.objects.filter( + deck_variant=self.deck, arcana="MINOR", suit=suit, + ).count() + for suit in ("BRANDS", "GRAILS", "BLADES", "CROWNS") + } + self.assertEqual(minors_per_suit, {"BRANDS": 14, "GRAILS": 14, "BLADES": 14, "CROWNS": 14}) + + def test_no_minor_uses_dropped_english_suit(self): + bad_suits = set( + TarotCard.objects.filter(deck_variant=self.deck, arcana="MINOR") + .values_list("suit", flat=True) + .distinct() + ) - {"BRANDS", "GRAILS", "BLADES", "CROWNS"} + self.assertEqual(bad_suits, set()) + + def test_minor_court_ranks_11_through_14(self): + """Page=11, Knight=12, Queen=13, King=14 per [[reference-card-image-naming-convention]].""" + for suit in ("BRANDS", "GRAILS", "BLADES", "CROWNS"): + court_numbers = set( + TarotCard.objects.filter( + deck_variant=self.deck, arcana="MINOR", suit=suit, number__gte=11, + ).values_list("number", flat=True) + ) + self.assertEqual(court_numbers, {11, 12, 13, 14}, f"{suit} courts") + + def test_page_of_batons_exists(self): + """BRANDS is the canonical enum; Italian display 'Batons' lives in the card name.""" + card = TarotCard.objects.get( + deck_variant=self.deck, suit="BRANDS", number=11, + ) + self.assertEqual(card.name, "Page of Batons") + self.assertEqual(card.slug, "page-of-batons") + + def test_king_of_coins_exists(self): + """CROWNS canonical enum, 'Coins' Italian-family display.""" + card = TarotCard.objects.get( + deck_variant=self.deck, suit="CROWNS", number=14, + ) + self.assertEqual(card.name, "King of Coins") + + def test_no_middle_arcana(self): + """Minchiate has only MAJOR + MINOR; MIDDLE is Earthman-only.""" + self.assertEqual( + TarotCard.objects.filter(deck_variant=self.deck, arcana="MIDDLE").count(), + 0, + )