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 abef9e8..70db57b 100644
--- a/django/templates/base.html
+++ b/django/templates/base.html
@@ -9,6 +9,9 @@
qivip
+
diff --git a/django/templates/library/detail_quiz.html b/django/templates/library/detail_quiz.html
index 111b946..a7cb110 100644
--- a/django/templates/library/detail_quiz.html
+++ b/django/templates/library/detail_quiz.html
@@ -98,9 +98,6 @@
Eingabe
-
-
-
Multiple Choice
@@ -116,10 +113,15 @@
Reihenfolge
-
Schätzen
+
+
+ Karte
+
{% endif %}
@@ -140,8 +142,14 @@
{{ question.data.question }}
-
- {% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% elif question.data.type == 'true_false' %}Wahr/Falsch{% elif question.data.type == 'order' %}Reihenfolge{% else %}Schätzen{% endif %}
+
+ {% if question.data.type == 'multiple_choice' %}Multiple Choice
+ {% elif question.data.type == 'input' %}Eingabe
+ {% elif question.data.type == 'map' %}Karte
+ {% elif question.data.type == 'true_false' %}Wahr/Falsch
+ {% elif question.data.type == 'order' %}Reihenfolge
+ {% else %}Schätzen
+ {% endif %}
@@ -173,8 +181,46 @@

{% endif %}
+
+ {% elif question.data.type == 'map' and detail %}
+
+
+ Zielort:
+
+ {% if question.data.target_location %}
+ {{ question.data.target_location }}
+ {% else %}
+ Kein Zielort festgelegt
+ {% endif %}
+
+
+ {% if question.data.target_location %}
+
+ {% endif %}
+ {% if question.data.tolerance_radius %}
+
+ Toleranzradius: {{ question.data.tolerance_radius }} Meter
+
+ {% endif %}
+ {% if question.image.image.url %}
+

+ {% endif %}
+
{% elif question.data.type == 'order' %}
-
+
{% for option in question.data.options|dictsort:"order" %}
-
@@ -188,7 +234,6 @@
{% endif %}
-
{% elif question.data.type == 'input' %}
diff --git a/django/templates/library/question/question_map.html b/django/templates/library/question/question_map.html
new file mode 100644
index 0000000..db8461d
--- /dev/null
+++ b/django/templates/library/question/question_map.html
@@ -0,0 +1,131 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/django/templates/play/game/question_host.html b/django/templates/play/game/question_host.html
index f3a168c..9d08997 100644
--- a/django/templates/play/game/question_host.html
+++ b/django/templates/play/game/question_host.html
@@ -50,8 +50,7 @@
- {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
-
+ {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
{% for option in question_data.options %}
{% endblock %}
diff --git a/django/templates/play/game/question_participant.html b/django/templates/play/game/question_participant.html
index 74c3632..9bc0c23 100644
--- a/django/templates/play/game/question_participant.html
+++ b/django/templates/play/game/question_participant.html
@@ -25,8 +25,7 @@
-
- {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
+ {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
{% for option in question_data.options %}
+ {% elif question_data.type == "map" %}
+
+ Ziel auswählen
+
+
+
+
+ Abgeben
+
+
+
+ {% else %}
+ aaaaah gibts nicht
{% endif %}
@@ -203,7 +254,22 @@
wsUtil.handleClose(e);
};
- function submitAnswer(optionIndex) {
+ function submitAnswer(optionIndex=0, has_options=true) {
+ if (!has_options){
+ answer = document.getElementById("singleanswer").value;
+
+ if (answer == ""){
+ alert("Bitte eine Antwort eingeben!");
+ return;
+ }
+
+ gameSocket.send(JSON.stringify({
+ type: 'submit_answer',
+ participant_id: participantId,
+ answer:answer,
+ time_remaining: questionDuration
+ }));
+ }
if (hasAnswered) return;
let input = ""; // Declare input here
@@ -249,7 +315,42 @@
}
}
-
+ function submitAnswerMap() {
+ answer = document.getElementById("location").value;
+
+ if (answer == ""){
+ alert("Bitte eine Antwort eingeben!");
+ return;
+ }
+
+ // Add grey overlay
+ const overlay = document.createElement('div');
+ overlay.style.position = 'fixed';
+ overlay.style.top = '0';
+ overlay.style.left = '0';
+ overlay.style.width = '100%';
+ overlay.style.height = '100%';
+ overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
+ overlay.style.zIndex = '1000';
+ overlay.style.display = 'flex';
+ overlay.style.justifyContent = 'center';
+ overlay.style.alignItems = 'center';
+ overlay.style.color = 'white';
+ overlay.style.fontSize = '24px';
+ overlay.style.fontWeight = 'bold';
+ overlay.textContent = 'Antwort übermittelt';
+ document.body.appendChild(overlay);
+
+ const now = Date.now();
+ const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
+
+ gameSocket.send(JSON.stringify({
+ type: 'submit_answer',
+ participant_id: participantId,
+ answer: answer,
+ time_remaining: timeRemaining
+ }));
+ }
// Timer
function updateTimer() {
diff --git a/django/templates/play/game/score_overview.html b/django/templates/play/game/score_overview.html
index 7230ee2..bdc76d8 100644
--- a/django/templates/play/game/score_overview.html
+++ b/django/templates/play/game/score_overview.html
@@ -9,9 +9,100 @@
Aktuelle Frage
{{ question_data.question }}
- {% if question_data.type != "order" and question_data.type != "guess" %}
+
+ {% if question_data.type == "map" %}
+
+
+
+
+
+
+ {% elif question_data.type != "order" and question_data.type != "guess" %}
+
-
{% for option in question_data.options %}
@@ -25,7 +116,7 @@
{% endfor %}
- {% elif question_data.type == "guess" %}
+ {% elif question_data.type == "guess" %}
diff --git a/requirements.txt b/requirements.txt
index 376bf71..be6814f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,4 +11,6 @@ python-dotenv # Production server safety
qrcode~=7.4.2
reportlab~=4.2.0
pip-licenses ~= 5.0.0
-requests
\ No newline at end of file
+requests
+requests
+geopy