Files
python-tdd/src/functional_tests/test_game_room_seed_map.py
Disco DeDisco 9ed877168e
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
SEED MAP shared wheel rim: the table's OWN sky (planets-only, canonical signs) rings the tessellation — one frame for all six gamers — TDD
The Voronoi felt gains a stripped sky-wheel rim drawn from the ROOM's own sky
(Room.sky_chart) — identical for every gamer, updating toward the shared map —
with the tessellation sized into the wheel's freed hub. Roadmap step 21, Step
2's coordinate frame.

- Room.convened_at + Room.sky_chart (migration 0019); pick_roles stamps
  convened_at at gate-close (stamp only — no HTTP on the transition)
- epic.table_sky: lazy planets-only chart via PySwiss at the null location
  (geocentric longitudes need only the convened TIME; houses/ASC/MC need a
  birth LOCATION a virtual table lacks → omitted), cached on Room.sky_chart;
  legacy rooms key off created_at; seated-gamer gated, 502 on PySwiss down
- SkyWheel.drawRim(svg, data): pure static renderer — canonical asc=0 frame,
  signs ring + planet glyphs only, NO element ring / centre disc / houses /
  axes / aspects / tooltips; never writes the interactive wheel's singleton
  state; returns {size, cx, cy, r, hubR} so the felt sizes the map into the hub
- _seed_map_overlay.html: rim draws on open; map svg shrinks to 2×hubR +
  .voronoi-map--rimmed clip; lazy table-sky fetch on open; preload-then-repaint
  so a cold-cache open doesn't strand the zodiac glyphs; ResizeObserver on the
  col (not the self-sized map svg)
- _sky.scss: stacked centred svgs in .seed-map-col; .seed-wheel pointer-events
  none; circle clip on the rimmed map
- room_sky_json ctx in _role_select_context; rootvars: --sixUser/--octUser
  nudged within the Trs ramp (parallel palette tune)
