Merge pull request 'F3: Beregn point efter guessfase og skift til reveal' (#10) from feature/f3-score-calculate into main
All checks were successful
CI / test-and-quality (push) Successful in 51s
All checks were successful
CI / test-and-quality (push) Successful in 51s
This commit was merged in pull request #10.
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -58,7 +58,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo
|
|||||||
- [x] Spørgsmål vises -> alle skriver løgn inden X sek
|
- [x] Spørgsmål vises -> alle skriver løgn inden X sek
|
||||||
- [x] System blander korrekt svar + løgne
|
- [x] System blander korrekt svar + løgne
|
||||||
- [x] Guessfase: alle gætter inden Z sek
|
- [x] Guessfase: alle gætter inden Z sek
|
||||||
- [ ] Pointudregning (konfigurerbar pr. runde)
|
- [x] Pointudregning (konfigurerbar pr. runde)
|
||||||
- [ ] Scoreboard + næste spørgsmål/runde
|
- [ ] Scoreboard + næste spørgsmål/runde
|
||||||
- [ ] Slutresultat
|
- [ ] Slutresultat
|
||||||
|
|
||||||
|
|||||||
103
lobby/tests.py
103
lobby/tests.py
@@ -443,3 +443,106 @@ class GuessSubmissionTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Guess submission window has closed")
|
self.assertEqual(response.json()["error"], "Guess submission window has closed")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreCalculationTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.host = User.objects.create_user(username="host_score", password="secret123")
|
||||||
|
self.other_user = User.objects.create_user(username="other_score", password="secret123")
|
||||||
|
self.session = GameSession.objects.create(host=self.host, code="SC0RE1", status=GameSession.Status.GUESS)
|
||||||
|
self.category = Category.objects.create(name="Sport", slug="sport", is_active=True)
|
||||||
|
self.question = Question.objects.create(
|
||||||
|
category=self.category,
|
||||||
|
prompt="Hvilken sport spiller man i Wimbledon?",
|
||||||
|
correct_answer="Tennis",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
RoundConfig.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
number=1,
|
||||||
|
category=self.category,
|
||||||
|
points_correct=5,
|
||||||
|
points_bluff=2,
|
||||||
|
)
|
||||||
|
self.round_question = RoundQuestion.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
round_number=1,
|
||||||
|
question=self.question,
|
||||||
|
correct_answer="Tennis",
|
||||||
|
)
|
||||||
|
self.player_one = Player.objects.create(session=self.session, nickname="Luna")
|
||||||
|
self.player_two = Player.objects.create(session=self.session, nickname="Mads")
|
||||||
|
self.player_three = Player.objects.create(session=self.session, nickname="Nora")
|
||||||
|
|
||||||
|
def test_host_can_calculate_scores_and_transition_to_reveal(self):
|
||||||
|
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
||||||
|
Guess.objects.create(
|
||||||
|
round_question=self.round_question,
|
||||||
|
player=self.player_two,
|
||||||
|
selected_text="Padel",
|
||||||
|
is_correct=False,
|
||||||
|
fooled_player=self.player_three,
|
||||||
|
)
|
||||||
|
Guess.objects.create(
|
||||||
|
round_question=self.round_question,
|
||||||
|
player=self.player_three,
|
||||||
|
selected_text="Padel",
|
||||||
|
is_correct=False,
|
||||||
|
fooled_player=self.player_three,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(username="host_score", password="secret123")
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:calculate_scores",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL)
|
||||||
|
self.assertEqual(payload["events_created"], 2)
|
||||||
|
|
||||||
|
self.player_one.refresh_from_db()
|
||||||
|
self.player_three.refresh_from_db()
|
||||||
|
self.session.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.player_one.score, 5)
|
||||||
|
self.assertEqual(self.player_three.score, 4)
|
||||||
|
self.assertEqual(self.session.status, GameSession.Status.REVEAL)
|
||||||
|
|
||||||
|
def test_calculate_scores_requires_host(self):
|
||||||
|
self.client.login(username="other_score", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:calculate_scores",
|
||||||
|
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 calculate scores")
|
||||||
|
|
||||||
|
def test_calculate_scores_rejects_duplicate_calculation(self):
|
||||||
|
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
||||||
|
|
||||||
|
self.client.login(username="host_score", password="secret123")
|
||||||
|
first = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:calculate_scores",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
second = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:calculate_scores",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": self.round_question.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(first.status_code, 200)
|
||||||
|
self.assertEqual(second.status_code, 409)
|
||||||
|
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
||||||
|
|||||||
@@ -25,5 +25,10 @@ urlpatterns = [
|
|||||||
views.submit_guess,
|
views.submit_guess,
|
||||||
name="submit_guess",
|
name="submit_guess",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"sessions/<str:code>/questions/<int:round_question_id>/scores/calculate",
|
||||||
|
views.calculate_scores,
|
||||||
|
name="calculate_scores",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
111
lobby/views.py
111
lobby/views.py
@@ -17,6 +17,7 @@ from fupogfakta.models import (
|
|||||||
Question,
|
Question,
|
||||||
RoundConfig,
|
RoundConfig,
|
||||||
RoundQuestion,
|
RoundQuestion,
|
||||||
|
ScoreEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
@@ -496,3 +497,113 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
},
|
},
|
||||||
status=201,
|
status=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
def calculate_scores(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 calculate scores"}, status=403)
|
||||||
|
|
||||||
|
already_calculated = ScoreEvent.objects.filter(
|
||||||
|
session=session,
|
||||||
|
meta__round_question_id=round_question_id,
|
||||||
|
).exists()
|
||||||
|
if already_calculated:
|
||||||
|
return JsonResponse({"error": "Scores already calculated for this round question"}, status=409)
|
||||||
|
|
||||||
|
if session.status != GameSession.Status.GUESS:
|
||||||
|
return JsonResponse({"error": "Scores can only be calculated in guess 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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
|
except RoundConfig.DoesNotExist:
|
||||||
|
return JsonResponse({"error": "Round config missing"}, status=400)
|
||||||
|
|
||||||
|
guesses = list(round_question.guesses.select_related("player"))
|
||||||
|
if not guesses:
|
||||||
|
return JsonResponse({"error": "No guesses submitted for this round question"}, status=400)
|
||||||
|
|
||||||
|
bluff_counts = {}
|
||||||
|
for guess in guesses:
|
||||||
|
if guess.fooled_player_id:
|
||||||
|
bluff_counts[guess.fooled_player_id] = bluff_counts.get(guess.fooled_player_id, 0) + 1
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
|
if locked_session.status != GameSession.Status.GUESS:
|
||||||
|
return JsonResponse({"error": "Scores can only be calculated in guess phase"}, status=400)
|
||||||
|
|
||||||
|
score_events = []
|
||||||
|
|
||||||
|
for guess in guesses:
|
||||||
|
if guess.is_correct:
|
||||||
|
guess.player.score += round_config.points_correct
|
||||||
|
guess.player.save(update_fields=["score"])
|
||||||
|
score_events.append(
|
||||||
|
ScoreEvent(
|
||||||
|
session=session,
|
||||||
|
player=guess.player,
|
||||||
|
delta=round_config.points_correct,
|
||||||
|
reason="guess_correct",
|
||||||
|
meta={"round_question_id": round_question.id, "guess_id": guess.id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for player_id, fooled_count in bluff_counts.items():
|
||||||
|
delta = fooled_count * round_config.points_bluff
|
||||||
|
player = Player.objects.get(pk=player_id, session=session)
|
||||||
|
player.score += delta
|
||||||
|
player.save(update_fields=["score"])
|
||||||
|
score_events.append(
|
||||||
|
ScoreEvent(
|
||||||
|
session=session,
|
||||||
|
player=player,
|
||||||
|
delta=delta,
|
||||||
|
reason="bluff_success",
|
||||||
|
meta={"round_question_id": round_question.id, "fooled_count": fooled_count},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ScoreEvent.objects.bulk_create(score_events)
|
||||||
|
|
||||||
|
locked_session.status = GameSession.Status.REVEAL
|
||||||
|
locked_session.save(update_fields=["status"])
|
||||||
|
|
||||||
|
leaderboard = list(
|
||||||
|
Player.objects.filter(session=session)
|
||||||
|
.order_by("-score", "nickname")
|
||||||
|
.values("id", "nickname", "score")
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"session": {
|
||||||
|
"code": session.code,
|
||||||
|
"status": GameSession.Status.REVEAL,
|
||||||
|
"current_round": session.current_round,
|
||||||
|
},
|
||||||
|
"round_question": {
|
||||||
|
"id": round_question.id,
|
||||||
|
"round_number": round_question.round_number,
|
||||||
|
},
|
||||||
|
"events_created": len(score_events),
|
||||||
|
"leaderboard": leaderboard,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user