Skip to main content

Overview

ClassQuiz’s live game feature allows you to host real-time quiz sessions where multiple players can join and compete simultaneously. Games use WebSocket connections for real-time interaction and Redis for session state management.

Starting a Game

Game Configuration

To start a live game session (classquiz/routers/quiz.py:77):
@router.post("/start/{quiz_id}")
async def start_quiz(
    quiz_id: str,
    game_mode: str,
    captcha_enabled: bool = True,
    custom_field: str | None = None,
    cqcs_enabled: bool = False,
    randomize_answers: bool = False,
    user: User = Depends(get_current_user),
):
quiz_id
UUID
required
The unique identifier of the quiz to play
game_mode
string
required
The game mode (e.g., “normal”, “kahoot”)
captcha_enabled
boolean
default:"true"
Enable CAPTCHA verification for players joining
custom_field
string
Optional custom field name to collect from players (e.g., “Student ID”, “Class”)
cqcs_enabled
boolean
default:"false"
Enable ClassQuiz Controller Support for physical remote controls
randomize_answers
boolean
default:"false"
Randomly shuffle answer order for each question

Game PIN Generation

When a game starts, the system generates a unique 6-digit game PIN:
game_pin = randint(100000, 999999)
game = await redis.get(f"game:{game_pin}")
while game is not None:
    game_pin = randint(100000, 999999)
    game = await redis.get(f"game:{game_pin}")
The PIN is guaranteed to be unique by checking Redis for collisions.

Game Data Structure

PlayGame Model

The live game session is stored in Redis using the PlayGame model (classquiz/db/models.py:237):
class PlayGame(BaseModel):
    quiz_id: uuid.UUID | str
    description: str
    user_id: uuid.UUID  # Game host
    title: str
    questions: list[QuizQuestion]
    game_id: uuid.UUID  # Unique game instance ID
    game_pin: str  # 6-digit PIN
    started: bool = False
    captcha_enabled: bool = False
    cover_image: str | None = None
    game_mode: str | None = None
    current_question: int = -1  # Index of current question (-1 = lobby)
    background_color: str | None = None
    background_image: str | None = None
    custom_field: str | None = None
    question_show: bool = False
Game data is stored in Redis with an 18,000 second (5 hour) expiration to automatically clean up inactive games.

Game Session State

Parallel to game data, a GameSession tracks participants and answers:
class GameSession(BaseModel):
    admin: str  # Host socket ID
    game_id: str
    answers: list[GameAnswer1 | None]  # Answers for each question
Players are stored in a Redis set:
game_session:{game_pin}:players

Player Join Flow

Joining a Game

1

Enter Game PIN

Players navigate to /play and enter the 6-digit game PIN
2

CAPTCHA Verification

If enabled, players must complete CAPTCHA verification
@router.get("/play/check_captcha/{game_pin}")
async def check_if_captcha_enabled(game_pin: str):
    game = await redis.get(f"game:{game_pin}")
    game = PlayGame.model_validate_json(game)
    return CheckIfCaptchaEnabledResponse(
        enabled=game.captcha_enabled,
        game_mode=game.game_mode,
        custom_field=game.custom_field
    )
3

Enter Username

Players enter their display name
4

Custom Field (Optional)

If configured, players must fill out the custom field (e.g., student ID)
5

WebSocket Connection

Player connects via Socket.IO and joins the game room
socket.emit('join_game', {
    username: username,
    game_pin: game_pin
});

Player Reconnection

Players can reconnect if they lose connection (frontend/src/routes/play/+page.svelte:78):
socket.on('connect', async () => {
    const cookie_data = Cookies.get('joined_game');
    if (!cookie_data) return;
    
    const data = JSON.parse(cookie_data);
    socket.emit('rejoin_game', {
        old_sid: data.sid,
        username: data.username,
        game_pin: data.game_pin
    });
});

Game Lifecycle

