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 return wands_crowns + swords_cups + major
def levity_sig_cards(room): def _filter_major_unlocks(cards, user):
"""The 18 cards available to the levity group (PC/NC/SC).""" """Remove Nomad (0) and Schizo (1) unless the user has the matching Note unlock."""
return _sig_unique_cards(room) 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): def levity_sig_cards(room, user=None):
"""The 18 cards available to the gravity group (BC/EC/AC).""" """Cards available to the levity group (PC/NC/SC), filtered by user's Note unlocks."""
return _sig_unique_cards(room) 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): def sig_seat_order(room):

View File

@@ -445,25 +445,44 @@ class SigReservationModelTest(TestCase):
class SigCardHelperTest(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). Relies on the Earthman deck seeded by migrations (no manual card creation).
""" """
def setUp(self): def setUp(self):
# Earthman deck is already seeded by migrations from django.utils import timezone
self.earthman = DeckVariant.objects.get(slug="earthman") self.earthman = DeckVariant.objects.get(slug="earthman")
self.owner = User.objects.create(email="founder@test.io") self.owner = User.objects.create(email="founder@test.io")
self.owner.equipped_deck = self.earthman TableSeat.objects.create(
self.owner.save() room=Room.objects.create(name="Card Test", owner=self.owner),
self.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): def test_levity_sig_cards_returns_16_without_notes(self):
cards = levity_sig_cards(self.room) cards = levity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 18) self.assertEqual(len(cards), 16)
def test_gravity_sig_cards_returns_18(self): def test_gravity_sig_cards_returns_16_without_notes(self):
cards = gravity_sig_cards(self.room) cards = gravity_sig_cards(self.room, self.owner)
self.assertEqual(len(cards), 18) 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): def test_levity_and_gravity_share_same_card_objects(self):
"""Both piles draw from the same 18 TarotCard instances — visual distinction """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), 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.equipped_deck = None
self.owner.save() self.owner.save()
self.assertEqual(levity_sig_cards(self.room), []) self.assertEqual(levity_sig_cards(self.room, self.owner), [])
self.assertEqual(gravity_sig_cards(self.room), []) self.assertEqual(gravity_sig_cards(self.room, self.owner), [])
class TarotCardCautionsTest(TestCase): class TarotCardCautionsTest(TestCase):

View File

@@ -5,7 +5,7 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone 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.lyric.models import Token, User
from apps.epic.models import ( from apps.epic.models import (
Character, DeckVariant, GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, 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.status = GateSlot.FILLED
slot.save() slot.save()
TableSeat.objects.create( 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.gate_status = Room.OPEN
room.table_status = Room.SIG_SELECT room.table_status = Room.SIG_SELECT
@@ -1055,7 +1056,24 @@ class SigSelectRenderingTest(TestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertContains(response, "id_sig_deck") 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) response = self.client.get(self.url)
self.assertEqual(response.content.decode().count('data-card-id='), 18) 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) ctx["sig_reservations_json"] = json.dumps(reservations)
if user_polarity == 'levity': if user_polarity == 'levity':
ctx["sig_cards"] = levity_sig_cards(room) ctx["sig_cards"] = levity_sig_cards(room, user)
elif user_polarity == 'gravity': elif user_polarity == 'gravity':
ctx["sig_cards"] = gravity_sig_cards(room) ctx["sig_cards"] = gravity_sig_cards(room, user)
else: else:
ctx["sig_cards"] = [] ctx["sig_cards"] = []