Files
python-tdd/src/apps/epic/utils.py

101 lines
3.7 KiB
Python
Raw Normal View History

from django.db.models import Q
from apps.epic.models import Room, RoomInvite
# ── Game-wide constants ────────────────────────────────────────────────────
# Reversal probability applied to any card pulled from a stack, anywhere in
# the game (PICK SEA initially; future phases — gameplay draws etc. — will
# share this single source of truth). Stub for a future per-user profile
# override: callers MUST go through stack_reversal_probability(user, room)
# rather than referencing the constant directly so the user-config hookup is
# a one-line change inside the helper.
STACK_REVERSAL_PROBABILITY = 0.25
def stack_reversal_probability(user=None, room=None):
"""Reversal probability for a draw stack in this user's context.
Current behavior: returns the module default for everyone. Plumbing point
for a forthcoming per-user setting when that lands, swap the body to
something like `return getattr(user.profile, 'reversal_rate', STACK_REVERSAL_PROBABILITY)`
and every call site picks up the per-user value automatically.
"""
return STACK_REVERSAL_PROBABILITY
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
# Element key → in-game capacitor name (mirrors ELEMENT_INFO in natus-wheel.js).
# Used by the SKY_SAVED provenance event to render prose like
# "yields them a unique Ardor capacity."
ELEMENT_CAPACITOR_NAMES = {
"Fire": "Ardor",
"Stone": "Ossum",
"Time": "Tempo",
"Space": "Nexus",
"Air": "Pneuma",
"Water": "Humor",
}
# Canonical clockwise-ring ordering for tie-break and prose joining.
ELEMENT_ORDER = ["Fire", "Stone", "Time", "Space", "Air", "Water"]
def top_capacitors(elements):
"""Return capacitor names tied for the highest count in `elements`.
`elements` is the chart-data dict whose values are either ints (raw counts)
or {"count": int, ...} enriched dicts. Order follows ELEMENT_ORDER so tied
output is deterministic across runs and matches the wheel's visual order.
"""
if not elements:
return []
def _count(v):
return v.get("count", 0) if isinstance(v, dict) else (v or 0)
counts = {k: _count(v) for k, v in elements.items()}
if not counts or max(counts.values()) <= 0:
return []
top = max(counts.values())
return [
ELEMENT_CAPACITOR_NAMES[k]
for k in ELEMENT_ORDER
if counts.get(k) == top and k in ELEMENT_CAPACITOR_NAMES
]
def _planet_house(degree, cusps):
"""Return 1-based house number for a planet at ecliptic degree.
cusps is the 12-element list from PySwiss where cusps[i] is the start of
house i+1. Handles the wrap-around case where a cusp crosses 0°/360°.
"""
degree = degree % 360
for i in range(12):
start = cusps[i] % 360
end = cusps[(i + 1) % 12] % 360
if start < end:
if start <= degree < end:
return i + 1
else: # wrap-around: e.g. cusp at 350° → next at 10°
if degree >= start or degree < end:
return i + 1
return 1
def _compute_distinctions(planets, houses):
"""Return dict {house_number_str: planet_count} for all 12 houses."""
cusps = houses['cusps']
counts = {str(i): 0 for i in range(1, 13)}
for planet_data in planets.values():
h = _planet_house(planet_data['degree'], cusps)
counts[str(h)] += 1
return counts
def rooms_for_user(user):
"""Return a queryset of rooms the user owns, has a gate slot in, or is invited to."""
return Room.objects.filter(
Q(owner=user) |
Q(gate_slots__gamer=user) |
Q(invites__invitee_email=user.email, invites__status=RoomInvite.PENDING)
).distinct()