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:
@@ -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)]
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user