feat(f3): mix correct answer with lies and open guess phase
This commit is contained in:
3
TODO.md
3
TODO.md
@@ -56,7 +56,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo
|
|||||||
- [x] Lobby: host opretter session, spillere joiner via kode
|
- [x] Lobby: host opretter session, spillere joiner via kode
|
||||||
- [x] Runde starter med kategori
|
- [x] Runde starter med kategori
|
||||||
- [x] Spørgsmål vises -> alle skriver løgn inden X sek
|
- [x] Spørgsmål vises -> alle skriver løgn inden X sek
|
||||||
- [ ] System blander korrekt svar + løgne
|
- [x] System blander korrekt svar + løgne
|
||||||
- [ ] Guessfase: alle gætter inden Z sek
|
- [ ] Guessfase: alle gætter inden Z sek
|
||||||
- [ ] Pointudregning (konfigurerbar pr. runde)
|
- [ ] Pointudregning (konfigurerbar pr. runde)
|
||||||
- [ ] Scoreboard + næste spørgsmål/runde
|
- [ ] Scoreboard + næste spørgsmål/runde
|
||||||
@@ -103,6 +103,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo
|
|||||||
- [ ] Migrations + static + health checks
|
- [ ] Migrations + static + health checks
|
||||||
|
|
||||||
### Backlog — Need-to-have / Nice-to-have
|
### Backlog — Need-to-have / Nice-to-have
|
||||||
|
- [ ] (Need-to-have) Persistér mixed svarrækkefølge pr. round question, så alle spillere ser samme rækkefølge ved reconnect/refresh
|
||||||
- [ ] (Need-to-have) Tilføj spiller-auth/session-token for submit_lie (pt. baseret på player_id i payload)
|
- [ ] (Need-to-have) Tilføj spiller-auth/session-token for submit_lie (pt. baseret på player_id i payload)
|
||||||
- [ ] (Nice-to-have) Endpoint til status/progress i løgnfasen (antal indsendt ud af total)
|
- [ ] (Nice-to-have) Endpoint til status/progress i løgnfasen (antal indsendt ud af total)
|
||||||
- [ ] (Need-to-have) [Fejltype: CI/lint F401] [Fil/område: core_admin/*, fupogfakta/tests.py+views.py, lobby/admin.py+models.py, realtime/*, voice/*] [Branch/PR: feature/f3-lobby-create-join, feature/fase0-mvp-fup-og-fakta, feature/lobby-mvp (ingen åbne PRs fundet)] Fjern ubrugte scaffold-imports (eller kør ruff check --fix) så quality gate kan blive grøn før merge.
|
- [ ] (Need-to-have) [Fejltype: CI/lint F401] [Fil/område: core_admin/*, fupogfakta/tests.py+views.py, lobby/admin.py+models.py, realtime/*, voice/*] [Branch/PR: feature/f3-lobby-create-join, feature/fase0-mvp-fup-og-fakta, feature/lobby-mvp (ingen åbne PRs fundet)] Fjern ubrugte scaffold-imports (eller kør ruff check --fix) så quality gate kan blive grøn før merge.
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|||||||
@@ -265,3 +265,75 @@ class LieSubmissionTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 409)
|
self.assertEqual(response.status_code, 409)
|
||||||
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
|
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
|
||||||
|
|
||||||
|
class MixAnswersTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.host = User.objects.create_user(username="host", password="secret123")
|
||||||
|
self.other_user = User.objects.create_user(username="other", password="secret123")
|
||||||
|
self.session = GameSession.objects.create(host=self.host, code="ABCD23", status=GameSession.Status.LIE)
|
||||||
|
self.category = Category.objects.create(name="Historie", slug="historie", is_active=True)
|
||||||
|
self.question = Question.objects.create(
|
||||||
|
category=self.category,
|
||||||
|
prompt="Hvilken by er Danmarks hovedstad?",
|
||||||
|
correct_answer="København",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
RoundConfig.objects.create(session=self.session, number=1, category=self.category)
|
||||||
|
self.round_question = RoundQuestion.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
round_number=1,
|
||||||
|
question=self.question,
|
||||||
|
correct_answer="København",
|
||||||
|
)
|
||||||
|
self.player_one = Player.objects.create(session=self.session, nickname="Luna")
|
||||||
|
self.player_two = Player.objects.create(session=self.session, nickname="Mads")
|
||||||
|
|
||||||
|
def test_host_can_mix_answers_and_transition_to_guess(self):
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="Aarhus")
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Odense")
|
||||||
|
|
||||||
|
self.client.login(username="host", password="secret123")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:mix_answers",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
answer_texts = [entry["text"] for entry in payload["answers"]]
|
||||||
|
self.assertEqual(set(answer_texts), {"København", "Aarhus", "Odense"})
|
||||||
|
self.assertEqual(payload["session"]["status"], GameSession.Status.GUESS)
|
||||||
|
|
||||||
|
self.session.refresh_from_db()
|
||||||
|
self.assertEqual(self.session.status, GameSession.Status.GUESS)
|
||||||
|
|
||||||
|
def test_mix_answers_requires_host(self):
|
||||||
|
self.client.login(username="other", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:mix_answers",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error"], "Only host can mix answers")
|
||||||
|
|
||||||
|
def test_mix_answers_deduplicates_case_insensitive_lies(self):
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="københavn")
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Aarhus")
|
||||||
|
|
||||||
|
self.client.login(username="host", password="secret123")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:mix_answers",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
answer_texts = [entry["text"] for entry in response.json()["answers"]]
|
||||||
|
self.assertEqual(set(answer_texts), {"København", "Aarhus"})
|
||||||
|
|||||||
@@ -15,4 +15,9 @@ urlpatterns = [
|
|||||||
views.submit_lie,
|
views.submit_lie,
|
||||||
name="submit_lie",
|
name="submit_lie",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"sessions/<str:code>/questions/<int:round_question_id>/answers/mix",
|
||||||
|
views.mix_answers,
|
||||||
|
name="mix_answers",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -335,3 +335,66 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
|||||||
},
|
},
|
||||||
status=201,
|
status=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> JsonResponse:
|
||||||
|
session_code = code.strip().upper()
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = GameSession.objects.get(code=session_code)
|
||||||
|
except GameSession.DoesNotExist:
|
||||||
|
return JsonResponse({"error": "Session not found"}, status=404)
|
||||||
|
|
||||||
|
if session.host_id != request.user.id:
|
||||||
|
return JsonResponse({"error": "Only host can mix answers"}, status=403)
|
||||||
|
|
||||||
|
if session.status != GameSession.Status.LIE:
|
||||||
|
return JsonResponse({"error": "Answers can only be mixed in lie phase"}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
round_question = RoundQuestion.objects.get(
|
||||||
|
pk=round_question_id,
|
||||||
|
session=session,
|
||||||
|
round_number=session.current_round,
|
||||||
|
)
|
||||||
|
except RoundQuestion.DoesNotExist:
|
||||||
|
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||||
|
|
||||||
|
lie_texts = list(round_question.lies.values_list("text", flat=True))
|
||||||
|
|
||||||
|
deduped_answers = []
|
||||||
|
seen = set()
|
||||||
|
for text in [round_question.correct_answer, *lie_texts]:
|
||||||
|
normalized = text.strip().casefold()
|
||||||
|
if not normalized or normalized in seen:
|
||||||
|
continue
|
||||||
|
seen.add(normalized)
|
||||||
|
deduped_answers.append(text.strip())
|
||||||
|
|
||||||
|
if len(deduped_answers) < 2:
|
||||||
|
return JsonResponse({"error": "Not enough answers to mix"}, status=400)
|
||||||
|
|
||||||
|
random.shuffle(deduped_answers)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
|
if locked_session.status != GameSession.Status.LIE:
|
||||||
|
return JsonResponse({"error": "Answers can only be mixed in lie phase"}, status=400)
|
||||||
|
locked_session.status = GameSession.Status.GUESS
|
||||||
|
locked_session.save(update_fields=["status"])
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"session": {
|
||||||
|
"code": session.code,
|
||||||
|
"status": GameSession.Status.GUESS,
|
||||||
|
"current_round": session.current_round,
|
||||||
|
},
|
||||||
|
"round_question": {
|
||||||
|
"id": round_question.id,
|
||||||
|
"round_number": round_question.round_number,
|
||||||
|
},
|
||||||
|
"answers": [{"text": text} for text in deduped_answers],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user