diff --git a/django/library/views.py b/django/library/views.py index 5da7064..bc62d6f 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -861,12 +861,12 @@ def new_question(request): messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.') return redirect('library:overview_quiz') - if question_type not in ['multiple_choice', 'true_false','input','order','guess']: + if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']: base_url = reverse('library:new_question') return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}') if request.method == 'POST': - question_text = request.POST.get('question', '') + question_text = request.POST.get('question', 'Kein Fragentext angegeben') # Handle different question types if question_type == 'true_false': @@ -946,20 +946,27 @@ def new_question(request): max_value = request.POST.get('max') tolerance = request.POST.get('tolerance') json_data = { - 'type': question_type, - 'question': question_text, - 'correct_answer': correct_answer, - 'min': min_value, - 'max': max_value, - 'tolerance': tolerance, - 'options': [ - {'value': correct_answer, 'is_correct': True} - ] - } - - - + 'type': question_type, + 'question': question_text, + 'correct_answer': correct_answer, + 'min': min_value, + 'max': max_value, + 'tolerance': tolerance, + 'options': [ + {'value': correct_answer, 'is_correct': True} + ] + } + elif question_type == 'map': + target_location = request.POST.get('location') + tolerance_radius = request.POST.get('tolerance_radius') + + json_data = { + 'type': question_type, + 'question': question_text, + 'target_location': target_location, + 'tolerance_radius': tolerance_radius + } question = QivipQuestion() question.data = json.dumps(json_data) question.quiz_id = quiz @@ -1030,18 +1037,26 @@ def new_question(request): max_value = request.POST.get('max') tolerance = request.POST.get('tolerance') json_data = { - 'type': question_type, - 'question': '', - 'correct_answer': correct_answer, - 'min': min_value, - 'max': max_value, - 'tolerance': tolerance, - 'options': [ - {'value': correct_answer, 'is_correct': True} - ] - } + 'type': question_type, + 'question': '', + 'correct_answer': correct_answer, + 'min': min_value, + 'max': max_value, + 'tolerance': tolerance, + 'options': [ + {'value': correct_answer, 'is_correct': True} + ] + } standard_time_per_question = 30 + elif question_type == 'map': + question_data = { + 'type': question_type, + 'question': '', + 'target_location': "", + 'tolerance_radius': 90000 + } + standard_time_per_question = 30 template_name = f'library/question/question_{question_type}.html' context = { @@ -1083,6 +1098,13 @@ def edit_question(request, pk): {'value': 'Falsch', 'is_correct': not correct_answer} ] } + elif question_type == 'map': + question_data = { + 'type': question_type, + 'question': question_data.get('question', ''), + 'target_location': question_data.get('target_location', ''), + 'tolerance_radius': question_data.get('tolerance_radius', 90000) + } else: # For multiple choice questions question_data.setdefault('type', question_type) @@ -1200,17 +1222,27 @@ def edit_question(request, pk): max_value = request.POST.get('max') tolerance = request.POST.get('tolerance') json_data = { - 'type': question_type, - 'question': question_text, - 'correct_answer': correct_answer, - 'min': min_value, - 'max': max_value, - 'tolerance': tolerance, - 'options': [ - {'value': correct_answer, 'is_correct': True} - ] - } + 'type': question_type, + 'question': question_text, + 'correct_answer': correct_answer, + 'min': min_value, + 'max': max_value, + 'tolerance': tolerance, + 'options': [ + {'value': correct_answer, 'is_correct': True} + ] + } + elif question_type == 'map': + target_location = request.POST.get('location') + tolerance_radius = request.POST.get('tolerance_radius') + + json_data = { + 'type': question_type, + 'question': question_text, + 'target_location': target_location, + 'tolerance_radius': tolerance_radius + } question.data = json.dumps(json_data) question.time_per_question = request.POST.get('time_per_question', '30') try: diff --git a/django/play/consumers/game.py b/django/play/consumers/game.py index a227b4e..088343e 100644 --- a/django/play/consumers/game.py +++ b/django/play/consumers/game.py @@ -6,6 +6,7 @@ from django.utils import timezone from django.urls import reverse from play.models import QuizGame, QuizGameParticipant, QuizAnswer from django.conf import settings +from geopy.distance import geodesic class GameConsumer(AsyncWebsocketConsumer): @@ -31,6 +32,13 @@ class GameConsumer(AsyncWebsocketConsumer): # Check if game should be deleted await self.cleanup_game() + + @database_sync_to_async + def get_question_data(self): + quiz_game = QuizGame.objects.get(join_code=self.join_code) + current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index] + return json.loads(current_question.data) + async def receive(self, text_data): text_data_json = json.loads(text_data) message_type = text_data_json['type'] @@ -84,14 +92,16 @@ class GameConsumer(AsyncWebsocketConsumer): elif message_type == 'submit_answer': participant_id = text_data_json['participant_id'] - try: - answer = text_data_json['answer'] - except: - answer=-1 + 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) + + question_data = await self.get_question_data() + if question_data['type'] == "map": + await self.save_answer_map(participant_id, answer, time_remaining) + else: + await self.save_answer(participant_id, answer, time_remaining) + # Notify host about the new answer await self.channel_layer.group_send( @@ -182,6 +192,97 @@ class GameConsumer(AsyncWebsocketConsumer): except QuizGame.DoesNotExist: return False + @database_sync_to_async + def save_answer_map(self, participant_id, answer_location, time_remaining): + answer_index = -1 + try: + # Get all necessary data first + 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] + question_data = json.loads(current_question.data) + target_location = question_data['target_location'] + tolerance_radius = question_data['tolerance_radius'] + print(f"target_location: {target_location}") + print(f"answer_location: {answer_location}") + print(f"tolerance_radius: {tolerance_radius}") + + # Configuration - Make these easily adjustable + MAX_DISTANCE_KM = 1000 # Maximum distance for scoring (in km) + DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance + + # Calculate base score + distance_km = geodesic(answer_location, target_location).kilometers + tolerance_km = int(tolerance_radius) / 1000 # Convert meters to kilometers + + # Calculate base score with non-linear decrease + if distance_km <= tolerance_km: + # Direct hit - full points + bonus + base_score = 1000 + DIRECT_HIT_BONUS # 1000 + 200 bonus + is_correct = True + else: + if distance_km > MAX_DISTANCE_KM: + # Beyond max distance - 0 points + base_score = 0 + is_correct = False + else: + # Non-linear decrease using exponential falloff + # This creates a steeper drop-off at the beginning + progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km) + # Using a power function to create non-linear falloff + falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning + base_score = 1000 * (1 - falloff) + is_correct = True + base_score = max(0, min(1000, int(base_score))) + + # Apply time penalty (reduces score based on time taken) + if is_correct and base_score > 0: + # Calculate time taken as a fraction of total time (0-1) + time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000 + max_time_sec = int(current_question.time_per_question) + time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0 + + # Reduce points based on time taken (up to 50% reduction for answering at the last second) + time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty + final_score = max(1, base_score - time_penalty) # At least 1 point if correct + else: + final_score = base_score # 0 points for incorrect answers + + score = int(round(final_score)) + + # Save the answer + answer_obj, created = QuizAnswer.objects.get_or_create( + participant=participant, + question_index=quiz_game.current_question_index, + defaults={ + 'answer_index': answer_index, + 'is_correct': is_correct, + 'score': score, + 'time_remaining': time_remaining + } + ) + + if not created: + answer_obj.answer_index = answer_index + answer_obj.is_correct = is_correct + answer_obj.score = score + answer_obj.time_remaining = time_remaining + answer_obj.save() + + participant.last_score = score + participant.score += score + participant.last_answer_correct = is_correct + participant.save() + + return True + + except (QuizGameParticipant.DoesNotExist, IndexError): + return False + + @database_sync_to_async def save_answer(self, participant_id, answer, time_remaining): try: diff --git a/django/static/css/t-input.css b/django/static/css/t-input.css index bc9d632..bf58810 100644 --- a/django/static/css/t-input.css +++ b/django/static/css/t-input.css @@ -124,4 +124,13 @@ div.qp-question-container { div.qp-question-container img { @apply max-w-[50vw] max-h-[40vh] mx-auto; +} + +div#mapquestion-container { + display: flex; + justify-content: center; +} + +div#map { + @apply md:w-[600px] lg:w-[900px] h-[400px] } \ No newline at end of file diff --git a/django/templates/base.html b/django/templates/base.html index b834707..5ebe66c 100644 --- a/django/templates/base.html +++ b/django/templates/base.html @@ -9,6 +9,9 @@
{{ question.data.question }}
aaaaah gibts nicht
{% endif %}{{ question_data.question }}
- {% if question_data.type != "order" and question_data.type != "guess" %} + + {% if question_data.type == "map" %} + +