From a4ac25605d25467c08d94f7eac2d85fdd0a99dee Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Sun, 24 May 2026 23:32:19 -0400 Subject: [PATCH] =?UTF-8?q?A.1=20seed=20Minchiate=20Fiorentine=201860-1890?= =?UTF-8?q?=20deck=20(97=20cards)=20=E2=80=94=20TDD.=20Sprint=20A.1=20foll?= =?UTF-8?q?ow-up=20to=20[[project-image-based-deck-face-rendering]].=20Cre?= =?UTF-8?q?ates=20the=20actual=20Minchiate=20Fiorentine=20`DeckVariant`=20?= =?UTF-8?q?(separate=20from=20the=20renamed-from-fiorentine-minchiate=20RW?= =?UTF-8?q?S=20Tarot=20now=20living=20at=20`tarot-rider-waite-smith`=20per?= =?UTF-8?q?=20A.0).=20Slug=20`minchiate-fiorentine-1860-1890`=20matches=20?= =?UTF-8?q?the=20asset=20dir=20committed=20in=200add163=20(98=20PNGs=20at?= =?UTF-8?q?=20`src/apps/epic/static/apps/epic/images/cards-faces/minchiate?= =?UTF-8?q?-fiorentine-1860-1890/`);=20Sprint=20A.2's=20`image=5Ffilename`?= =?UTF-8?q?=20property=20will=20use=20`deck.slug`=20to=20point=20at=20thos?= =?UTF-8?q?e=20images.=20Schema=20fields=20set:=20`family=3D'italian'`=20(?= =?UTF-8?q?drives=20display=20+=20filename=20slug=20mapping=20per=20[[refe?= =?UTF-8?q?rence-card-image-naming-convention]]=20=E2=80=94=20BRANDS?= =?UTF-8?q?=E2=86=92batons,=20CROWNS=E2=86=92coins,=20GRAILS=E2=86=92cups,?= =?UTF-8?q?=20BLADES=E2=86=92swords),=20`has=5Fcard=5Fimages=3DTrue`=20(fi?= =?UTF-8?q?rst=20deck=20w.=20images=20shipped),=20`is=5Fpolarized=3DFalse`?= =?UTF-8?q?=20(Earthman=20remains=20the=20only=20polarized=20deck),=20`is?= =?UTF-8?q?=5Fdefault=3DFalse`=20(Earthman=20is=20default),=20`card=5Fcoun?= =?UTF-8?q?t=3D97`.=2097=20TarotCard=20rows=20seeded:=2041=20trumps=20(Il?= =?UTF-8?q?=20Matto=20at=20rank=200=20per=20the=20unnumbered-Fool-gets-sor?= =?UTF-8?q?table-position=20convention=20from=20[[reference-card-image-nam?= =?UTF-8?q?ing-convention]];=20then=2040=20numbered=201-40)=20+=2056=20min?= =?UTF-8?q?ors=20(4=20suits=20=C3=97=2014=20cards=20=3D=20pip=201-10=20+?= =?UTF-8?q?=20page=3D11=20+=20knight=3D12=20+=20queen=3D13=20+=20king=3D14?= =?UTF-8?q?=20per=20the=20v2=20convention's=20number-prefixed-courts=20dec?= =?UTF-8?q?ision).=20Trump=20names=20are=20Italian=20(Papa=20Uno=20/=20Pap?= =?UTF-8?q?a=20Due=20/=20La=20Temperanza=20/=20La=20Forza=20/=20La=20Giust?= =?UTF-8?q?izia=20/=20La=20Ruota=20della=20Fortuna=20/=20Il=20Carro=20/=20?= =?UTF-8?q?Il=20Gobbo=20/=20L'Impiccato=20/=20La=20Morte=20/=20Il=20Diavol?= =?UTF-8?q?o=20/=20La=20Casa=20del=20Diavolo=20/=20La=20Speranza=20/=20La?= =?UTF-8?q?=20Prudenza=20/=20La=20Fede=20/=20La=20Carita=20/=20Il=20Fuoco?= =?UTF-8?q?=20/=20L'Acqua=20/=20La=20Terra=20/=20L'Aria=20+=2012=20zodiac?= =?UTF-8?q?=20signs=20+=20La=20Stella=20/=20La=20Luna=20/=20Il=20Sole=20/?= =?UTF-8?q?=20Il=20Mondo=20/=20Le=20Trombe).=20Card-suit=20canonical=20enu?= =?UTF-8?q?m=20stays=20BRANDS/CROWNS/GRAILS/BLADES=20per=20A.0's=20lock;?= =?UTF-8?q?=20minor=20card=20NAMES=20use=20Italian-family=20display=20voca?= =?UTF-8?q?b=20("Page=20of=20Batons"=20not=20"Page=20of=20Brands")=20since?= =?UTF-8?q?=20names=20are=20the=20user-facing=20label=20whereas=20suit=20i?= =?UTF-8?q?s=20the=20structural=20identity.=2016=20trumps=20carry=20a=20`c?= =?UTF-8?q?orrespondence`=20field=20pointing=20to=20their=20RWS=20Tarot=20?= =?UTF-8?q?equivalent=20(Il=20Matto=E2=86=92The=20Fool,=20Il=20Carro?= =?UTF-8?q?=E2=86=92The=20Chariot,=20Il=20Gobbo=E2=86=92The=20Hermit,=20L'?= =?UTF-8?q?Impiccato=E2=86=92The=20Hanged=20Man,=20La=20Morte=E2=86=92Deat?= =?UTF-8?q?h,=20Il=20Diavolo=E2=86=92The=20Devil,=20La=20Casa=20del=20Diav?= =?UTF-8?q?olo=E2=86=92The=20Tower,=20La=20Temperanza=E2=86=92Temperance,?= =?UTF-8?q?=20La=20Forza=E2=86=92Strength,=20La=20Giustizia=E2=86=92Justic?= =?UTF-8?q?e,=20La=20Ruota=20della=20Fortuna=E2=86=92Wheel=20of=20Fortune,?= =?UTF-8?q?=20La=20Stella=E2=86=92The=20Star,=20La=20Luna=E2=86=92The=20Mo?= =?UTF-8?q?on,=20Il=20Sole=E2=86=92The=20Sun,=20Il=20Mondo=E2=86=92The=20W?= =?UTF-8?q?orld,=20Le=20Trombe=E2=86=92Judgement);=20the=2025=20Minchiate-?= =?UTF-8?q?only=20trumps=20(5=20popes=20+=204=20theological/cardinal=20vir?= =?UTF-8?q?tues=20+=204=20elements=20+=2012=20zodiac)=20have=20no=20RWS=20?= =?UTF-8?q?parallel=20=E2=86=92=20empty=20correspondence.=20`keywords=5Fup?= =?UTF-8?q?right`=20/=20`keywords=5Freversed`=20intentionally=20left=20emp?= =?UTF-8?q?ty=20`[]`:=20those=20are=20interpretive=20content=20the=20user?= =?UTF-8?q?=20owns;=20admin=20form=20(Sprint=20B)=20will=20enrich=20via=20?= =?UTF-8?q?UI=20rather=20than=20have=20them=20committed=20as=20code=20in?= =?UTF-8?q?=20a=20migration.=20Five=20trumps=20in=20the=20v2=20filename=20?= =?UTF-8?q?convention=20have=20elided-apostrophe=20slugs=20restored=20(l-i?= =?UTF-8?q?mpiccato,=20l-acqua,=20l-aria,=20l-ariete,=20l-acquario);=20DB?= =?UTF-8?q?=20slug=20field=20matches=20(no=20apostrophe,=20but=20with=20th?= =?UTF-8?q?e=20leading=20`l-`=20prefix).=2017=20new=20ITs=20in=20`Minchiat?= =?UTF-8?q?eFiorentine1860SeedTest`=20cover=20the=20deck=20attributes=20(n?= =?UTF-8?q?ame=20+=20family=20+=20has=5Fcard=5Fimages=20+=20is=5Fpolarized?= =?UTF-8?q?=20+=20card=5Fcount)=20+=20total=20row=20count=20(97)=20+=20arc?= =?UTF-8?q?ana=20breakdown=20(41=20trumps=20+=2056=20minors=20+=200=20midd?= =?UTF-8?q?le)=20+=20specific=20cards=20(Il=20Matto=20at=20rank=200=20+=20?= =?UTF-8?q?Il=20Gobbo=20at=20rank=2011=20w.=20correspondence=20"The=20Herm?= =?UTF-8?q?it"=20+=20Le=20Trombe=20at=20rank=2040=20+=205=20popes=20are=20?= =?UTF-8?q?MAJOR=20ranks=201-5=20+=20Page=20of=20Batons=20+=20King=20of=20?= =?UTF-8?q?Coins)=20+=20canonical-suit-only=20check=20(no=20WANDS/CUPS/SWO?= =?UTF-8?q?RDS/PENTACLES=20in=20DB)=20+=20court=20rank=20range=20(11-14=20?= =?UTF-8?q?per=20suit).=20Tests:=2017=20new=20green;=201272/1272=20IT+UT?= =?UTF-8?q?=20total=20green=20(64s;=20+17=20from=20A.0's=201255).=20Out=20?= =?UTF-8?q?of=20scope:=20A.2=20adds=20the=20`TarotCard.image=5Ffilename`?= =?UTF-8?q?=20+=20`display=5Fsuit=5Fname`=20properties=20consuming=20`deck?= =?UTF-8?q?.family`=20for=20per-family=20translation;=20A.3=20wires=20my?= =?UTF-8?q?=5Fsign.html's=20first=20render=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- ...013_seed_minchiate_fiorentine_1860_1890.py | 137 ++++++++++++++++++ src/apps/epic/tests/integrated/test_models.py | 112 ++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 src/apps/epic/migrations/0013_seed_minchiate_fiorentine_1860_1890.py 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, + )