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>
198 lines
9.3 KiB
Python
198 lines
9.3 KiB
Python
"""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"), [])
|