Set the Game Clock — increment 1: the position-circle-6 gamer places Uranus in a sign on the shared game wheel — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

The SEED MAP rim's planets are no longer auto-computed — they are PLACED by the
gamers in turn (circle 6→1, Uranus→Saturn→Jupiter→Mars→Sun→Moon; Mercury/Venus
Sun-derived; Neptune/Pluto excluded). This is the first turn: the wheel starts
SIGNS-ONLY (planets eliminated) and the gamer at POSITION CIRCLE 6 places Uranus
by clicking a sign wedge. Roadmap step 21. See project_voronoi_spec.

KEYED ON THE POSITION CIRCLE (slot_number), NOT the role — select_role assigns
roles freely, so position 6 can hold any role; in a generic room BC merely
defaults there. Tests INVERT the slot→role defaults (position 6 = PC, position 1
= BC) so a role-keyed bug would fail them.

- Room.clock_placements JSONField (migration 0020): {planet: sign}, the ritual
  state. The 9ed8771 auto-sky infra (table_sky / sky_chart / convened_at) stays
  DORMANT — future home of the resolved sky; the rim no longer reads it.
- epic.place_clock_planet POST {planet, sign}: the acting seat must be the circle
  whose turn it is (its circle's planet, every earlier planet placed, this one
  not yet) + a real zodiac sign → persists. _clock_placeable_for shared by the
  endpoint + the seed-felt ctx so gate & affordance can't drift. 403 wrong
  circle/turn/seat, 400 bad sign.
- SkyWheel.drawRim(svg, data, opts): opts.placeable turns the sign wedges into
  placement targets (.nw-sign--placeable + click → opts.onPickSign(sign)); still
  singleton-pure (no module writes — Jasmine R9).
- _seed_map_overlay.html: rim renders the placements (sign-midpoint glyph);
  empty → signs-only; the gamer whose turn it is gets the #id_clock_prompt + the
  clickable wedges → POST → adopt the server's placements + repaint (reload-safe).
- _sky.scss: .nw-sign--placeable re-enables pointer-events on the click-through
  rim + a hover brighten; the .clock-prompt label.
- ctx: clock_placements_json + clock_placeable in _role_select_context (drops the
  now-unused room_sky_json). Repointed the 9ed8771 rim FT/ITs (auto-sky →
  placements, empty = signs-only).
- Coverage: Jasmine drawRim R7–R9 (placement clickable / inert without opts /
  singleton-safe); epic PlaceClockPlanetTest (position-keyed, role-inverted) +
  repointed rim ITs; FT position-6 places Uranus → glyph + persist. 1021
  epic+gameboard ITs green; live-verified in Firefox.

DEFERRED: turn progression 5→1 + WS live-broadcast (increment 2); the CSP
ephemeris narrowing + resolving placements → a datetime (later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-10 01:06:14 -04:00
parent 9ed877168e
commit 14afb108c0
11 changed files with 557 additions and 96 deletions

View File

@@ -696,11 +696,14 @@ def _role_select_context(room, user, seat_param=None):
if (sky_confirmed and confirmed_char.chart_data)
else "null"
)
# The table's OWN sky — the shared SEED MAP rim frame (one canonical
# frame for all six gamers, NOT this seat's natal chart). None until
# the lazy table_sky endpoint computes + caches it; the felt then
# fetches it on open.
ctx["room_sky_json"] = json.dumps(room.sky_chart) if room.sky_chart else None
# Set the Game Clock — the shared SEED MAP wheel's planets are PLACED by
# the gamers (Room.clock_placements), one canonical frame for all six.
# The rim renders the placements; the gamer whose POSITION CIRCLE's turn
# it is gets the placement affordance (`clock_placeable` = their circle's
# planet, keyed on slot_number not role). [[project-voronoi-spec]]
_placements = room.clock_placements or {}
ctx["clock_placements_json"] = json.dumps(_placements)
ctx["clock_placeable"] = _clock_placeable_for(_sky_seat, _placements)
if sky_confirmed:
# Fall back to seat.significator for Characters created before the sync was added
ctx["my_tray_sig"] = confirmed_char.significator or _sky_seat.significator
@@ -1873,6 +1876,63 @@ def table_sky(request, room_id):
return JsonResponse(room.sky_chart)
# Set the Game Clock ritual ([[project-voronoi-spec]]): the six seats place a
# planet each in turn, circle 6 → 1, by decreasing orbital period so each turn
# narrows the start-time window. Mercury + Venus are Sun-bound (≤28°/≤48°
# elongation) → DERIVED, not placed; Neptune/Pluto excluded (too slow to pin a
# time). The CSP sign-narrowing + resolving placements → a datetime are later.
CLOCK_PLANET_BY_SLOT = {6: "Uranus", 5: "Saturn", 4: "Jupiter", 3: "Mars", 2: "Sun", 1: "Moon"}
CLOCK_ORDER = ["Uranus", "Saturn", "Jupiter", "Mars", "Sun", "Moon"] # placement order
_ZODIAC_SIGNS = {
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
}
def _clock_placeable_for(seat, placements):
"""The planet `seat`'s gamer may place RIGHT NOW — its circle's planet, when
it's that planet's turn (every earlier planet placed, this one not yet) — or
None. Shared by the placement endpoint + the seed-felt context so the gate
and the affordance can't drift apart."""
if seat is None:
return None
planet = CLOCK_PLANET_BY_SLOT.get(seat.slot_number)
if planet is None or planet in placements:
return None
idx = CLOCK_ORDER.index(planet)
if any(p not in placements for p in CLOCK_ORDER[:idx]):
return None
return planet
@login_required
def place_clock_planet(request, room_id):
"""Set the Game Clock — place a planet in a sign on the shared game wheel
([[project-voronoi-spec]]). POST {planet, sign}. The acting seat must be the
circle whose turn it is (circle 6 places Uranus first, then 5→1); the posted
planet must be exactly the one that seat may place now; the sign must be a
real zodiac sign. Persists to Room.clock_placements. Returns {ok, placements}
200 · 400 bad sign · 403 not your seat / not your turn / not seated.
"""
if request.method != "POST":
return HttpResponse(status=405)
room = Room.objects.get(id=room_id)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponse(status=403)
planet = (request.POST.get("planet") or "").strip()
sign = (request.POST.get("sign") or "").strip()
if sign not in _ZODIAC_SIGNS:
return JsonResponse({"error": "invalid_sign"}, status=400)
placements = dict(room.clock_placements or {})
if planet != _clock_placeable_for(seat, placements):
return HttpResponse(status=403)
placements[planet] = sign
room.clock_placements = placements
room.save(update_fields=["clock_placements"])
return JsonResponse({"ok": True, "placements": placements})
@login_required
def sky_save(request, room_id):
"""Create or update the draft Character for the requesting gamer's seat.