- drawRim Jasmine suite (R1–R6: signs+planets, strip, hub geometry, static
  placement, singleton untouched, signs-only fallback) in both spec copies;
  epic TableSkyViewTest + convened_at stamp + seed-overlay rim ITs; FT rim
  assertions (12 signs, 3 planets, no stripped/located layers, hub sizing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:33:23 -04:00

198 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Functional test for the SEED MAP felt — Voronoi map, roadmap step 21.
The SEED MAP felt is the inline --duoUser sibling of the CAST SKY / DRAW SEA
felts: once the seat's Celtic-Cross hand is complete (hand_complete), SEED MAP
is the live hex phase btn, and clicking it opens a felt that paints a 2D
d3-delaunay DUAL GRAPH — a Voronoi cell layer (territory) + a Delaunay edge
layer (adjacency). STEP 1 is placeholder-seeded (card-driven seeding is Step 2),
so we assert only that the dual graph renders both layers.
Step 2's first piece — the SHARED WHEEL RIM: the stripped sky wheel (signs ring
at canonical orientation + planet glyphs; NO element ring, centre disc, houses
or axes — a virtual table has a convened TIME but no birth LOCATION) rings the
tessellation, drawn from the ROOM'S OWN sky (Room.sky_chart, identical for all
six gamers). The map svg shrinks into the wheel's freed hub. The fixture
pre-stores Room.sky_chart so the lazy PySwiss compute path never fires HTTP.
Plain FunctionalTest (NOT @tag("channels")): the felt-open is pure client-side.
We seed a CONFIRMED Character with a complete hand directly in the DB, so there
is no WebSocket sky-confirm flow to drive — unlike the sky/sea FTs.
"""
from django.urls import reverse
from django.utils import timezone
from selenium.webdriver.common.by import By
from apps.epic.models import Character, DeckVariant, TarotCard
from apps.lyric.models import User
from .base import FunctionalTest
from .test_game_room_select_sea import _make_sky_confirmed_room
_HAND_POSITIONS = ["cover", "cross", "crown", "lay", "leave", "loom"]
# The room's own sky — planets only (location-independent), mirroring the
# Jasmine ROOM_SKY fixture. Mercury retrograde exercises the ℞ badge.
_ROOM_SKY = {
"planets": {
"Sun": {"sign": "Pisces", "degree": 338.4, "retrograde": False},
"Moon": {"sign": "Capricorn", "degree": 295.1, "retrograde": False},
"Mercury": {"sign": "Aquarius", "degree": 312.8, "retrograde": True},
},
"aspects": [],
}
class SeedMapFeltTest(FunctionalTest):
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200) # portrait — clicking hex-area btns
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman", "card_count": 106, "is_default": True,
"is_polarized": True, "has_card_images": False},
)
# One card is enough: hand_complete is purely len(hand) >= 6, and STEP-1
# rendering uses PLACEHOLDER seeds, so card identity is irrelevant. Seed
# it explicitly — TransactionTestCase flushes migration cards (no
# serialized_rollback). [[feedback-transactiontestcase-flush]]
self.card, _ = TarotCard.objects.get_or_create(
deck_variant=self.earthman, slug="seed-fixture-em",
defaults={"arcana": "MAJOR", "suit": None, "number": 0, "name": "Fixture"},
)
self.gamer, _ = User.objects.get_or_create(email="founder@test.io")
self.gamer.unlocked_decks.add(self.earthman)
self.gamer.equipped_deck = self.earthman
self.gamer.save(update_fields=["equipped_deck"])
def _seed_room(self, hand_len=6):
"""SKY_SELECT room, founder seated PC, with a CONFIRMED Character whose
celtic_cross holds `hand_len` placed cards. hand_len>=6 => hand_complete
=> SEED MAP is the live phase btn + the felt renders."""
room, seat = _make_sky_confirmed_room(self.live_server_url, self.gamer, self.earthman)
# Pre-store the table's own sky so the rim renders without the lazy
# PySwiss compute (no live HTTP from FTs).
room.sky_chart = _ROOM_SKY
room.save(update_fields=["sky_chart"])
hand = [
{"position": p, "card_id": self.card.id, "reversed": False, "polarity": "gravity"}
for p in _HAND_POSITIONS[:hand_len]
]
Character.objects.create(
seat=seat, significator=seat.significator or self.card,
chart_data={"planets": {}}, confirmed_at=timezone.now(),
celtic_cross={"spread": "waite-smith", "hand": hand},
)
return room
def _room_url(self, room):
return self.live_server_url + reverse("epic:room", kwargs={"room_id": room.id})
def _open_seed_felt(self, room):
"""Load the room, wait for SEED MAP to go live, click it open."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self._room_url(room))
# hand_complete → SEED MAP is the live phase btn (loses hex-phase-btn--out).
self.wait_for(lambda: self.assertNotIn(
"hex-phase-btn--out",
self.browser.find_element(By.ID, "id_seed_map_btn").get_attribute("class"),
))
# Click opens the felt. JS-click + retry absorbs the parse-time bind race
# (mirrors _click_and_assert_sea_open in test_game_room_select_sea.py).
def _click_and_assert_open():
btn = self.browser.find_element(By.ID, "id_seed_map_btn")
self.browser.execute_script("arguments[0].click()", btn)
self.assertTrue(self.browser.execute_script(
"return document.documentElement.classList.contains('seed-open')"
))
self.wait_for(_click_and_assert_open)
def test_seed_map_felt_opens_and_paints_dual_graph(self):
room = self._seed_room(hand_len=6)
self._open_seed_felt(room)
self.assertTrue(
self.browser.find_element(By.ID, "id_seed_map_overlay").is_displayed()
)
# The d3-delaunay dual graph paints BOTH layers from placeholder seeds:
# Voronoi cells (territory) + Delaunay edges (adjacency).
self.wait_for(lambda: self.assertGreaterEqual(
self.browser.execute_script(
"return document.querySelectorAll('#id_seed_map_svg .voronoi-cell').length"
), 1,
))
self.assertGreaterEqual(
self.browser.execute_script(
"return document.querySelectorAll('#id_seed_map_svg .delaunay-edge').length"
), 1,
)
def test_seed_map_felt_rings_tessellation_with_room_sky_rim(self):
"""Step 2's shared frame: the stripped sky wheel — canonical signs ring +
the ROOM's own planets, NOTHING location-bound (no houses/axes) and none
of the stripped chrome (element ring / centre disc / aspect web) — rings
the tessellation, which shrinks into the wheel's freed hub."""
room = self._seed_room(hand_len=6)
self._open_seed_felt(room)
# The rim paints: 12 canonical sign slices + the room sky's 3 planets.
self.wait_for(lambda: self.assertEqual(
self.browser.execute_script(
"return document.querySelectorAll('#id_seed_wheel_svg .nw-sign-group').length"
), 12,
))
self.assertEqual(
self.browser.execute_script(
"return document.querySelectorAll('#id_seed_wheel_svg .nw-planet-group').length"
), 3,
)
# The strip: no element ring / centre disc / aspect web, and no
# location-bound layers (houses, ASC/MC axes) on a shared rim.
for cls in ["nw-elements", "nw-inner-disc", "nw-aspects", "nw-houses", "nw-axes"]:
self.assertEqual(
self.browser.execute_script(
f"return document.querySelectorAll('#id_seed_wheel_svg .{cls}').length"
), 0, f"expected no .{cls} on the shared rim",
)
# The tessellation sits INSIDE the rim: the map svg is shrunk square
# into the wheel's hub (2 × hubR = 2 × 0.52 × 0.46 × wheel span) and
# carries the rimmed clip class.
map_w, map_h, col_w, col_h = self.browser.execute_script(
"const m = document.getElementById('id_seed_map_svg'),"
" c = m.closest('.seed-map-col');"
"return [m.clientWidth, m.clientHeight, c.clientWidth, c.clientHeight];"
)
self.assertEqual(map_w, map_h) # square
self.assertLess(map_w, min(col_w, col_h)) # inside the wheel
self.assertAlmostEqual( # = the freed hub
map_w, 2 * 0.52 * 0.46 * min(col_w, col_h), delta=2,
)
self.assertIn("voronoi-map--rimmed",
self.browser.find_element(By.ID, "id_seed_map_svg").get_attribute("class"))
# And the dual graph still paints inside the hub.
self.assertGreaterEqual(
self.browser.execute_script(
"return document.querySelectorAll('#id_seed_map_svg .voronoi-cell').length"
), 1,
)
def test_seed_map_felt_absent_until_hand_complete(self):
room = self._seed_room(hand_len=5) # only 5 placed → NOT hand_complete
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self._room_url(room))
self.wait_for(lambda: self.browser.find_element(By.ID, "id_seed_map_btn"))
# SEED MAP stays --out and the felt is never injected into the DOM.
self.assertIn(
"hex-phase-btn--out",
self.browser.find_element(By.ID, "id_seed_map_btn").get_attribute("class"),
)
self.assertEqual(self.browser.find_elements(By.ID, "id_seed_map_overlay"), [])