From 3f07ce4ec75a727af2465e123b0f4024cd99ba09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhn-Vitus=20Sa=C3=9F?= Date: Sun, 6 Apr 2025 15:57:42 +0200 Subject: [PATCH] Initial Game Logic 2 --- django/library/models.py | 26 +- django/library/views.py | 4 +- django/play/consumers.py | 410 ++++++++++++++++++ django/play/models.py | 31 +- django/play/routing.py | 6 +- django/play/urls.py | 6 +- django/play/views.py | 198 +++++++-- django/templates/base.html | 44 ++ django/templates/library/overview_quiz.html | 24 +- django/templates/play/game/question_host.html | 152 +++++-- .../play/game/question_participant.html | 132 +++++- .../templates/play/game/score_overview.html | 131 +++++- .../play/game/wait_for_other_players.html | 56 ++- .../play/initialize_participant.html | 66 ++- django/templates/play/lobby.html | 371 ++++++++++++++-- 15 files changed, 1506 insertions(+), 151 deletions(-) diff --git a/django/library/models.py b/django/library/models.py index f49d071..ceb2eb5 100644 --- a/django/library/models.py +++ b/django/library/models.py @@ -38,6 +38,8 @@ class QivipQuiz(models.Model): description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...") difficulty= models.CharField(max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit") credits=models.TextField(blank=True, editable=True) + average_rating = models.FloatField(default=3.0) + rating_count = models.IntegerField(default=0) def __str__(self): return self.name @@ -47,7 +49,7 @@ class QivipQuiz(models.Model): # Create your models here. class QivipQuestion(models.Model): uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='question') + quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions') creation_date = models.DateTimeField(auto_now_add=True) update_date = models.DateTimeField(auto_now=True) data = models.TextField() @@ -61,6 +63,28 @@ class Tag(models.Model): def __str__(self): return self.name +class QuizRating(models.Model): + quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='ratings') + participant_id = models.CharField(max_length=200) + rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)]) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('quiz', 'participant_id') + + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + + if is_new: + # Update quiz average rating + quiz = self.quiz + total_ratings = quiz.ratings.count() + avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg'] + quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0 + quiz.rating_count = total_ratings + quiz.save() + class QuizCategory(models.Model): name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True, null=True) diff --git a/django/library/views.py b/django/library/views.py index e05de20..0211216 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -22,11 +22,11 @@ def overview_quiz(request): try: - filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('question')) + filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions')) except: filter_my_quizzes = QivipQuiz.objects.none() - filter_other_quizzes = QivipQuiz.objects.annotate(max_amout_questions=Count('question')) + filter_other_quizzes = QivipQuiz.objects.annotate(max_amout_questions=Count('questions')) diff --git a/django/play/consumers.py b/django/play/consumers.py index bf44a27..090d975 100644 --- a/django/play/consumers.py +++ b/django/play/consumers.py @@ -4,8 +4,263 @@ from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from django.utils import timezone from datetime import timedelta +from django.urls import reverse from .models import QuizGame, QuizGameParticipant +class GameConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.join_code = self.scope['url_route']['kwargs']['join_code'] + self.game_group_name = f'game_{self.join_code}' + + # Join game group + await self.channel_layer.group_add( + self.game_group_name, + self.channel_name + ) + + await self.accept() + + async def disconnect(self, close_code): + # Leave game group + await self.channel_layer.group_discard( + self.game_group_name, + self.channel_name + ) + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message_type = text_data_json['type'] + + if message_type == 'submit_answer': + participant_id = text_data_json['participant_id'] + answer = text_data_json['answer'] + time_remaining = text_data_json.get('time_remaining', 0) + + # Save answer and update participant score + await self.save_answer(participant_id, answer, time_remaining) + + # Notify host about the new answer + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'participant_answer', + 'answer': answer + } + ) + + elif message_type == 'get_answer_stats': + stats = await self.get_answer_stats() + await self.send(text_data=json.dumps({ + 'type': 'answer_stats', + 'stats': stats + })) + + elif message_type == 'update_participants': + participants = await self.get_participants() + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'participant_list_update', + 'participants': participants + } + ) + + elif message_type == 'next_question': + host_id = text_data_json['host_id'] + if await self.verify_host(host_id): + await self.advance_to_next_question() + + elif message_type == 'finish_game': + host_id = text_data_json['host_id'] + if await self.verify_host(host_id): + await self.finish_game() + + elif message_type == 'advance_to_scores': + host_id = text_data_json['host_id'] + if await self.verify_host(host_id): + await self.show_scores() + + async def participant_answer(self, event): + await self.send(text_data=json.dumps({ + 'type': 'participant_answer', + 'answer': event['answer'] + })) + + async def participant_list_update(self, event): + await self.send(text_data=json.dumps({ + 'type': 'participant_list_update', + 'participants': event['participants'] + })) + + 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') + })) + + @database_sync_to_async + def verify_host(self, host_id): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + return quiz_game.host_id == host_id + except QuizGame.DoesNotExist: + return False + + @database_sync_to_async + def save_answer(self, participant_id, answer_index, time_remaining): + try: + participant = QuizGameParticipant.objects.get( + quiz_game__join_code=self.join_code, + participant_id=participant_id + ) + quiz_game = participant.quiz_game + current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index] + + # Check if answer is correct + is_correct = False + if answer_index >= 0: # -1 means timeout + is_correct = current_question.data['options'][answer_index]['is_correct'] + + # Calculate score based on correctness and time + score = 0 + if is_correct: + base_score = 1000 + time_factor = time_remaining / 30000 # 30 seconds max + score = int(base_score * (0.5 + 0.5 * time_factor)) + + participant.score += score + participant.last_answer_correct = is_correct + participant.save() + + return True + except (QuizGameParticipant.DoesNotExist, IndexError): + return False + + @database_sync_to_async + def get_answer_stats(self): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game) + + # Count answers for each option + stats = {} + for participant in participants: + if participant.last_answer is not None: + stats[participant.last_answer] = stats.get(participant.last_answer, 0) + 1 + + return stats + except QuizGame.DoesNotExist: + return {} + + @database_sync_to_async + def get_participants(self): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game) + return [ + { + 'id': p.participant_id, + 'display_name': p.display_name, + 'score': p.score + } + for p in participants + ] + except QuizGame.DoesNotExist: + return [] + + @database_sync_to_async + def _advance_to_next_question(self): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + quiz = quiz_game.quiz_id + + # Move to next question + quiz_game.current_question_index += 1 + + # Check if we've reached the end + if quiz_game.current_question_index >= quiz.questions.count(): + quiz_game.current_state = 'finished' + quiz_game.save() + return 'finished' + else: + # Update game state and start time + quiz_game.current_state = 'question' + quiz_game.question_start_time = timezone.now() + quiz_game.save() + return 'question' + except QuizGame.DoesNotExist: + return None + + async def advance_to_next_question(self): + result = await self._advance_to_next_question() + if result == 'finished': + # Notify clients to redirect to finished page + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'game_state_update', + 'action': 'finish_game', + 'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code}) + } + ) + elif result == 'question': + # Notify clients to redirect to next question + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'game_state_update', + 'action': 'next_question', + 'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code}) + } + ) + + @database_sync_to_async + def _show_scores(self): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + quiz_game.current_state = 'scores' + quiz_game.save() + return True + except QuizGame.DoesNotExist: + return False + + async def show_scores(self): + success = await self._show_scores() + if success: + # Notify clients to redirect to scores page + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'game_state_update', + 'action': 'show_scores', + 'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code}) + } + ) + + @database_sync_to_async + def _finish_game(self): + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + quiz_game.current_state = 'finished' + quiz_game.save() + return True + except QuizGame.DoesNotExist: + return False + + async def finish_game(self): + success = await self._finish_game() + if success: + # Notify clients to redirect to finished page + await self.channel_layer.group_send( + self.game_group_name, + { + 'type': 'game_state_update', + 'action': 'finish_game', + 'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code}) + } + ) + class LobbyConsumer(AsyncWebsocketConsumer): async def connect(self): self.join_code = self.scope['url_route']['kwargs']['join_code'] @@ -63,6 +318,96 @@ class LobbyConsumer(AsyncWebsocketConsumer): 'participants': participants } ) + + elif message_type == 'start_game': + # Verify sender is host + sender_id = text_data_json.get('host_id') + if await self._is_host(sender_id): + # Update game state and notify all participants + await self._start_game() + 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}/0' + } + ) + + async def game_state_update(self, event): + # Send game state update to WebSocket + await self.send(text_data=json.dumps({ + 'type': 'game_state_update', + 'action': event['action'], + 'redirect_url': event.get('redirect_url') + })) + + @database_sync_to_async + def _start_game(self): + game = QuizGame.objects.get(join_code=self.join_code) + game.current_state = 'question' + game.current_question_index = 0 + game.question_start_time = timezone.now() + game.save() + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message_type = text_data_json['type'] + + if message_type == 'heartbeat': + # Update participant's last activity timestamp + participant_id = text_data_json.get('participant_id') + if participant_id: + await self._update_participant_heartbeat(participant_id) + + elif message_type == 'update_participants': + participants = await self.get_participants() + await self.channel_layer.group_send( + self.room_group_name, + { + 'type': 'participant_list_update', + 'participants': participants + } + ) + + elif message_type == 'start_game': + # Verify sender is host + sender_id = text_data_json.get('host_id') + if await self._is_host(sender_id): + # Update game state and notify all participants + await self._start_game() + 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}/0' + } + ) + + elif message_type == 'submit_answer': + participant_id = text_data_json.get('participant_id') + answer = text_data_json.get('answer') + time_remaining = text_data_json.get('time_remaining') + if participant_id: + score = await self._process_answer(participant_id, answer, time_remaining) + await self.send(text_data=json.dumps({ + 'type': 'answer_processed', + 'score': score + })) + + elif message_type == 'next_question': + sender_id = text_data_json.get('host_id') + if await self._is_host(sender_id): + next_state = await self._advance_game_state() + await self.channel_layer.group_send( + self.room_group_name, + { + 'type': 'game_state_update', + 'action': next_state['action'], + 'redirect_url': next_state['redirect_url'] + } + ) async def participant_list_update(self, event): participants = event['participants'] @@ -84,6 +429,71 @@ class LobbyConsumer(AsyncWebsocketConsumer): participant.save() # This will update last_heartbeat due to auto_now=True except QuizGameParticipant.DoesNotExist: pass + + @database_sync_to_async + def _is_host(self, host_id): + try: + return QuizGame.objects.filter(join_code=self.join_code, host_id=host_id).exists() + except QuizGame.DoesNotExist: + return False + + @database_sync_to_async + def _start_game(self): + game = QuizGame.objects.get(join_code=self.join_code) + game.current_state = 'question' + game.current_question_index = 0 + game.question_start_time = timezone.now() + game.save() + + @database_sync_to_async + def _process_answer(self, participant_id, answer, time_remaining): + game = QuizGame.objects.get(join_code=self.join_code) + participant = QuizGameParticipant.objects.get(participant_id=participant_id) + question = game.quiz_id.questions.all()[game.current_question_index] + + # Load question data + question_data = json.loads(question.data) + correct_answer = question_data.get('correct_answer') + + # Calculate score based on correctness and time + score = 0 + if answer == correct_answer: + # Base score for correct answer + bonus for speed + score = 1000 + int(time_remaining * 10) # 10 points per remaining second + + participant.score += score + participant.save() + return score + + @database_sync_to_async + def _advance_game_state(self): + game = QuizGame.objects.get(join_code=self.join_code) + total_questions = game.quiz_id.questions.count() + + if game.current_state == 'question': + game.current_state = 'scores' + game.save() + return { + 'action': 'show_scores', + 'redirect_url': f'/play/game/{self.join_code}/scores' + } + elif game.current_state == 'scores': + if game.current_question_index + 1 < total_questions: + game.current_state = 'question' + game.current_question_index += 1 + game.question_start_time = timezone.now() + game.save() + return { + 'action': 'next_question', + 'redirect_url': f'/play/game/{self.join_code}/{game.current_question_index}' + } + else: + game.current_state = 'finished' + game.save() + return { + 'action': 'game_finished', + 'redirect_url': f'/play/game/{self.join_code}/finished' + } @database_sync_to_async def _remove_inactive_participants(self): diff --git a/django/play/models.py b/django/play/models.py index e3e0a72..adf9fee 100644 --- a/django/play/models.py +++ b/django/play/models.py @@ -6,9 +6,19 @@ import random, string # Create your models here. class QuizGame(models.Model): + GAME_STATES = [ + ('lobby', 'In Lobby'), + ('question', 'Frage läuft'), + ('scores', 'Punkteübersicht'), + ('finished', 'Beendet') + ] + host_id = models.CharField(max_length=200, unique=True) join_code = models.CharField(max_length=6, unique=True, blank=True) quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE) + current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby') + current_question_index = models.IntegerField(default=0) + question_start_time = models.DateTimeField(null=True, blank=True) def save(self, *args, **kwargs): if not self.join_code: @@ -30,15 +40,34 @@ class QuizGame(models.Model): class QuizGameParticipant(models.Model): participant_id = models.CharField(max_length=200, unique=True) display_name = models.CharField(verbose_name="Anzeigename", max_length=15) - quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participant") + quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants") score = models.IntegerField(verbose_name="Punkte", default=0) avatar = models.CharField(max_length=200, blank=True, default="") last_heartbeat = models.DateTimeField(auto_now=True) + last_answer = models.IntegerField(null=True, blank=True) + last_answer_correct = models.BooleanField(null=True, blank=True) def save(self, *args, **kwargs): if not self.participant_id: self.participant_id = self.generate_unique_id() super().save(*args, **kwargs) + + def generate_unique_id(self): + for i in range(10): + new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60)) + if not QuizGameParticipant.objects.filter(participant_id=new_id).exists(): + return new_id + +class QuizAnswer(models.Model): + participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers') + question_index = models.IntegerField() + answer_index = models.IntegerField() + is_correct = models.BooleanField() + score = models.IntegerField() + time_remaining = models.IntegerField() + + class Meta: + unique_together = ['participant', 'question_index'] def generate_unique_id(self): for i in range(10): diff --git a/django/play/routing.py b/django/play/routing.py index 8eaabe6..59f7b46 100644 --- a/django/play/routing.py +++ b/django/play/routing.py @@ -1,6 +1,8 @@ from django.urls import re_path -from . import consumers +from .consumers.game import GameConsumer +from .consumers.lobby import LobbyConsumer websocket_urlpatterns = [ - re_path(r'ws/game/(?P\w+)/$', consumers.LobbyConsumer.as_asgi()), + re_path(r'ws/game/(?P\w+)/$', GameConsumer.as_asgi()), + re_path(r'ws/play/lobby/(?P\w+)/$', LobbyConsumer.as_asgi()), ] diff --git a/django/play/urls.py b/django/play/urls.py index 8a9e692..81f59e8 100644 --- a/django/play/urls.py +++ b/django/play/urls.py @@ -8,6 +8,8 @@ urlpatterns = [ path('game/', views.lobby, name='lobby'), path('game//participate', views.create_participant, name='create_participant'), path('join', views.join_game, name='join_game'), - path('game//', views.question, name='question'), - path('game///participant', views.question_participant, name='question_participant'), + path('game//question', views.question, name='question'), + path('game//waiting', views.waiting_room, name='waiting'), + path('game//scores', views.scores, name='scores'), + path('game//finished', views.finished, name='finished'), ] \ No newline at end of file diff --git a/django/play/views.py b/django/play/views.py index 4a04bda..8feb7a8 100644 --- a/django/play/views.py +++ b/django/play/views.py @@ -29,6 +29,7 @@ def lobby(request, join_code): participant = QuizGameParticipant.objects.get(participant_id=participant_id) if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen participant.quiz_game = quiz_game + participant.score = 0 # Reset score when joining new game participant.save() except QuizGameParticipant.DoesNotExist: return redirect('play:create_participant', join_code=join_code) @@ -37,34 +38,41 @@ def lobby(request, join_code): return render(request, 'play/lobby.html', context=context) def create_participant(request, join_code): + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + if "participant_id" in request.COOKIES: # Prüfe ob der Teilnehmer noch existiert participant_id = request.COOKIES['participant_id'] try: participant = QuizGameParticipant.objects.get(participant_id=participant_id) + if participant.quiz_game.join_code != join_code: + participant.quiz_game = quiz_game + participant.score = 0 # Reset score when joining new game + participant.save() return redirect('play:lobby', join_code=join_code) except QuizGameParticipant.DoesNotExist: # Cookie löschen wenn Teilnehmer nicht mehr existiert response = HttpResponseRedirect(request.path) response.delete_cookie('participant_id') return response + if request.method == 'POST': display_name = request.POST.get('display_name') - join_code = request.POST.get('join_code') - try: - quiz = QuizGame.objects.get(join_code=join_code) - participant = QuizGameParticipant() - participant.display_name = display_name - participant.quiz_game = quiz - participant.save() + if not display_name: + return render(request, 'play/initialize_participant.html', { + 'join_code': join_code, + 'error': 'Bitte geben Sie einen Namen ein' + }) - response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) - response.set_cookie('participant_id', participant.participant_id, max_age=3600) + participant = QuizGameParticipant() + participant.display_name = display_name + participant.quiz_game = quiz_game + participant.score = 0 # Initialize score + participant.save() - return response - except QuizGame.DoesNotExist: - # TODO: Fehlermeldung fuer nicht-existierendes Quiz - pass + response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) + response.set_cookie('participant_id', participant.participant_id, max_age=3600) + return response return render(request, 'play/initialize_participant.html', {'join_code': join_code}) @@ -89,32 +97,162 @@ def join_game(request): join_code = request.POST.get('game_code') try: quiz = QuizGame.objects.get(join_code=join_code) + # Redirect to create_participant if no participant cookie exists + if not "participant_id" in request.COOKIES: + return redirect('play:create_participant', join_code=join_code) return redirect('play:lobby', join_code=join_code) except QuizGame.DoesNotExist: # TODO: Mit message eine Fehlermeldung weitergeben (und im entsprechenden Template Designen) pass return render(request, 'play/join_game.html') -def question(request, join_code, question_id): - question = get_object_or_404(QivipQuestion, pk=question_id) +def finished(request, join_code): + try: + game = QuizGame.objects.get(join_code=join_code) + participant = None + participant_id = request.session.get('participant_id') + if participant_id: + participant = QuizGameParticipant.objects.filter( + quiz_game=game, + participant_id=participant_id + ).first() - if question.data: - question.data = json.loads(question.data) + context = { + 'join_code': join_code, + 'is_host': str(game.host_id) == str(request.session.get('host_id')), + 'participant_id': participant_id if participant else None + } + return render(request, 'play/game/finished.html', context) + except QuizGame.DoesNotExist: + return redirect('home') - context = { - 'question': question, - } +def scores(request, join_code): + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + + # Verify game state + if quiz_game.current_state != 'scores': + return redirect('play:lobby', join_code=join_code) + + # Get participants sorted by score + participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score') + + # Check if user is host + is_host = False + if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id: + is_host = True + + # Get current question for context + questions = quiz_game.quiz_id.questions.all() + current_question = questions[quiz_game.current_question_index] + question_data = json.loads(current_question.data) + + return render(request, 'play/game/score_overview.html', { + 'quiz_game': quiz_game, + 'participants': participants, + 'is_host': is_host, + 'host_id': quiz_game.host_id if is_host else None, + 'is_last_question': quiz_game.current_question_index + 1 >= len(questions), + 'question_data': question_data, + 'question_index': quiz_game.current_question_index, + 'total_questions': len(questions) + }) - return render(request, 'play/game/question_host.html', {'question': question, 'debug': "debug"}) +def finished(request, join_code): + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + + # Verify game state + if quiz_game.current_state != 'finished': + return redirect('play:lobby', join_code=join_code) + + # Get participants sorted by score + participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score') + + # Check if user is host + is_host = False + if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id: + is_host = True -def question_participant(request, join_code, question_id): - question = get_object_or_404(QivipQuestion, pk=question_id) + # Get participant if exists + participant_id = request.COOKIES.get('participant_id') + participant = None + if participant_id: + participant = QuizGameParticipant.objects.filter( + quiz_game=quiz_game, + participant_id=participant_id + ).first() + + return render(request, 'play/game/finished.html', { + 'quiz_game': quiz_game, + 'participants': participants, + 'winner': participants.first() if participants.exists() else None, + 'join_code': join_code, + 'is_host': is_host, + 'participant_id': participant_id if participant else None + }) - if question.data: - question.data = json.loads(question.data) +def question(request, join_code): + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + + # Verify game state + if quiz_game.current_state != 'question': + return redirect('play:lobby', join_code=join_code) + + # Get current question + questions = quiz_game.quiz_id.questions.all() + if quiz_game.current_question_index >= len(questions): + return redirect('play:lobby', join_code=join_code) + + current_question = questions[quiz_game.current_question_index] + question_data = json.loads(current_question.data) + + # Check if user is host + if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id: + return render(request, 'play/game/question_host.html', { + 'quiz_game': quiz_game, + 'question_data': question_data, + 'host_id': quiz_game.host_id, + 'start_time': int(quiz_game.question_start_time.timestamp() * 1000), + 'question_index': quiz_game.current_question_index, + 'total_questions': len(questions) + }) + + # Handle participant view + if not 'participant_id' in request.COOKIES: + return redirect('play:create_participant', join_code=join_code) + + participant_id = request.COOKIES['participant_id'] + try: + participant = QuizGameParticipant.objects.get(participant_id=participant_id) + except QuizGameParticipant.DoesNotExist: + return redirect('play:create_participant', join_code=join_code) + + return render(request, 'play/game/question_participant.html', { + 'quiz_game': quiz_game, + 'question_data': question_data, + 'participant': participant, + 'start_time': int(quiz_game.question_start_time.timestamp() * 1000), + 'question_index': quiz_game.current_question_index, + 'total_questions': len(questions) + }) - context = { - 'question': question, - } - - return render(request, 'play/game/question_participant.html', {'question': question, 'debug': "debug"}) \ No newline at end of file +def waiting_room(request, join_code): + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + + # Check if user is host + if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id: + return redirect('play:question', join_code=join_code) + + # Handle participant view + if not 'participant_id' in request.COOKIES: + return redirect('play:create_participant', join_code=join_code) + + participant_id = request.COOKIES['participant_id'] + try: + participant = QuizGameParticipant.objects.get(participant_id=participant_id) + except QuizGameParticipant.DoesNotExist: + return redirect('play:create_participant', join_code=join_code) + + return render(request, 'play/game/wait_for_other_players.html', { + 'quiz_game': quiz_game, + 'participant': participant + }) \ No newline at end of file diff --git a/django/templates/base.html b/django/templates/base.html index 9340c71..3ec89de 100644 --- a/django/templates/base.html +++ b/django/templates/base.html @@ -6,12 +6,56 @@ + qivip {% include 'partials/_nav.html' %} {% block content%}{% endblock %} {% include 'partials/_footer.html' %} + + +
+ + + {% block extra_js %}{% endblock %} \ No newline at end of file diff --git a/django/templates/library/overview_quiz.html b/django/templates/library/overview_quiz.html index d64a02c..9278100 100644 --- a/django/templates/library/overview_quiz.html +++ b/django/templates/library/overview_quiz.html @@ -57,17 +57,27 @@
{% for quiz in filter_my_quizzes %}
-

