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})"