From b0d153ebc133c5131c3122f24b1d4dd0356e594b Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 8 Jun 2026 22:08:31 -0400 Subject: [PATCH] =?UTF-8?q?sea=20affinity=20scroll=20log:=20personalized?= =?UTF-8?q?=20per-Role=20prose=20with=20pronouns=20(draws=20POSS=20Sea=20o?= =?UTF-8?q?f=20cards,=20where=20the=20CARD=20...)=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the generic 'finding affinity with the X in the Crown' SEA_DRAWN prose with a role-specific clause per user-spec 2026-06-09 — the @handle is template-prepended, pronouns (subj/poss) resolve via the actor: PC: draws POSS Sea of cards, where the CARD crowns all POSS loftiest illusions. NC: ...the CARD traces all the narratives SUBJ leaves behind. EC: ...always looms before POSS calling. SC: ...covers all POSS righteous conduct. AC: ...always crosses POSS sinister connections. BC: ...lays all POSS foundational work. to_prose branches on data['role']; 'The ' still stripped so a qualifier butts the proper name; an unknown role falls back to a generic 'marks POSS affinity' clause. The stored position_label/corner_rank/suit_icon are now unused by the prose but left in the event data (harmless). TDD: drama prose tests reworked (per-role clauses + bawlmorese pronoun substitution) + the epic affinity-prose IT updated to the PC crown clause. 49 drama+epic ITs green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/apps/drama/models.py | 36 ++++++-------- .../drama/tests/integrated/test_models.py | 49 +++++++++++++------ src/apps/epic/tests/integrated/test_views.py | 8 +-- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/apps/drama/models.py b/src/apps/drama/models.py index ba8d732..3db366d 100644 --- a/src/apps/drama/models.py +++ b/src/apps/drama/models.py @@ -163,26 +163,22 @@ class GameEvent(models.Model): f"which yields {obj} equal {joined} capacities." ) if self.verb == self.SEA_DRAWN: - # Affinity statement: the card drawn into the gamer's Role-correlated - # Celtic position. `card_name`/`corner_rank`/`suit_icon` mirror - # SIG_READY (qualifier-prefixed name + abbrev, "The " stripped so a - # qualifier butts the proper name); `position_label` is the - # spread-specific label for the role's position key. - card_name = d.get("card_name", "a card") - corner_rank = d.get("corner_rank", "") - suit_icon = d.get("suit_icon", "") - position_label = d.get("position_label", "spread") - if corner_rank: - icon_html = f' ' if suit_icon else "" - abbrev = f" ({corner_rank}{icon_html})" - else: - abbrev = "" - card_name = card_name.replace("The ", "", 1) - _, _, poss = _actor_pronouns(self.actor) - return ( - f"draws {poss} Celtic Cross, finding affinity with " - f"the {card_name}{abbrev} in the {position_label}." - ) + # Personalized per-Role affinity (user-spec 2026-06-09): the card drawn + # into the gamer's Role-correlated Celtic position, woven into a + # role-specific clause. The actor's @handle is prepended by the + # template; pronouns resolve via the actor (subj/poss). "The " is + # stripped so a levity/gravity qualifier butts the proper name. + card = d.get("card_name", "a card").replace("The ", "", 1) + subj, _, poss = _actor_pronouns(self.actor) + clause = { + "PC": f"the {card} crowns all {poss} loftiest illusions", + "NC": f"the {card} traces all the narratives {subj} leaves behind", + "EC": f"the {card} always looms before {poss} calling", + "SC": f"the {card} covers all {poss} righteous conduct", + "AC": f"the {card} always crosses {poss} sinister connections", + "BC": f"the {card} lays all {poss} foundational work", + }.get(d.get("role", ""), f"the {card} marks {poss} affinity") + return f"draws {poss} Sea of cards, where {clause}." if self.verb == self.SEA_RELINQUISHED: card_name = d.get("card_name", "a card").replace("The ", "", 1) _, _, poss = _actor_pronouns(self.actor) diff --git a/src/apps/drama/tests/integrated/test_models.py b/src/apps/drama/tests/integrated/test_models.py index d694a7c..c8b694b 100644 --- a/src/apps/drama/tests/integrated/test_models.py +++ b/src/apps/drama/tests/integrated/test_models.py @@ -181,24 +181,43 @@ class GameEventModelTest(TestCase): ) # ── to_prose — SEA_DRAWN / SEA_RELINQUISHED ────────────────────────── - def test_sea_drawn_prose_states_affinity_and_position(self): + def test_sea_drawn_prose_pc_crown_clause(self): event = record(self.room, GameEvent.SEA_DRAWN, actor=self.user, - role="PC", slot_number=1, position="crown", - position_label="Crown", card_name="The Magician", - corner_rank="I", suit_icon="") - prose = event.to_prose() - # Default pronouns = pluralism → "their"; "The " is stripped before "the". - self.assertIn( - "draws their Celtic Cross, finding affinity with the Magician (I) " - "in the Crown.", prose) + role="PC", slot_number=1, card_name="The Magician") + # Default pronouns = pluralism → "their"; "The " stripped before "the". + self.assertEqual( + event.to_prose(), + "draws their Sea of cards, where the Magician crowns all " + "their loftiest illusions.") - def test_sea_drawn_prose_uses_spread_label(self): - # Escape-Velocity calls the `loom` position "Loom"; the label is passed - # in `position_label`, so to_prose stays spread-agnostic. + def test_sea_drawn_prose_per_role_clauses(self): + # Each Role gets its own clause (user-spec 2026-06-09). Default pronouns: + # subj "they", poss "their". + cases = { + "PC": "the Maid crowns all their loftiest illusions", + "NC": "the Maid traces all the narratives they leaves behind", + "EC": "the Maid always looms before their calling", + "SC": "the Maid covers all their righteous conduct", + "AC": "the Maid always crosses their sinister connections", + "BC": "the Maid lays all their foundational work", + } + for role, clause in cases.items(): + event = record(self.room, GameEvent.SEA_DRAWN, actor=self.user, + role=role, card_name="Maid") + self.assertEqual( + event.to_prose(), + f"draws their Sea of cards, where {clause}.", f"role={role}") + + def test_sea_drawn_prose_uses_actor_pronouns(self): + self.user.pronouns = "bawlmorese" + self.user.save(update_fields=["pronouns"]) event = record(self.room, GameEvent.SEA_DRAWN, actor=self.user, - role="EC", slot_number=3, position="loom", - position_label="Loom", card_name="Maid of Brands") - self.assertIn("in the Loom.", event.to_prose()) + role="EC", card_name="The Nomad") + # Bawlmorese → poss "yos". + self.assertEqual( + event.to_prose(), + "draws yos Sea of cards, where the Nomad always looms before " + "yos calling.") def test_sea_relinquished_prose(self): event = record(self.room, GameEvent.SEA_RELINQUISHED, actor=self.user, diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 5cb20fd..2fc3413 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -4330,11 +4330,13 @@ class PickSeaPersistTest(TestCase): crown_card = TarotCard.objects.get(id=crown_id) self.assertIn(crown_card.name_title, ev.data["card_name"]) - def test_affinity_prose_names_the_waite_smith_position_label(self): + def test_affinity_prose_is_the_pc_crown_clause(self): + # PC → the crown-card crowns clause (user-spec 2026-06-09). Founder has + # default pronouns → "their". self._post_hand() prose = self._drawn()[0].to_prose() - self.assertIn("finding affinity with", prose) - self.assertIn("in the Crown.", prose) + self.assertIn("draws their Sea of cards, where the", prose) + self.assertIn("crowns all their loftiest illusions.", prose) def test_no_affinity_logged_before_hand_complete(self): self._post_hand(self._hand()[:5]) # only 5 of 6 placed