SIG SELECT: Nomad/Schizo locked by default; Note-unlock gate — TDD

- _filter_major_unlocks(cards, user): strips Major 0 (Nomad) and Major 1
  (Schizo) unless user has matching 'nomad'/'schizo' Note; unauthenticated
  users see 0 majors
- levity_sig_cards(room, user) / gravity_sig_cards(room, user): accept user
  param; default 16 court cards, up to 18 with both Note unlocks
- View wires user into both calls; _sig_unique_cards / sig_deck_cards unchanged
  (game-table deck still includes all 18 unique)
- _full_sig_setUp: seats now carry deck_variant=earthman
- SigCardHelperTest: 4 new ITs (default 16, nomad +1, schizo +1); empty-deck
  test updated to clear seats + owner
- SigSelectRenderingTest: 18-card test updated to 16-default + 3 Note-unlock ITs

Pending: superusers auto-granted nomad + schizo Notes on creation (ask user)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-28 01:05:25 -04:00
parent eaff2a1edb
commit 1c2b8f96ab
4 changed files with 77 additions and 25 deletions

View File

@@ -491,14 +491,27 @@ def _sig_unique_cards(room):
return wands_crowns + swords_cups + major
def levity_sig_cards(room):
"""The 18 cards available to the levity group (PC/NC/SC)."""
return _sig_unique_cards(room)
def _filter_major_unlocks(cards, user):
"""Remove Nomad (0) and Schizo (1) unless the user has the matching Note unlock."""
if user is None or not user.is_authenticated:
return [c for c in cards if c.arcana != TarotCard.MAJOR]
earned = set(user.notes.values_list("slug", flat=True))
return [
c for c in cards
if c.arcana != TarotCard.MAJOR
or (c.number == 0 and "nomad" in earned)
or (c.number == 1 and "schizo" in earned)
]
def gravity_sig_cards(room):
"""The 18 cards available to the gravity group (BC/EC/AC)."""
return _sig_unique_cards(room)
def levity_sig_cards(room, user=None):
"""Cards available to the levity group (PC/NC/SC), filtered by user's Note unlocks."""
return _filter_major_unlocks(_sig_unique_cards(room), user)
def gravity_sig_cards(room, user=None):
"""Cards available to the gravity group (BC/EC/AC), filtered by user's Note unlocks."""
return _filter_major_unlocks(_sig_unique_cards(room), user)
def sig_seat_order(room):

View File

@@ -445,25 +445,44 @@ class SigReservationModelTest(TestCase):
class SigCardHelperTest(TestCase):
"""levity_sig_cards() and gravity_sig_cards() return 18 cards each.
"""levity_sig_cards() and gravity_sig_cards() return 16 courts by default;
Nomad/Schizo added when the user has the matching Note unlock.
Relies on the Earthman deck seeded by migrations (no manual card creation).
"""
def setUp(self):
# Earthman deck is already seeded by migrations
from django.utils import timezone
self.earthman = DeckVariant.objects.get(slug="earthman")
self.owner = User.objects.create(email="founder@test.io")
self.owner.equipped_deck = self.earthman
self.owner.save()
self.room = Room.objects.create(name="Card Test", owner=self.owner)
TableSeat.objects.create(
room=Room.objects.create(name="Card Test", owner=self.owner),
gamer=self.owner, slot_number=1, role="PC",
deck_variant=self.earthman,
)
self.room = self.owner.table_seats.first().room
self._tz = timezone
def test_levity_sig_cards_returns_18(self):
cards = levity_sig_cards(self.room)
self.assertEqual(len(cards), 18)
def test_levity_sig_cards_returns_16_without_notes(self):
cards = levity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 16)
def test_gravity_sig_cards_returns_18(self):
cards = gravity_sig_cards(self.room)
self.assertEqual(len(cards), 18)
def test_gravity_sig_cards_returns_16_without_notes(self):
cards = gravity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 16)
def test_nomad_note_includes_nomad(self):
from apps.drama.models import Note
Note.objects.create(user=self.owner, slug="nomad", earned_at=self._tz.now())
cards = levity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 17)
self.assertTrue(any(c.number == 0 and c.arcana == "MAJOR" for c in cards))
def test_schizo_note_includes_schizo(self):
from apps.drama.models import Note
Note.objects.create(user=self.owner, slug="schizo", earned_at=self._tz.now())
cards = levity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 17)
self.assertTrue(any(c.number == 1 and c.arcana == "MAJOR" for c in cards))
def test_levity_and_gravity_share_same_card_objects(self):
"""Both piles draw from the same 18 TarotCard instances — visual distinction
@@ -475,11 +494,13 @@ class SigCardHelperTest(TestCase):
sorted(c.pk for c in gravity),
)
def test_returns_empty_when_no_equipped_deck(self):
def test_returns_empty_when_no_deck_on_seats_or_owner(self):
"""Falls back to empty list when neither seats nor owner have a deck."""
self.room.table_seats.update(deck_variant=None)
self.owner.equipped_deck = None
self.owner.save()
self.assertEqual(levity_sig_cards(self.room), [])
self.assertEqual(gravity_sig_cards(self.room), [])
self.assertEqual(levity_sig_cards(self.room, self.owner), [])
self.assertEqual(gravity_sig_cards(self.room, self.owner), [])
class TarotCardCautionsTest(TestCase):

