A.1 seed Minchiate Fiorentine 1860-1890 deck (97 cards) — TDD. Sprint A.1 follow-up to [[project-image-based-deck-face-rendering]]. Creates the actual Minchiate Fiorentine DeckVariant (separate from the renamed-from-fiorentine-minchiate RWS Tarot now living at tarot-rider-waite-smith per A.0). Slug minchiate-fiorentine-1860-1890 matches the asset dir committed in 0add163 (98 PNGs at src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine-1860-1890/); Sprint A.2's image_filename property will use deck.slug to point at those images. Schema fields set: family='italian' (drives display + filename slug mapping per [[reference-card-image-naming-convention]] — BRANDS→batons, CROWNS→coins, GRAILS→cups, BLADES→swords), has_card_images=True (first deck w. images shipped), is_polarized=False (Earthman remains the only polarized deck), is_default=False (Earthman is default), card_count=97. 97 TarotCard rows seeded: 41 trumps (Il Matto at rank 0 per the unnumbered-Fool-gets-sortable-position convention from [[reference-card-image-naming-convention]]; then 40 numbered 1-40) + 56 minors (4 suits × 14 cards = pip 1-10 + page=11 + knight=12 + queen=13 + king=14 per the v2 convention's number-prefixed-courts decision). Trump names are Italian (Papa Uno / Papa Due / La Temperanza / La Forza / La Giustizia / La Ruota della Fortuna / Il Carro / Il Gobbo / L'Impiccato / La Morte / Il Diavolo / La Casa del Diavolo / La Speranza / La Prudenza / La Fede / La Carita / Il Fuoco / L'Acqua / La Terra / L'Aria + 12 zodiac signs + La Stella / La Luna / Il Sole / Il Mondo / Le Trombe). Card-suit canonical enum stays BRANDS/CROWNS/GRAILS/BLADES per A.0's lock; minor card NAMES use Italian-family display vocab ("Page of Batons" not "Page of Brands") since names are the user-facing label whereas suit is the structural identity. 16 trumps carry a correspondence field pointing to their RWS Tarot equivalent (Il Matto→The Fool, Il Carro→The Chariot, Il Gobbo→The Hermit, L'Impiccato→The Hanged Man, La Morte→Death, Il Diavolo→The Devil, La Casa del Diavolo→The Tower, La Temperanza→Temperance, La Forza→Strength, La Giustizia→Justice, La Ruota della Fortuna→Wheel of Fortune, La Stella→The Star, La Luna→The Moon, Il Sole→The Sun, Il Mondo→The World, Le Trombe→Judgement); the 25 Minchiate-only trumps (5 popes + 4 theological/cardinal virtues + 4 elements + 12 zodiac) have no RWS parallel → empty correspondence. keywords_upright / keywords_reversed intentionally left empty []: those are interpretive content the user owns; admin form (Sprint B) will enrich via UI rather than have them committed as code in a migration. Five trumps in the v2 filename convention have elided-apostrophe slugs restored (l-impiccato, l-acqua, l-aria, l-ariete, l-acquario); DB slug field matches (no apostrophe, but with the leading l- prefix). 17 new ITs in MinchiateFiorentine1860SeedTest cover the deck attributes (name + family + has_card_images + is_polarized + card_count) + total row count (97) + arcana breakdown (41 trumps + 56 minors + 0 middle) + specific cards (Il Matto at rank 0 + Il Gobbo at rank 11 w. correspondence "The Hermit" + Le Trombe at rank 40 + 5 popes are MAJOR ranks 1-5 + Page of Batons + King of Coins) + canonical-suit-only check (no WANDS/CUPS/SWORDS/PENTACLES in DB) + court rank range (11-14 per suit). Tests: 17 new green; 1272/1272 IT+UT total green (64s; +17 from A.0's 1255). Out of scope: A.2 adds the TarotCard.image_filename + display_suit_name properties consuming deck.family for per-family translation; A.3 wires my_sign.html's first render branch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-24 23:32:19 -04:00
parent f107522b20
commit a4ac25605d
2 changed files with 249 additions and 0 deletions

View File

@@ -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 (18601890)",
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)]

View File

@@ -1037,3 +1037,115 @@ class DeckSchemaA0Test(TestCase):
self.assertNotIn("CUPS", choice_values) self.assertNotIn("CUPS", choice_values)
self.assertNotIn("SWORDS", choice_values) self.assertNotIn("SWORDS", choice_values)
self.assertNotIn("PENTACLES", 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 (18601890)")
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,
)