{{ quiz.name }}

-

+

{{ quiz.description }}
Schwierigkeit: {{ quiz.difficulty }}
- Anzahl der Fragen: {{ quiz.question.count }}
- Status: {{ quiz.status }} + Anzahl der Fragen: {{ quiz.question.count }}
+ Status: {{ quiz.status }}
+ Bewertung: + {% for i in '12345'|make_list %} + {% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %} + + {% else %} + + {% endif %} + {% endfor %} + ({{ quiz.rating_count }}) +

@@ -85,14 +95,14 @@

- Spiel starten + Spiel starten
- + - + diff --git a/django/templates/play/game/question_host.html b/django/templates/play/game/question_host.html index c3c78bf..7a4f17c 100644 --- a/django/templates/play/game/question_host.html +++ b/django/templates/play/game/question_host.html @@ -1,35 +1,129 @@ -{% load static %} - - - - - - - qivip - - -
-
-

Frage X

- -

Frage: {{ question.data.question }}

-
-
-

Verbleibende Zeit:

-

36

-
-
-

7/14 Antworten

-
+{% extends 'base.html' %} + +{% block content %} +
+
+

Frage {{ question_index|add:1 }} von {{ total_questions }}

+

{{ question_data.question }}

+ +
+
+

Verbleibende Zeit

