Initial Game Logic
This commit is contained in:
285
django/play/consumers/lobby.py
Normal file
285
django/play/consumers/lobby.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
import json
|
||||
import asyncio
|
||||
from ..models import QuizGame, QuizGameParticipant
|
||||
from channels.db import database_sync_to_async
|
||||
|
||||
# Dictionary to track inactive check tasks per game
|
||||
game_check_tasks = {}
|
||||
|
||||
class LobbyConsumer(AsyncWebsocketConsumer):
|
||||
@classmethod
|
||||
async def start_inactive_check(cls, join_code, channel_layer):
|
||||
"""Start the inactive player check for a game if not already running."""
|
||||
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
|
||||
async def check_inactive_players():
|
||||
room_group_name = f'lobby_{join_code}'
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
# Get participants list and broadcast update
|
||||
try:
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
|
||||
cutoff_time = timezone.now() - timedelta(minutes=1)
|
||||
# Get previous active participants
|
||||
prev_active = await database_sync_to_async(lambda: set(
|
||||
game.participants.values_list('display_name', flat=True)
|
||||
))()
|
||||
|
||||
# Get current active participants
|
||||
active_participants = await database_sync_to_async(lambda: list(
|
||||
game.participants.filter(last_heartbeat__gte=cutoff_time)
|
||||
.values_list('display_name', flat=True)
|
||||
))()
|
||||
|
||||
# Find removed participants
|
||||
removed = set(prev_active) - set(active_participants)
|
||||
|
||||
# Send notifications for removed participants
|
||||
for name in removed:
|
||||
await channel_layer.group_send(
|
||||
room_group_name,
|
||||
{
|
||||
'type': 'player_left',
|
||||
'player_name': name,
|
||||
'was_kicked': True
|
||||
}
|
||||
)
|
||||
|
||||
# Broadcast update to all clients
|
||||
await channel_layer.group_send(
|
||||
room_group_name,
|
||||
{
|
||||
'type': 'participants_list_update',
|
||||
'participants': active_participants
|
||||
}
|
||||
)
|
||||
except QuizGame.DoesNotExist:
|
||||
break # Stop checking if game no longer exists
|
||||
except Exception as e:
|
||||
print(f'Error checking inactive players: {e}')
|
||||
await asyncio.sleep(5) # Wait before retry
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'Error in inactive check loop: {e}')
|
||||
await asyncio.sleep(5)
|
||||
|
||||
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
|
||||
|
||||
async def connect(self):
|
||||
self.join_code = self.scope['url_route']['kwargs']['join_code']
|
||||
self.room_group_name = f'lobby_{self.join_code}'
|
||||
|
||||
# Get participant ID from session
|
||||
self.participant_id = self.scope['session'].get('participant_id')
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
# Send initial participants list
|
||||
await self.update_participants()
|
||||
|
||||
# Ensure inactive check is running for this game
|
||||
await self.start_inactive_check(self.join_code, self.channel_layer)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def receive(self, text_data):
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get('type')
|
||||
print(f'Received message type: {message_type}')
|
||||
|
||||
if message_type == 'ping':
|
||||
# Respond with pong to keep connection alive
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'pong'
|
||||
}))
|
||||
return
|
||||
|
||||
if message_type == 'heartbeat':
|
||||
participant_id = data.get('participant_id')
|
||||
if participant_id:
|
||||
await self.update_participant_heartbeat(participant_id)
|
||||
# Update participants list after heartbeat
|
||||
await self.update_participants()
|
||||
elif message_type in ['leave_game', 'kick_player']:
|
||||
participant_id = data.get('participant_id')
|
||||
if participant_id:
|
||||
try:
|
||||
# Get participant info before deletion
|
||||
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(participant_id=participant_id)
|
||||
display_name = participant.display_name
|
||||
|
||||
# For kick_player, verify that the request comes from the host
|
||||
if message_type == 'kick_player':
|
||||
game = await database_sync_to_async(lambda: participant.quiz_game)()
|
||||
# Get host_id from message
|
||||
host_id = data.get('host_id')
|
||||
print(f'Kick attempt - Game host_id: {game.host_id}, Message host_id: {host_id}')
|
||||
if not host_id:
|
||||
print('No host_id in message')
|
||||
return
|
||||
if str(game.host_id) != str(host_id):
|
||||
print(f'Host ID mismatch: {game.host_id} != {host_id}')
|
||||
return
|
||||
|
||||
# Also verify against cookie as backup
|
||||
cookie_host_id = self.scope.get('cookies', {}).get('host_id')
|
||||
if not cookie_host_id or str(cookie_host_id) != str(host_id):
|
||||
print(f'Cookie host_id mismatch: {cookie_host_id} != {host_id}')
|
||||
return
|
||||
|
||||
# Delete the participant
|
||||
await database_sync_to_async(participant.delete)()
|
||||
|
||||
# Notify other players
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'player_left',
|
||||
'player_name': display_name,
|
||||
'was_kicked': message_type == 'kick_player',
|
||||
'participant_id': participant_id
|
||||
}
|
||||
)
|
||||
|
||||
# Update participants list
|
||||
await self.update_participants()
|
||||
|
||||
# If it's the kicked player's connection, send them home
|
||||
if participant_id == self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'redirect',
|
||||
'url': '/'
|
||||
}))
|
||||
except QuizGameParticipant.DoesNotExist:
|
||||
pass
|
||||
elif message_type == 'update_participants':
|
||||
await self.update_participants()
|
||||
elif message_type == 'start_game':
|
||||
host_id = data.get('host_id')
|
||||
if host_id:
|
||||
success = await self.start_game(host_id)
|
||||
if success:
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'game_state_update',
|
||||
'action': 'start_game',
|
||||
'redirect_url': f'/play/game/{self.join_code}/question'
|
||||
}
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def update_participant_heartbeat(self, participant_id):
|
||||
try:
|
||||
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
|
||||
from django.utils import timezone
|
||||
participant.last_heartbeat = timezone.now()
|
||||
participant.save()
|
||||
return True
|
||||
except QuizGameParticipant.DoesNotExist:
|
||||
return False
|
||||
|
||||
async def get_participants_list(self):
|
||||
try:
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code)
|
||||
# Only return participants that have been seen in the last minute
|
||||
cutoff_time = timezone.now() - timedelta(minutes=1)
|
||||
participants = await database_sync_to_async(lambda: list(
|
||||
game.participants.filter(last_heartbeat__gte=cutoff_time)
|
||||
.values('participant_id', 'display_name')
|
||||
))()
|
||||
|
||||
|
||||
# Send join notification for new participants
|
||||
try:
|
||||
prev_names = set(p['display_name'] for p in self.last_participants) if hasattr(self, 'last_participants') else set()
|
||||
current_names = set(p['display_name'] for p in participants)
|
||||
new_participants = current_names - prev_names
|
||||
|
||||
for name in new_participants:
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'player_joined',
|
||||
'player_name': name
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Error sending join notifications: {e}')
|
||||
|
||||
self.last_participants = participants
|
||||
return participants
|
||||
except QuizGame.DoesNotExist:
|
||||
return []
|
||||
|
||||
async def update_participants(self):
|
||||
participants = await self.get_participants_list()
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'participants_list_update',
|
||||
'participants': participants
|
||||
}
|
||||
)
|
||||
|
||||
async def participants_list_update(self, event):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'participants_list_update',
|
||||
'participants': event['participants']
|
||||
}))
|
||||
|
||||
@database_sync_to_async
|
||||
def start_game(self, host_id):
|
||||
try:
|
||||
game = QuizGame.objects.get(join_code=self.join_code)
|
||||
if str(game.host_id) == str(host_id):
|
||||
from django.utils import timezone
|
||||
game.current_state = 'question'
|
||||
game.question_start_time = timezone.now()
|
||||
game.save()
|
||||
return True
|
||||
except QuizGame.DoesNotExist:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def game_state_update(self, event):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'game_state_update',
|
||||
'action': event['action'],
|
||||
'redirect_url': event.get('redirect_url')
|
||||
}))
|
||||
|
||||
async def player_joined(self, event):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'player_joined',
|
||||
'player_name': event['player_name']
|
||||
}))
|
||||
|
||||
async def player_left(self, event):
|
||||
message = {
|
||||
'type': 'player_left',
|
||||
'player_name': event['player_name'],
|
||||
'was_kicked': event.get('was_kicked', False)
|
||||
}
|
||||
if event.get('was_kicked') and event.get('participant_id'):
|
||||
message['participant_id'] = event['participant_id']
|
||||
await self.send(text_data=json.dumps(message))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user