A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three DeckVariant fields: has_card_images (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), family (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), is_polarized (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). TarotCard.SUIT_CHOICES collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since sig_deck_cards + levity/gravity_sig_cards already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (batons for Italian, wands for English, clubs for Playing) lives in Sprint A.2's display_suit_name property, not in the enum. Audit 2026-05-25 revealed the existing fiorentine-minchiate DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → tarot-rider-waite-smith, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: sig_deck_cards + _sig_unique_cards_for_deck queries shrink from suit__in=[3 values] and [4 values] to [2 values] each (one per segment); TarotCard.suit_icon mapping shrinks from 8 entries to 4; gameboard.views.tarot_fan._suit_order shrinks from 8 keys to 4. Existing test files updated: test_game_room_tray.py (largest update — self.fiorentine → self.rws, id_kit_fiorentine_deck → id_kit_tarot_deck (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); test_game_room_deck_contrib.py (same pattern, smaller); lyric/test_models.py + gameboard/test_views.py (slug literal swaps only); epic/test_models.py _make_sig_card test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in DeckSchemaA0Test cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the image_filename + display_suit_name properties that consume the new family field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -221,13 +221,27 @@ class TableSeat(models.Model):
|
||||
|
||||
|
||||
class DeckVariant(models.Model):
|
||||
"""A named deck variant, e.g. Earthman (108 cards) or Fiorentine Minchiate (78 cards)."""
|
||||
"""A named deck variant, e.g. Earthman or Tarot (Rider-Waite-Smith)."""
|
||||
|
||||
EARTHMAN = "earthman"
|
||||
ITALIAN = "italian"
|
||||
ENGLISH = "english"
|
||||
PLAYING = "playing"
|
||||
FAMILY_CHOICES = [
|
||||
(EARTHMAN, "Earthman"),
|
||||
(ITALIAN, "Italian / Minchiate"),
|
||||
(ENGLISH, "English Tarot"),
|
||||
(PLAYING, "Playing card"),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
card_count = models.IntegerField()
|
||||
description = models.TextField(blank=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
family = models.CharField(max_length=10, choices=FAMILY_CHOICES, default=EARTHMAN)
|
||||
has_card_images = models.BooleanField(default=True)
|
||||
is_polarized = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def short_key(self):
|
||||
@@ -248,23 +262,18 @@ class TarotCard(models.Model):
|
||||
(MIDDLE, "Middle Arcana"),
|
||||
]
|
||||
|
||||
WANDS = "WANDS"
|
||||
CUPS = "CUPS"
|
||||
SWORDS = "SWORDS"
|
||||
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
||||
CROWNS = "CROWNS" # Earthman 4th suit
|
||||
BRANDS = "BRANDS" # Earthman Wands
|
||||
GRAILS = "GRAILS" # Earthman Cups
|
||||
BLADES = "BLADES" # Earthman Swords
|
||||
# Canonical SUIT_CHOICES = Earthman vocabulary (2026-05-25 lock).
|
||||
# Per-family display + filename slug mapping lives in image_filename /
|
||||
# display_suit_name properties driven by DeckVariant.family.
|
||||
BRANDS = "BRANDS"
|
||||
CROWNS = "CROWNS"
|
||||
GRAILS = "GRAILS"
|
||||
BLADES = "BLADES"
|
||||
SUIT_CHOICES = [
|
||||
(WANDS, "Wands"),
|
||||
(CUPS, "Cups"),
|
||||
(SWORDS, "Swords"),
|
||||
(PENTACLES, "Pentacles"),
|
||||
(CROWNS, "Crowns"),
|
||||
(BRANDS, "Brands"),
|
||||
(GRAILS, "Grails"),
|
||||
(BLADES, "Blades"),
|
||||
(BRANDS, "Brands"),
|
||||
(CROWNS, "Crowns"),
|
||||
(GRAILS, "Grails"),
|
||||
(BLADES, "Blades"),
|
||||
]
|
||||
|
||||
deck_variant = models.ForeignKey(
|
||||
@@ -437,14 +446,10 @@ class TarotCard(models.Model):
|
||||
if self.arcana == self.MAJOR:
|
||||
return ''
|
||||
return {
|
||||
self.WANDS: 'fa-wand-sparkles',
|
||||
self.CUPS: 'fa-trophy',
|
||||
self.SWORDS: 'fa-gun',
|
||||
self.PENTACLES: 'fa-star',
|
||||
self.CROWNS: 'fa-crown',
|
||||
self.BRANDS: 'fa-wand-sparkles',
|
||||
self.GRAILS: 'fa-trophy',
|
||||
self.BLADES: 'fa-gun',
|
||||
self.BRANDS: 'fa-wand-sparkles',
|
||||
self.CROWNS: 'fa-crown',
|
||||
self.GRAILS: 'fa-trophy',
|
||||
self.BLADES: 'fa-gun',
|
||||
}.get(self.suit, '')
|
||||
|
||||
@property
|
||||
@@ -558,32 +563,12 @@ def _room_deck_variant(room):
|
||||
def sig_deck_cards(room):
|
||||
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
|
||||
|
||||
PC/BC pair → BRANDS/WANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
||||
SC/AC pair → BLADES/SWORDS + GRAILS/CUPS Middle Arcana court cards (11–14): 8 unique
|
||||
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
||||
PC/BC pair → BRANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
||||
SC/AC pair → BLADES + GRAILS Middle Arcana court cards (11–14): 8 unique
|
||||
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
||||
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
|
||||
"""
|
||||
deck_variant = _room_deck_variant(room)
|
||||
if deck_variant is None:
|
||||
return []
|
||||
wands_crowns = list(TarotCard.objects.filter(
|
||||
deck_variant=deck_variant,
|
||||
arcana=TarotCard.MIDDLE,
|
||||
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
||||
number__in=[11, 12, 13, 14],
|
||||
))
|
||||
swords_cups = list(TarotCard.objects.filter(
|
||||
deck_variant=deck_variant,
|
||||
arcana=TarotCard.MIDDLE,
|
||||
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
||||
number__in=[11, 12, 13, 14],
|
||||
))
|
||||
major = list(TarotCard.objects.filter(
|
||||
deck_variant=deck_variant,
|
||||
arcana=TarotCard.MAJOR,
|
||||
number__in=[0, 1],
|
||||
))
|
||||
unique_cards = wands_crowns + swords_cups + major # 18 unique
|
||||
unique_cards = _sig_unique_cards_for_deck(_room_deck_variant(room))
|
||||
return unique_cards + unique_cards # × 2 = 36
|
||||
|
||||
|
||||
@@ -594,16 +579,16 @@ def _sig_unique_cards_for_deck(deck_variant):
|
||||
via personal_sig_cards from User.equipped_deck)."""
|
||||
if deck_variant is None:
|
||||
return []
|
||||
wands_crowns = list(TarotCard.objects.filter(
|
||||
brands_crowns = list(TarotCard.objects.filter(
|
||||
deck_variant=deck_variant,
|
||||
arcana=TarotCard.MIDDLE,
|
||||
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
||||
suit__in=[TarotCard.BRANDS, TarotCard.CROWNS],
|
||||
number__in=[11, 12, 13, 14],
|
||||
))
|
||||
swords_cups = list(TarotCard.objects.filter(
|
||||
blades_grails = list(TarotCard.objects.filter(
|
||||
deck_variant=deck_variant,
|
||||
arcana=TarotCard.MIDDLE,
|
||||
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
||||
suit__in=[TarotCard.BLADES, TarotCard.GRAILS],
|
||||
number__in=[11, 12, 13, 14],
|
||||
))
|
||||
major = list(TarotCard.objects.filter(
|
||||
@@ -611,7 +596,7 @@ def _sig_unique_cards_for_deck(deck_variant):
|
||||
arcana=TarotCard.MAJOR,
|
||||
number__in=[0, 1],
|
||||
))
|
||||
return wands_crowns + swords_cups + major
|
||||
return brands_crowns + blades_grails + major
|
||||
|
||||
|
||||
def _sig_unique_cards(room):
|
||||
|
||||
Reference in New Issue
Block a user