1. Lobby Phase

  • current_question = -1
  • started = false
  • Host sees player count updating in real-time
  • Players see waiting screen

2. Game Start

  • Host clicks “Start Game”
  • started = true
  • Title screen shown to all players
  • WebSocket event: start_game

3. Question Phase

For each question:
The host controls question flow using the admin interface:
  • Display question to players
  • Start timer
  • View live answer submissions
  • End question manually or when timer expires
  • Show correct answers and results

4. Results Phase

After each question:
  • Correct answer highlighted
  • Answer distribution shown
  • Top players displayed
  • Score calculations based on correctness and speed

5. Final Results

  • Leaderboard with final rankings
  • Export results option
  • Option to save results to database

Answer Submission & Scoring

Answer Data Structure

When a player submits an answer (classquiz/db/models.py:319):
class AnswerData(BaseModel):
    username: str
    answer: str  # The answer submitted
    right: bool  # Whether answer is correct
    time_taken: float  # Milliseconds to answer
    score: int  # Points awarded
Answers are stored in Redis:
game_session:{game_pin}:{question_number}

Score Calculation

Scoring factors:
  • Correctness: Must be correct to receive points
  • Speed: Faster answers receive more points
  • Question Time: Points scale based on question time limit
Player scores are tracked in Redis hash:
game_session:{game_pin}:player_scores

Live API Endpoints

The live API (classquiz/routers/live.py) provides real-time game data for integrations:

Get Live Game Data

GET /api/v1/live/?game_pin={pin}&api_key={key}
Returns complete game state including:
  • Quiz metadata
  • Current question
  • All submitted answers
  • Player count

Get Player Count

GET /api/v1/live/user_count?game_pin={pin}&api_key={key}

Get Current Question

GET /api/v1/live/get_question/now?game_pin={pin}&api_key={key}

Set Question (Control Flow)

POST /api/v1/live/set_question?game_pin={pin}&question_number={num}&api_key={key}
Allows external control of question flow via API.

Get Player Scores

GET /api/v1/live/scores?game_pin={pin}&api_key={key}
Returns sorted array of players by score:
[
  {"username": "Alice", "score": 1250},
  {"username": "Bob", "score": 980}
]

Custom Fields & Data Collection

Custom fields allow you to collect additional information from players:
custom_field = "Student ID"
Custom field responses are stored in Redis hash:
game:{game_pin}:players:custom_fields
This data is included when:
  • Viewing live game data
  • Exporting results
  • Saving results to database

Controller Support (CQCS)

ClassQuiz supports physical remote controllers for player input:
if cqcs_enabled:
    code = generate_code(6)
    await redis.set(f"game:cqc:code:{code}", game_pin, ex=3600)
Controllers connect using the generated code and can submit answers like regular players.

Game Modes

Different game modes affect presentation:
  • Normal: Standard quiz flow
  • Kahoot: Kahoot-style results display
The frontend adapts UI based on game_mode setting.

Answer Randomization

When randomize_answers is enabled (classquiz/routers/quiz.py:106):
if randomize_answers:
    for question in quiz.questions:
        if question["type"] == QuizQuestionType.RANGE:
            continue
        if question["type"] == QuizQuestionType.SLIDE:
            continue
        random.shuffle(question["answers"])
RANGE and SLIDE type questions are excluded from randomization as they have fixed answer structures.

Voting Results

For VOTING type questions, get live voting results:
GET /api/v1/live/voting?game_pin={pin}&api_key={key}
Returns answer distribution:
{
  "Option A": 15,
  "Option B": 8,
  "Option C": 22
}

Best Practices

Test Connection

Test your internet connection before hosting large games

Manage Player Count

Be aware of your server’s capacity for concurrent players

Use Custom Fields

Collect student IDs or class names for better result tracking

Enable CAPTCHA

Use CAPTCHA for public games to prevent bot spam
Games automatically expire after 5 hours. Save results before the game expires if you need them long-term.

Build docs developers (and LLMs) love