View File

@@ -5,7 +5,7 @@ from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from apps.drama.models import GameEvent
from apps.drama.models import GameEvent, Note
from apps.lyric.models import Token, User
from apps.epic.models import (
Character, DeckVariant, GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard,
@@ -1032,7 +1032,8 @@ def _full_sig_setUp(test_case, role_order=None):
slot.status = GateSlot.FILLED
slot.save()
TableSeat.objects.create(
room=room, gamer=gamer, slot_number=i, role=role, role_revealed=True,
room=room, gamer=gamer, slot_number=i, role=role,
role_revealed=True, deck_variant=earthman,
)
room.gate_status = Room.OPEN
room.table_status = Room.SIG_SELECT
@@ -1055,7 +1056,24 @@ class SigSelectRenderingTest(TestCase):
response = self.client.get(self.url)
self.assertContains(response, "id_sig_deck")
def test_sig_deck_contains_18_sig_cards(self):
def test_sig_deck_contains_16_sig_cards_by_default(self):
"""Without Note unlocks the deck shows only 16 court cards (no Nomad/Schizo)."""
response = self.client.get(self.url)
self.assertEqual(response.content.decode().count('data-card-id='), 16)
def test_nomad_note_adds_nomad_to_sig_deck(self):
Note.objects.create(user=self.gamers[0], slug="nomad", earned_at=timezone.now())
response = self.client.get(self.url)
self.assertEqual(response.content.decode().count('data-card-id='), 17)
def test_schizo_note_adds_schizo_to_sig_deck(self):
Note.objects.create(user=self.gamers[0], slug="schizo", earned_at=timezone.now())
response = self.client.get(self.url)
self.assertEqual(response.content.decode().count('data-card-id='), 17)
def test_both_notes_gives_18_sig_cards(self):
Note.objects.create(user=self.gamers[0], slug="nomad", earned_at=timezone.now())
Note.objects.create(user=self.gamers[0], slug="schizo", earned_at=timezone.now())
response = self.client.get(self.url)
self.assertEqual(response.content.decode().count('data-card-id='), 18)

View File

@@ -338,9 +338,9 @@ def _role_select_context(room, user):
ctx["sig_reservations_json"] = json.dumps(reservations)
if user_polarity == 'levity':
ctx["sig_cards"] = levity_sig_cards(room)
ctx["sig_cards"] = levity_sig_cards(room, user)
elif user_polarity == 'gravity':
ctx["sig_cards"] = gravity_sig_cards(room)
ctx["sig_cards"] = gravity_sig_cards(room, user)
else:
ctx["sig_cards"] = []