+

30

+
+
+

Antworten

+

0/0

-
- {% for option in question.data.options %} -
-

{{ option.value }}

+ +
+ {% for option in question_data.options %} +
+

{{ option.value }}

+
{% endfor %}
- - \ No newline at end of file +
+{% endblock %} + +{% block extra_js %} +{% include 'play/game/common_scripts.html' %} + +{% endblock %} \ No newline at end of file diff --git a/django/templates/play/game/question_participant.html b/django/templates/play/game/question_participant.html index 0449a28..2caf10d 100644 --- a/django/templates/play/game/question_participant.html +++ b/django/templates/play/game/question_participant.html @@ -1,19 +1,115 @@ -{% load static %} - - - - - - - qivip - - -
- {% for option in question.data.options %} -
-

{{ option.value }}

-
- {% endfor %} +{% extends 'base.html' %} + +{% block content %} +
+
+
- - \ No newline at end of file +
+

Frage {{ question_index|add:1 }} von {{ total_questions }}

+

{{ question_data.question }}

+ +
+

Verbleibende Zeit

+

30

+
+ +
+ {% for option in question_data.options %} + + {% endfor %} +
+
+
+{% endblock %} + +{% block extra_js %} +{% include 'play/game/common_scripts.html' %} + +{% endblock %} \ No newline at end of file diff --git a/django/templates/play/game/score_overview.html b/django/templates/play/game/score_overview.html index f90e7b5..ae201ac 100644 --- a/django/templates/play/game/score_overview.html +++ b/django/templates/play/game/score_overview.html @@ -1,5 +1,128 @@ - - Spieler sehen ihre Punktzahl, Platzierung und Richtig/Falsch +{% extends 'base.html' %} - Host: Scoreboard, Button: 'Weiter' - \ No newline at end of file +{% block content %} +
+
+

