added default Earthman 108-card tarot deck, 78-card Minchiate Fiorentine deck, admin tests for each; DeckVariant model governs deck toggle; ran new migrations for apps.epic, apps.lyric; seeded DeckVariant migration to ensure Earthman is default deck; added min. tarot url; most new FTs passing
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from datetime import timedelta
|
||||
@@ -173,3 +174,96 @@ class TableSeat(models.Model):
|
||||
role = models.CharField(max_length=2, choices=ROLE_CHOICES, null=True, blank=True)
|
||||
role_revealed = models.BooleanField(default=False)
|
||||
seat_position = models.IntegerField(null=True, blank=True)
|
||||
|
||||
|
||||
class DeckVariant(models.Model):
|
||||
"""A named deck variant, e.g. Earthman (108 cards) or Fiorentine Minchiate (78 cards)."""
|
||||
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.card_count} cards)"
|
||||
|
||||
|
||||
class TarotCard(models.Model):
|
||||
MAJOR = "MAJOR"
|
||||
MINOR = "MINOR"
|
||||
ARCANA_CHOICES = [
|
||||
(MAJOR, "Major Arcana"),
|
||||
(MINOR, "Minor Arcana"),
|
||||
]
|
||||
|
||||
WANDS = "WANDS"
|
||||
CUPS = "CUPS"
|
||||
SWORDS = "SWORDS"
|
||||
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
||||
COINS = "COINS" # Earthman 4th suit (Ossum / Stone)
|
||||
SUIT_CHOICES = [
|
||||
(WANDS, "Wands"),
|
||||
(CUPS, "Cups"),
|
||||
(SWORDS, "Swords"),
|
||||
(PENTACLES, "Pentacles"),
|
||||
(COINS, "Coins"),
|
||||
]
|
||||
|
||||
deck_variant = models.ForeignKey(
|
||||
DeckVariant, null=True, blank=True,
|
||||
on_delete=models.CASCADE, related_name="cards",
|
||||
)
|
||||
name = models.CharField(max_length=200)
|
||||
arcana = models.CharField(max_length=5, choices=ARCANA_CHOICES)
|
||||
suit = models.CharField(max_length=10, choices=SUIT_CHOICES, null=True, blank=True)
|
||||
number = models.IntegerField() # 0–21 major (Fiorentine); 0–51 major (Earthman); 1–14 minor
|
||||
slug = models.SlugField(max_length=120)
|
||||
correspondence = models.CharField(max_length=200, blank=True) # standard / Italian equivalent
|
||||
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
|
||||
keywords_upright = models.JSONField(default=list)
|
||||
keywords_reversed = models.JSONField(default=list)
|
||||
|
||||
class Meta:
|
||||
ordering = ["deck_variant", "arcana", "suit", "number"]
|
||||
unique_together = [("deck_variant", "slug")]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TarotDeck(models.Model):
|
||||
"""One shuffled deck per room, scoped to the founder's chosen DeckVariant."""
|
||||
|
||||
room = models.OneToOneField(Room, on_delete=models.CASCADE, related_name="tarot_deck")
|
||||
deck_variant = models.ForeignKey(
|
||||
DeckVariant, null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name="active_decks",
|
||||
)
|
||||
drawn_card_ids = models.JSONField(default=list)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
def remaining_count(self):
|
||||
total = self.deck_variant.card_count if self.deck_variant else 0
|
||||
return total - len(self.drawn_card_ids)
|
||||
|
||||
def draw(self, n=1):
|
||||
"""Draw n cards at random. Returns list of (TarotCard, reversed: bool) tuples."""
|
||||
available = list(
|
||||
TarotCard.objects.filter(deck_variant=self.deck_variant)
|
||||
.exclude(id__in=self.drawn_card_ids)
|
||||
)
|
||||
if len(available) < n:
|
||||
raise ValueError(
|
||||
f"Not enough cards remaining: {len(available)} available, {n} requested"
|
||||
)
|
||||
drawn = random.sample(available, n)
|
||||
self.drawn_card_ids = self.drawn_card_ids + [card.id for card in drawn]
|
||||
self.save(update_fields=["drawn_card_ids"])
|
||||
return [(card, random.choice([True, False])) for card in drawn]
|
||||
|
||||
def shuffle(self):
|
||||
"""Reset the deck so all variant cards are available again."""
|
||||
self.drawn_card_ids = []
|
||||
self.save(update_fields=["drawn_card_ids"])
|
||||
|
||||
Reference in New Issue
Block a user