80 lines
2.7 KiB
Python
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})"
|