Ergebnisse

+ +
+

Aktuelle Frage

+

{{ question_data.question }}

+
+ {% for option in question_data.options %} +
+

{{ option.value }}

+

0 Antworten

+
+ {% endfor %} +
+
+ +
+

Punktestand

+
+ {% for participant in participants %} +
+
+

{{ participant.display_name }}

+

{{ participant.score }} Punkte

+
+ {% if participant.last_answer_correct %} + + {% elif participant.last_answer_correct == False %} + + {% endif %} +
+ {% endfor %} +
+
+ + {% if is_host %} + {% if is_last_question %} + + {% else %} + + {% endif %} + {% endif %} +
+
+{% endblock %} + +{% block extra_js %} +{% include 'play/game/common_scripts.html' %} + +{% endblock %} \ No newline at end of file diff --git a/django/templates/play/game/wait_for_other_players.html b/django/templates/play/game/wait_for_other_players.html index 8094ed8..9d18009 100644 --- a/django/templates/play/game/wait_for_other_players.html +++ b/django/templates/play/game/wait_for_other_players.html @@ -1,5 +1,53 @@ - - Spieler: Countdown, automatische Weiterleitung +{% extends 'base.html' %} - (Host: Frage) - \ No newline at end of file +{% block content %} +
+
+

Warte auf andere Spieler...

