A.2 TarotCard.image_filename + display_suit_name properties — TDD. Sprint A.2 of [[project-image-based-deck-face-rendering]]. Adds two per-card derived properties that consume the new DeckVariant.family field (locked in A.0) to translate canonical-Earthman SUIT enum (BRANDS/CROWNS/GRAILS/BLADES) into family-authentic filename slugs + UI labels per [[reference-card-image-naming-convention]] v2. DeckVariant gains the family-mapping tables + methods (suit_slug / suit_display / trump_category); TarotCard consumes them via image_filename + display_suit_name. Two mapping tables live on DeckVariant (single source of truth for per-family vocab): _SUIT_SLUG_BY_FAMILY (4 families × 4 suits = 16 entries: earthman is identity-mapped {BRANDS→brands, CROWNS→crowns, GRAILS→grails, BLADES→blades}; italian is {BRANDS→batons, CROWNS→coins, GRAILS→cups, BLADES→swords}; english is {BRANDS→wands, CROWNS→pentacles, GRAILS→cups, BLADES→swords}; playing is {BRANDS→clubs, CROWNS→diamonds, GRAILS→hearts, BLADES→spades}) and _TRUMP_CATEGORY_BY_FAMILY (earthman+italian use "trumps", english uses "majors" matching Modern Tarot's "Major Arcana", playing is None since 52-card decks have no trump category — jokers handled separately when a playing deck is seeded). DeckVariant.suit_slug(canonical) returns the filename slug; suit_display(canonical) returns capitalized UI label (via slug.capitalize()); trump_category is a property since it takes no per-card argument. TarotCard.image_filename branches on arcana: MAJOR returns <deck-slug>-<trump-category>-<NN>-<card-slug>.png (NN = zero-padded number per v2 convention, e.g. 00 for Il Matto; card-slug carries the italian name like "il-gobbo" or english like "the-fool"); MINOR/MIDDLE returns <deck-slug>-<suit-slug>-<NN>[-<court>].png where court suffix is "page"/"knight"/"queen"/"king" for ranks 11-14 (tarot family courts; playing-family's 3-court jack/queen/king deferred to playing-deck-seed sprint). display_suit_name returns capitalized family-authentic suit name ("Batons" for italian BRANDS, "Pentacles" for english CROWNS) or empty string for major arcana (no suit). Both properties are pure-derived — no schema migration needed, no DB writes; the template (Sprint A.3+) decides whether to render <img src=image_filename> based on deck.has_card_images. RWS deck's image_filename returns a path even though has_card_images=False (path is correct per convention; just no file exists at that path yet — once RWS images are sourced, flip the flag). 17 new ITs in CardImageFilenameA2Test cover: Minchiate trumps (Il Matto rank-00, Il Gobbo rank-11, Le Trombe rank-40, L'Acqua rank-21 w. apostrophe-restored slug); Minchiate minors (Ace of Batons pip-with-no-court-suffix, Ten of Coins, Page of Cups w. court suffix, King of Swords); RWS post-revocab (Ace of Cups uses english-family "cups" slug despite suit=GRAILS, The Fool uses "majors" category, King of Pentacles uses "pentacles" slug despite suit=CROWNS); Earthman identity-mapped (BRANDS→brands); display_suit_name across all 3 tarot families (italian BRANDS→"Batons", italian CROWNS→"Coins", english CROWNS→"Pentacles", earthman BRANDS→"Brands"); empty for majors. Tests: 17 new green; 1289/1289 IT+UT total green (63s; +17 from A.1's 1272). Out of scope: A.3 wires my_sign.html's first render branch (the visible-win first surface); A.4 builds card-deck icon + game_kit applet; A.5-A.8 DRY across my_sea + both billboard applets + room
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,22 @@ class DeckVariant(models.Model):
|
||||
(PLAYING, "Playing card"),
|
||||
]
|
||||
|
||||
# Per-family translation tables: canonical SUIT enum (Earthman vocab) →
|
||||
# family-authentic display slug used in image filenames + UI labels.
|
||||
# See [[reference-card-image-naming-convention]] v2.
|
||||
_SUIT_SLUG_BY_FAMILY = {
|
||||
EARTHMAN: {"BRANDS": "brands", "CROWNS": "crowns", "GRAILS": "grails", "BLADES": "blades"},
|
||||
ITALIAN: {"BRANDS": "batons", "CROWNS": "coins", "GRAILS": "cups", "BLADES": "swords"},
|
||||
ENGLISH: {"BRANDS": "wands", "CROWNS": "pentacles", "GRAILS": "cups", "BLADES": "swords"},
|
||||
PLAYING: {"BRANDS": "clubs", "CROWNS": "diamonds", "GRAILS": "hearts", "BLADES": "spades"},
|
||||
}
|
||||
_TRUMP_CATEGORY_BY_FAMILY = {
|
||||
EARTHMAN: "trumps",
|
||||
ITALIAN: "trumps",
|
||||
ENGLISH: "majors",
|
||||
PLAYING: None, # 52-card decks: no trump category (jokers handled separately)
|
||||
}
|
||||
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
card_count = models.IntegerField()
|
||||
@@ -243,6 +259,20 @@ class DeckVariant(models.Model):
|
||||
has_card_images = models.BooleanField(default=True)
|
||||
is_polarized = models.BooleanField(default=False)
|
||||
|
||||
def suit_slug(self, canonical_suit):
|
||||
"""Map canonical SUIT enum → family-authentic filename slug.
|
||||
e.g. ('italian', 'BRANDS') → 'batons'."""
|
||||
return self._SUIT_SLUG_BY_FAMILY[self.family][canonical_suit]
|
||||
|
||||
def suit_display(self, canonical_suit):
|
||||
"""User-facing capitalized suit label, e.g. ('italian', 'BRANDS') → 'Batons'."""
|
||||
return self.suit_slug(canonical_suit).capitalize()
|
||||
|
||||
@property
|
||||
def trump_category(self):
|
||||
"""Filename-slug category for trump cards in this family."""
|
||||
return self._TRUMP_CATEGORY_BY_FAMILY[self.family]
|
||||
|
||||
@property
|
||||
def short_key(self):
|
||||
"""First dash-separated word of slug — used as an HTML id component."""
|
||||
@@ -452,6 +482,35 @@ class TarotCard(models.Model):
|
||||
self.BLADES: 'fa-gun',
|
||||
}.get(self.suit, '')
|
||||
|
||||
# Tarot-family courts: rank 11=page, 12=knight, 13=queen, 14=king. Playing
|
||||
# family (3 courts: jack/queen/king at ranks 11-13) handled separately when
|
||||
# a playing deck is seeded — Sprint A.2 covers tarot families only.
|
||||
_COURT_NAME_BY_RANK = {11: "page", 12: "knight", 13: "queen", 14: "king"}
|
||||
|
||||
@property
|
||||
def image_filename(self):
|
||||
"""v2-convention filename per [[reference-card-image-naming-convention]].
|
||||
Always derives a path; the template decides whether to actually render
|
||||
an <img> based on `deck_variant.has_card_images`."""
|
||||
deck = self.deck_variant
|
||||
if self.arcana == self.MAJOR:
|
||||
return f"{deck.slug}-{deck.trump_category}-{self.number:02d}-{self.slug}.png"
|
||||
# MINOR or MIDDLE: <deck-slug>-<suit-slug>-<NN>[-<court>].png
|
||||
suit_slug = deck.suit_slug(self.suit)
|
||||
rank = f"{self.number:02d}"
|
||||
court = self._COURT_NAME_BY_RANK.get(self.number)
|
||||
if court:
|
||||
return f"{deck.slug}-{suit_slug}-{rank}-{court}.png"
|
||||
return f"{deck.slug}-{suit_slug}-{rank}.png"
|
||||
|
||||
@property
|
||||
def display_suit_name(self):
|
||||
"""Family-authentic capitalized suit label (e.g. 'Batons' for italian
|
||||
BRANDS, 'Pentacles' for english CROWNS). Empty for major arcana."""
|
||||
if not self.suit:
|
||||
return ""
|
||||
return self.deck_variant.suit_display(self.suit)
|
||||
|
||||
@property
|
||||
def cautions_json(self):
|
||||
import json
|
||||
|
||||
Reference in New Issue
Block a user