Quiz mode - Developer documentation

1) Scope

This document describes the current Quiz mode implementation in Earth Pulse, across backend and frontend.

Quiz mode is a real-time single-player experience with a virtual opponent (Desty):

  • one human player plus one bot

  • 6 rounds by default (8 when max players is 4 in room settings)

  • multiple-choice answer flow with score and time bonus

2) Main components

Backend

  • apiserver/apiserver/routers/quiz_game.py

  • apiserver/apiserver/controllers/quiz_game_controller.py

  • apiserver/apiserver/utils/quiz_turn.py

  • apiserver/apiserver/services/quiz_game_service.py

  • apiserver/apiserver/models/quiz_room.py

  • apiserver/apiserver/models/quiz_player.py

Frontend

  • frontend/engines/QuizEngine.ts

  • frontend/class/QuizGame.ts

  • frontend/types/QuizGame.ts

  • frontend/composables/useGameWebSocket.ts

  • frontend/pages/quiz-game-board.vue

3) Transport and endpoints

REST

  • POST /quiz/create_room

    • body: mode, max_players, deck_id, difficulty

    • returns room_id and difficulty

  • GET /quiz/rooms

  • GET /quiz/rooms/{room_id}

WebSocket

  • GET WS /quiz/ws/{room_id}/{pseudo}

  • GET WS /quiz/ws/{room_id}/{pseudo}/{token}

Core events:

  • server to client: game_context, player_join, game_start, round_start, turn_result, game_finish, error

  • client to server (answer): payload with card_id, player_id, rest_time

4) Room and round model

QuizRoom keeps in-memory state:

  • room metadata (room_id, mode, state, created_at)

  • players list and current player pointer

  • round counters (turn_played, max_turn, round_index)

  • round card state (turn_card, turn_cards, played_cards_data, used_card_ids)

  • difficulty-dependent timer (beginner=25s, intermediate=12s, expert=6s)

Round construction in QuizRoom.pick_all_round_cards:

  • choose a random card that can still form a 4-card set by type

  • choose 3 other cards of same type

  • fetch and serialize card + indicator + photo data for UI

  • prevent card reuse via used_card_ids

5) Game flow

Start

  1. Client creates room.

  2. Player joins via WebSocket.

  3. Controller validates pseudo, room state, and capacity.

  4. Bot player Desty is appended.

  5. Backend sends game_context and player_join.

  6. start_game persists a Game row and broadcasts game_start.

Round loop

  1. new_turn emits round_start with 4 candidate cards and deadline.

  2. Player sends selected card_id.

  3. Backend checks if selected card equals the hidden winning card.

  4. turn_result is broadcast.

  5. Next round starts or game ends.

End

end_game emits game_finish, updates game status, and writes leaderboard entries for active non-bot players.

6) Scoring logic

Scoring is difficulty-based:

  • correct answer: +100 / +200 / +400

  • wrong answer: -25 / -50 / -100

  • speed bonus: +10 * seconds_left when answer is correct

Only non-bot and non-eliminated players are scored/persisted.

7) Persistence and leaderboard

  • add_game(room) creates game record at start (type=quiz).

  • update_game(room, "finish") updates terminal state.

  • add_score_to_leaderboard(..., game_type=QUIZ) writes final score.

8) Frontend runtime behavior

QuizEngine handles event choreography:

  • round_start: prepares answer proposals and winner card metadata

  • turn_result: modal sequence (round_result -> card_reveal -> score bar)

  • game_finish: final score modal

UI deadline compensation is applied after result animations so timers stay aligned with server rounds.

9) Operational notes

  • Pseudo toxicity checks are enforced before room join.

  • Room join is rejected when game already started or room is full.

  • Room expiration is handled in model (is_expired, 2h threshold).

  • Existing backend tests for round card constraints: apiserver/tests/test_quiz_room.py.