feat: clock + place games
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from events.models import Event
|
||||
from games.models import Game
|
||||
|
||||
from .broadcast import clock_group
|
||||
from .models import PRESS_BONUS_SECONDS, ClockTimer, Press, random_cooldown_seconds
|
||||
from .wire import encode, to_epoch_ms
|
||||
|
||||
|
||||
class ClockConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.event_path = self.scope["url_route"]["kwargs"]["event_path"]
|
||||
self.event = await self._get_enabled_event(self.event_path)
|
||||
if self.event is None:
|
||||
await self.close()
|
||||
return
|
||||
|
||||
self.group_name = clock_group(self.event_path)
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
await self.accept()
|
||||
await self.send(text_data=await self._initial_state(self.scope.get("user")))
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
if getattr(self, "group_name", None):
|
||||
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||
|
||||
async def receive(self, text_data=None, bytes_data=None):
|
||||
if text_data == "press":
|
||||
await self._handle_press()
|
||||
|
||||
async def _handle_press(self):
|
||||
user = self.scope.get("user")
|
||||
if user is None or not user.is_authenticated:
|
||||
await self.send(text_data=encode("r", "a"))
|
||||
return
|
||||
|
||||
result = await self._record_press(user)
|
||||
|
||||
if result["rejected"]:
|
||||
await self.send(text_data=encode("r", "c", result["next"], result["now"]))
|
||||
return
|
||||
|
||||
# The cooldown outcome is only useful to the player who just pressed -
|
||||
# broadcasting it to every connection would be pure waste.
|
||||
await self.send(text_data=encode("k", result["next"], result["now"]))
|
||||
|
||||
# Everyone else only needs to know the shared timer moved.
|
||||
await self.channel_layer.group_send(
|
||||
self.group_name,
|
||||
{
|
||||
"type": "tick",
|
||||
"end": result["end"],
|
||||
"now": result["now"],
|
||||
"pressed": True,
|
||||
},
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def _get_enabled_event(self, event_path):
|
||||
event = Event.objects.filter(path=event_path).first()
|
||||
if event is None:
|
||||
return None
|
||||
if not Game.objects.enabled_for_event(event).filter(path="clock").exists():
|
||||
return None
|
||||
return event
|
||||
|
||||
@database_sync_to_async
|
||||
def _record_press(self, user):
|
||||
with transaction.atomic():
|
||||
timer = ClockTimer.objects.select_for_update().get(event=self.event)
|
||||
now = timezone.now()
|
||||
timer.reset_if_expired(now)
|
||||
|
||||
last_press = (
|
||||
Press.objects.filter(player=user, event=self.event)
|
||||
.order_by("-pressed_at")
|
||||
.first()
|
||||
)
|
||||
if last_press is not None:
|
||||
next_allowed = last_press.pressed_at + timedelta(
|
||||
seconds=last_press.cooldown_seconds
|
||||
)
|
||||
if next_allowed > now:
|
||||
return {
|
||||
"rejected": True,
|
||||
"next": to_epoch_ms(next_allowed),
|
||||
"now": to_epoch_ms(now),
|
||||
}
|
||||
|
||||
remaining = (timer.end_time - now).total_seconds()
|
||||
cooldown = random_cooldown_seconds()
|
||||
press = Press.objects.create(
|
||||
event=self.event,
|
||||
player=user,
|
||||
remaining_before_press=remaining,
|
||||
seconds_added=PRESS_BONUS_SECONDS,
|
||||
cooldown_seconds=cooldown,
|
||||
)
|
||||
timer.end_time = timer.end_time + timedelta(seconds=PRESS_BONUS_SECONDS)
|
||||
timer.save(update_fields=["end_time"])
|
||||
|
||||
return {
|
||||
"rejected": False,
|
||||
"end": to_epoch_ms(timer.end_time),
|
||||
"now": to_epoch_ms(now),
|
||||
"next": to_epoch_ms(press.pressed_at + timedelta(seconds=cooldown)),
|
||||
}
|
||||
|
||||
@database_sync_to_async
|
||||
def _initial_state(self, user):
|
||||
timer = ClockTimer.current(self.event)
|
||||
timer.reset_if_expired()
|
||||
now = timezone.now()
|
||||
|
||||
next_press_at = None
|
||||
if user is not None and user.is_authenticated:
|
||||
last_press = (
|
||||
Press.objects.filter(player=user, event=self.event)
|
||||
.order_by("-pressed_at")
|
||||
.first()
|
||||
)
|
||||
if last_press is not None:
|
||||
candidate = last_press.pressed_at + timedelta(
|
||||
seconds=last_press.cooldown_seconds
|
||||
)
|
||||
if candidate > now:
|
||||
next_press_at = to_epoch_ms(candidate)
|
||||
|
||||
return encode(
|
||||
"s",
|
||||
to_epoch_ms(timer.end_time),
|
||||
to_epoch_ms(now),
|
||||
timer.initial_duration,
|
||||
PRESS_BONUS_SECONDS,
|
||||
next_press_at,
|
||||
)
|
||||
|
||||
async def tick(self, event):
|
||||
await self.send(
|
||||
text_data=encode(
|
||||
"t",
|
||||
event["end"],
|
||||
event["now"],
|
||||
"1" if event.get("pressed") else "",
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user