SEED MAP felt: 2D d3-delaunay Voronoi/Delaunay dual graph (roadmap step 21, Step 1) — TDD
New inline --duoUser felt (sibling of CAST SKY / DRAW SEA) that opens on #id_seed_map_btn once the 6-card sea hand completes (hand_complete); paints a Voronoi cell layer (territory) + a Delaunay edge layer (adjacency) from PLACEHOLDER seeds. Card-driven seeding (the 6 Celtic-Cross cards) is Step 2. - voronoi-map.js: window.SeedMap.draw/drawPlaceholder/clear over the bundled d3.min.js (d3-delaunay ships in the v7.9.0 UMD bundle — no new dep); a ResizeObserver re-tessellates to fill the felt on resize; data-seed reads d3's ring.index (survives skipped/degenerate cells in Step 2) - _seed_map_overlay.html felt + room.html include + has-seed-stage (gated on hand_complete) - three-way felt close (T3): openSeed closes sky+sea; openSky/openSea reciprocally close seed - .room-menu-seed gear NVM pane + room-views.js seed-open branch - _sky.scss felt block (T1: no aperture-fill light; T2: chained selector; fills the pane) + _room.scss aperture pin - VoronoiMapSpec Jasmine + game_room_seed_map FT + 5 felt ITs; CarteTray NVM count 2->3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
119
src/functional_tests/test_game_room_seed_map.py
Normal file
119
src/functional_tests/test_game_room_seed_map.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Functional test for the SEED MAP felt — Voronoi map, roadmap step 21, Step 1.
|
||||
|
||||
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.
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
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)
|
||||
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 test_seed_map_felt_opens_and_paints_dual_graph(self):
|
||||
room = self._seed_room(hand_len=6)
|
||||
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)
|
||||
|
||||
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_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"), [])
|
||||
Reference in New Issue
Block a user