Set the Game Clock — increment 1: the position-circle-6 gamer places Uranus in a sign on the shared game wheel — TDD
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user