+ +
+

Die nächste Frage beginnt in

+

5

+
+ +
+

Bitte warte, bis alle Spieler bereit sind

+
+
+
+{% endblock %} + +{% block extra_js %} +{% include 'play/game/common_scripts.html' %} + +{% endblock %} \ No newline at end of file diff --git a/django/templates/play/initialize_participant.html b/django/templates/play/initialize_participant.html index 2bc4acc..9e92f51 100644 --- a/django/templates/play/initialize_participant.html +++ b/django/templates/play/initialize_participant.html @@ -1,13 +1,63 @@ {% extends 'base.html' %} +{% load static %} {% block content %} -

User for a Game:

-
-
- {% csrf_token %} - - - -
+
+ + {% endblock %} \ No newline at end of file diff --git a/django/templates/play/lobby.html b/django/templates/play/lobby.html index ff188de..29c6922 100644 --- a/django/templates/play/lobby.html +++ b/django/templates/play/lobby.html @@ -1,23 +1,62 @@ {% extends 'base.html' %} {% block content %} -
-

{{ quiz.name }}

-

{{ quiz_game.join_code }}

- {% if participant %} -
-

Sichtbar als {{ participant.display_name }}

+
+ +
+ +
+

{{ quiz.name }}

+
+

{{ quiz_game.join_code }}

+

Spiel-Code

+
+
+ + + {% if participant %} +
+
+ + Angemeldet als {{ participant.display_name }} +
+
+ {% endif %} + + +
+

+ + + + Teilnehmer +

+
    +
  • Warte auf Teilnehmer...
  • +
+
+ + +
+ {% if host_id %} + + {% endif %} + {% if participant %} + + {% endif %} +
- {% endif %} -
-

Teilnehmer:

-
    -
  • Warte auf Teilnehmer...
  • -
-
- {% if host_id %} - - {% endif %}
{% endblock %} @@ -25,50 +64,296 @@ {% endblock %} \ No newline at end of file