Files
polylan-submitter/polylan_submitter/clock/models.py
T
2026-09-17 01:33:54 +02:00

80 lines
2.7 KiB
Python

import random
from datetime import timedelta
from django.conf import settings
from django.db import models
from django.utils import timezone
from events.models import Event
from events.scoping import EventScopedManager
DEFAULT_TIMER_DURATION = 60
PRESS_BONUS_SECONDS = 5
PRESS_COOLDOWN_MIN_SECONDS = PRESS_BONUS_SECONDS
PRESS_COOLDOWN_MAX_SECONDS = PRESS_BONUS_SECONDS * 2
def random_cooldown_seconds() -> float:
return random.uniform(PRESS_COOLDOWN_MIN_SECONDS, PRESS_COOLDOWN_MAX_SECONDS)
class ClockTimer(models.Model):
"""The shared countdown for one event. One row per event, reset in place.
Unlike the enable/disable toggle for every other game (EventGame.enabled,
checked before a client can even reach this app), there's no separate
is_enabled flag here - the registry is the single source of truth.
"""
event = models.OneToOneField(
Event, on_delete=models.CASCADE, related_name="clock_timer"
)
initial_duration = models.PositiveIntegerField(default=DEFAULT_TIMER_DURATION)
end_time = models.DateTimeField()
def __str__(self):
return f"ClockTimer({self.event}, end_time={self.end_time.isoformat()})"
@classmethod
def current(cls, event: Event) -> "ClockTimer":
"""Fetch the event's singleton timer, creating it on first-ever run."""
obj, _ = cls.objects.get_or_create(
event=event,
defaults={
"end_time": timezone.now() + timedelta(seconds=DEFAULT_TIMER_DURATION)
},
)
return obj
def reset_if_expired(self, now=None) -> bool:
"""Resets end_time back to a fresh window if it has passed. Returns True if reset."""
now = now or timezone.now()
if self.end_time > now:
return False
self.end_time = now + timedelta(seconds=self.initial_duration)
self.save(update_fields=["end_time"])
return True
class Press(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="clock_presses")
player = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="clock_presses"
)
pressed_at = models.DateTimeField(auto_now_add=True)
remaining_before_press = models.FloatField()
seconds_added = models.PositiveIntegerField(default=PRESS_BONUS_SECONDS)
# Randomized 1x-2x PRESS_BONUS_SECONDS at press time and stored, since it's no
# longer a fixed constant that can be recomputed later.
cooldown_seconds = models.FloatField()
objects = models.Manager()
for_event = EventScopedManager()
class Meta:
ordering = ["-pressed_at"]
def __str__(self):
return f"{self.player} pressed at {self.remaining_before_press:.1f}s left ({self.event})"