Implemented ClassQuizController-logic

This commit is contained in:
Mawoka
2023-03-27 21:04:08 +02:00
parent 603939fb20
commit 46ff32a986
13 changed files with 488 additions and 98 deletions
+4
View File
@@ -32,6 +32,7 @@ from classquiz.routers import (
avatar,
results,
admin,
box_controller,
)
from classquiz.socket_server import sio
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
@@ -85,6 +86,9 @@ async def auth_middleware_wrapper(request: Request, call_next):
return await rememberme_middleware(request, call_next)
app.include_router(
box_controller.router, tags=["boxcontroller"], prefix="/api/v1/box-controller", include_in_schema=True
)
app.include_router(results.router, tags=["results"], prefix="/api/v1/results", include_in_schema=True)
app.include_router(remote.router, tags=["remote"], prefix="/api/v1/remote", include_in_schema=True)
app.include_router(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
+2 -1
View File
@@ -241,11 +241,12 @@ class PlayGame(BaseModel):
background_color: str | None
background_image: str | None
custom_field: str | None
question_show: bool = False
class GamePlayer(BaseModel):
username: str
sid: str
sid: str | None
class GameAnswer2(BaseModel):
@@ -0,0 +1,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from fastapi import APIRouter
from classquiz.routers.box_controller import web, embedded
router = APIRouter()
router.include_router(web.router, prefix="/web")
router.include_router(embedded.router, prefix="/embedded")
@@ -0,0 +1,153 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import enum
import os
import typing
from datetime import datetime
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, status
from pydantic import BaseModel, ValidationError
from classquiz.config import redis
from classquiz.db.models import PlayGame, QuizQuestionType, AnswerData, GamePlayer
from classquiz.socket_server import sio, calculate_score, set_answer
router = APIRouter()
class JoinGameInput(BaseModel):
code: str
name: str
class JoinGameResponse(BaseModel):
id: str
@router.post("/join")
async def join_game(data: JoinGameInput):
game_pin = await redis.get(f"game:cqc:code:{data.code}")
if game_pin is None:
raise HTTPException(status_code=404, detail="Game not found")
game = await redis.get(f"game:{game_pin}")
game = PlayGame.parse_raw(game)
# Check if game is already running
if game.started:
raise HTTPException(status_code=400, detail="Game started already")
# check if username already exists
if await redis.get(f"game_session:{game_pin}:players:{data.name}") is not None:
raise HTTPException(status_code=409, detail="Username already exists")
player_id = os.urandom(5).hex()
await redis.set(f"game:cqc:player:{player_id}", data.name)
return JoinGameResponse(id=f"{player_id}:{game_pin}")
class SubmitAnswerInput(BaseModel):
answer: int
async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now: datetime):
redis_res_game = await redis.get(f"game:{game_pin}")
username = await redis.get(f"game:cqc:player:{player_id}")
if redis_res_game is None or username is None:
raise HTTPException(status_code=404, detail="id not existent")
game = PlayGame.parse_raw(redis_res_game)
if not game.question_show:
return
question = game.questions[game.current_question]
question_time = datetime.fromisoformat(await redis.get(f"game:{game_pin}:current_time"))
try:
selected_answer = question.answers[data_answer].answer
except KeyError:
return
answer_right = False
if question.type == QuizQuestionType.ABCD:
for answer in question.answers:
if answer.answer == selected_answer and answer.right:
answer_right = True
break
elif question.type == QuizQuestionType.VOTING:
answer_right = False
else:
return
diff = (question_time - now).total_seconds() * 1000
score = 0
if answer_right:
score = calculate_score(abs(diff), int(float(question.time)))
await redis.hincrby(f"game_session:{game_pin}:player_scores", username, score)
answer_data = AnswerData(
username=username,
answer=selected_answer,
right=answer_right,
time_taken=abs(diff),
score=score,
)
answers = await redis.get(f"game_session:{game_pin}:{game.current_question}")
answers = await set_answer(answers, game_pin=game_pin, data=answer_data, q_index=game.current_question)
player_count = await redis.scard(f"game_session:{game_pin}:players")
print(player_count, answers)
if answers is not None and len(answers.__root__) == player_count:
await sio.emit("everyone_answered", {})
class WebSocketTypes(enum.Enum):
ButtonPress = "bp"
Error = "e"
class WebSocketRequest(BaseModel):
type: WebSocketTypes
data: typing.Any
wss_clients = {}
button_to_index_map = {"b": 0, "g": 1, "y": 2, "r": 3}
@router.websocket("/socket/{id}")
async def websocket_endpoint(ws: WebSocket, game_id: str, id: str):
try:
if id in wss_clients.keys():
await ws.close(code=status.WS_1001_GOING_AWAY)
print("Client {} already exists.".format(id))
return
await ws.accept()
wss_clients[id] = ws
player_id, game_pin = game_id.split(":")
if player_id is None or game_pin is None:
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="BadId").json())
await ws.close(code=status.WS_1003_UNSUPPORTED_DATA)
username = await redis.get(f"game:cqc:player:{player_id}")
await sio.emit(
"player_joined",
{"username": username, "sid": None},
room=f"admin:{game_pin}",
)
await redis.sadd(f"game_session:{game_pin}:players", GamePlayer(username=username, sid=None).json())
while True:
raw_data = await ws.receive_text()
try:
data = WebSocketRequest.parse_raw(raw_data)
except ValidationError:
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="ValidationError").json())
continue
if data.type == WebSocketTypes.ButtonPress:
now = datetime.now()
try:
answer_index = button_to_index_map[data.data.lower()]
except (KeyError, AttributeError):
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="InvalidButton").json())
continue
await submit_answer_fn(answer_index, game_pin, player_id, now)
print("Data from client {}: {}".format(id, data))
except WebSocketDisconnect as ex:
print("Client {} is disconnected: {}".format(id, ex))
wss_clients.pop(id, None)
@@ -0,0 +1,25 @@
## Basic Request
Every request is a json-object: `{"type": "SOME_TYPE", "data": "ANY_DATA"}`
### Types
#### e (Error)
- [ ] Client
- [x] Server
Pretty self-explanatory.
Value is a CamelCase errorcode.
Codes:
- `ValidationError`
- `BadId`
#### bp (ButtonPress)
- [x] Client
- [ ] Server
Sends a button-press to the server, where `data` is either `b`, `g`, `y` or `r`. Capital letters indicate a long-press.
+52
View File
@@ -0,0 +1,52 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import random
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from classquiz.auth import get_current_user
from classquiz.db.models import User, PlayGame
from classquiz.config import redis
router = APIRouter()
def generate_code() -> str:
specified_length = 6
buttons = [
"B",
"b",
"G",
"g",
"Y",
"y",
"R",
"r",
] # Capital stands for long press, lowercase letter for short press
resulting_code = ""
for i in range(specified_length):
resulting_code += random.choice(buttons)
return resulting_code
class ActivateCqbForCurrentQuizInput(BaseModel):
game_pin: int
class ActivateCqbForCurrentQuizResponse(BaseModel):
code: str
@router.post("/activate-for-quiz", response_model=ActivateCqbForCurrentQuizResponse)
async def activate_cqc_for_current_quiz(data: ActivateCqbForCurrentQuizInput, user: User = Depends(get_current_user)):
redis_res = await redis.get(f"game:{data.game_pin}")
if redis_res is None:
raise HTTPException(status_code=404, detail="Game not found")
game_data = PlayGame.parse_raw(redis_res)
if game_data.user_id != user.id:
raise HTTPException(status_code=401, detail="The quiz wasn't started by you")
code = generate_code()
await redis.set(f"game:cqc:code:{code}", game_data.game_pin, ex=3600)
return ActivateCqbForCurrentQuizResponse(code=code)
+26 -1
View File
@@ -3,6 +3,7 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import json
import random
import re
import uuid
from datetime import datetime
@@ -98,12 +99,31 @@ async def get_public_quiz(quiz_id: str):
return PublicQuizResponse(**quiz.dict())
def generate_code() -> str:
specified_length = 6
buttons = [
"B",
"b",
"G",
"g",
"Y",
"y",
"R",
"r",
] # Capital stands for long press, lowercase letter for short press
resulting_code = ""
for i in range(specified_length):
resulting_code += random.choice(buttons)
return resulting_code
@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,
user: User = Depends(get_current_user),
):
try:
@@ -138,14 +158,19 @@ async def start_quiz(
custom_field=custom_field,
background_image=quiz.background_image,
)
code = None
if cqcs_enabled:
code = generate_code()
await redis.set(f"game:cqc:code:{code}", game_pin, ex=3600)
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
await redis.set(
f"game_in_lobby:{user.id.hex}",
GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).json(),
ex=900,
)
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"}), "cqc_code": code}
class CheckIfCaptchaEnabledResponse(BaseModel):
+37 -38
View File
@@ -51,6 +51,26 @@ async def generate_final_results(game_data: PlayGame, game_pin: str) -> dict:
return results
def calculate_score(z: float, t: int) -> int:
t = t * 1000
res = (t - z) / t
return int(res * 1000)
async def set_answer(answers, game_pin: str, q_index: int, data: AnswerData) -> AnswerDataList:
if answers is None:
answers = AnswerDataList(__root__=[data])
else:
answers = AnswerDataList.parse_raw(answers)
answers.__root__.append(data)
await redis.set(
f"game_session:{game_pin}:{q_index}",
answers.json(),
ex=7200,
)
return answers
class _JoinGameData(BaseModel):
username: str
game_pin: str
@@ -205,6 +225,9 @@ async def get_question_results(sid: str, data: dict):
session = await sio.get_session(sid)
if session["admin"]:
redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}")
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
game_data.question_show = False
await redis.set(f"game:{session['game_pin']}", game_data.json())
game_pin = session["game_pin"]
await sio.emit("question_results", redis_res, room=game_pin)
@@ -244,6 +267,7 @@ async def set_question_number(sid, data: str):
game_pin = session["game_pin"]
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
game_data.current_question = int(float(data))
game_data.question_show = True
await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200)
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200)
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))]
@@ -323,7 +347,6 @@ async def submit_answer(sid: str, data: dict):
data.answer = ", ".join(answer_order)
if correct_answers == data.dict()["complex_answer"]:
answer_right = True
# TODO Set data.answer to a real value for export
elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.TEXT:
answer_right = False
@@ -344,10 +367,6 @@ async def submit_answer(sid: str, data: dict):
diff = (time_q_started - now).total_seconds() * 1000 # - timedelta(milliseconds=latency)
# print(abs(diff) - latency, latency, abs(diff))
def calculate_score(z: float, t: int) -> int:
t = t * 1000
res = (t - z) / t
return int(res * 1000)
score = 0
if answer_right:
@@ -355,39 +374,16 @@ async def submit_answer(sid: str, data: dict):
abs(diff) - latency, int(float(game_data.questions[int(float(data.question_index))].time))
)
await redis.hincrby(f"game_session:{session['game_pin']}:player_scores", session["username"], score)
if answers is None:
await redis.set(
f"game_session:{session['game_pin']}:{data.question_index}",
AnswerDataList(
__root__=[
AnswerData(
username=session["username"],
answer=data.answer,
right=answer_right,
time_taken=abs(diff) - latency,
score=score,
)
]
).json(),
ex=7200,
)
else:
answers = AnswerDataList.parse_raw(answers)
answers.__root__.append(
AnswerData(
username=session["username"],
answer=data.answer,
right=answer_right,
time_taken=abs(diff) - latency,
score=score,
)
)
await redis.set(
f"game_session:{session['game_pin']}:{data.question_index}",
answers.json(),
ex=7200,
)
answers = AnswerDataList.parse_raw(await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"))
answer_data = AnswerData(
username=session["username"],
answer=data.answer,
right=answer_right,
time_taken=abs(diff) - latency,
score=score,
)
answers = await set_answer(
answers, game_pin=session["game_pin"], data=answer_data, q_index=int(float(data.question_index))
)
player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
if len(answers.__root__) == player_count:
# await sio.emit(
@@ -395,6 +391,9 @@ async def submit_answer(sid: str, data: dict):
# await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"),
# room=session["game_pin"],
# )
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
game_data.question_show = False
await redis.set(f"game:{session['game_pin']}", game_data.json())
await sio.emit("everyone_answered", {})
+47 -5
View File
@@ -10,12 +10,21 @@
import { fade } from 'svelte/transition';
import Spinner from '$lib/Spinner.svelte';
import { onMount } from 'svelte';
import { createTippy } from 'svelte-tippy';
export let quiz_id;
let captcha_selected = false;
let selected_game_mode = 'kahoot';
let loading = false;
let custom_field = '';
let cqcs_enabled = false;
const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top-start',
allowHTML: true
});
onMount(() => {
const ls_data = localStorage.getItem('custom_field');
@@ -26,16 +35,17 @@
let res;
loading = true;
localStorage.setItem('custom_field', custom_field);
const cqcs_enabled_parsed = cqcs_enabled ? 'True' : 'False';
if (captcha_enabled && captcha_selected) {
res = await fetch(
`/api/v1/quiz/start/${id}?captcha_enabled=True&game_mode=${selected_game_mode}&custom_field=${custom_field}`,
`/api/v1/quiz/start/${id}?captcha_enabled=True&game_mode=${selected_game_mode}&custom_field=${custom_field}&cqcs_enabled=${cqcs_enabled_parsed}`,
{
method: 'POST'
}
);
} else {
res = await fetch(
`/api/v1/quiz/start/${id}?captcha_enabled=False&game_mode=${selected_game_mode}&custom_field=${custom_field}`,
`/api/v1/quiz/start/${id}?captcha_enabled=False&game_mode=${selected_game_mode}&custom_field=${custom_field}&cqcs_enabled=${cqcs_enabled_parsed}`,
{
method: 'POST'
}
@@ -54,7 +64,9 @@
const data = await res.json();
// eslint-disable-next-line no-undef
plausible('Started Game', { props: { quiz_id: id, game_id: data.game_id } });
window.location.assign(`/admin?token=${data.game_id}&pin=${data.game_pin}&connect=1`);
window.location.assign(
`/admin?token=${data.game_id}&pin=${data.game_pin}&connect=1&cqc_code=${data.cqc_code}`
);
}
};
</script>
@@ -89,7 +101,7 @@
</label>
</div>
{#if captcha_selected}
<div class="flex justify-center mt-2" transition:fade>
<div class="flex justify-center mt-2" in:fade>
<p class="w-1/3">
If enabled, Google's ReCaptcha will load in the browser of players. Only enable
if you really need it, since you need the consent of <b>EVERY</b> player to load
@@ -128,7 +140,7 @@
</p>
</div>
</div>
<div class="flex justify-center items-center">
<div class="flex justify-center items-center my-auto">
<label class="mr-4">Custom Field</label>
<input
bind:value={custom_field}
@@ -136,6 +148,36 @@
placeholder="Phone Number or Email"
/>
</div>
<div class="flex justify-center w-full my-auto">
<label
for="cqc-toggle"
class="inline-flex relative items-center cursor-pointer"
class:pointer-events-none={!captcha_enabled}
class:opacity-50={!captcha_enabled}
>
<input
type="checkbox"
bind:checked={cqcs_enabled}
id="cqc-toggle"
class="sr-only peer"
/>
<span
class="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
/>
<span class="ml-3 text-sm font-medium text-gray-900"
><a
href="/controller"
target="_blank"
use:tippy={{
content:
'ClassQuizControllers are small physical devices to play ClassQuiz. Click to learn more.'
}}
class="decoration-dashed underline cursor-help">ClassQuizControllers</a
>
are {cqcs_enabled ? 'enabled' : 'disabled'}</span
>
</label>
</div>
<button
class="mt-auto mx-auto bg-green-500 p-4 rounded-lg shadow-lg hover:bg-green-400 transition-all marck-script text-2xl"
@@ -0,0 +1,104 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import AudioPlayer from '$lib/play/audio_player.svelte';
import { getLocalization } from '$lib/i18n';
export let game_pin: string;
export let players;
export let socket;
export let cqc_code: string;
const { t } = getLocalization();
let play_music = false;
if (cqc_code === 'null') {
cqc_code = null;
}
const kick_player = (username: string) => {
socket.emit('kick_player', { username: username });
for (let i = 0; i < players.length; i++) {
console.log(players[i].username, username);
if (players[i].username === username) {
players.splice(i, 1);
break;
}
}
players = players;
};
const color_map = {
r: 'red',
g: 'green',
y: 'yellow',
b: 'blue'
};
</script>
<div class="w-full h-full">
<AudioPlayer bind:play={play_music} />
<div class="grid grid-cols-3 mt-12">
<div class="flex justify-center">
<p class="m-auto text-2xl">
Join at <b>{window.location.host}/play</b> and enter <b>{game_pin}</b>.
</p>
</div>
<img
alt="QR code to join the game"
src="/api/v1/utils/qr/{game_pin}"
class="block mx-auto w-1/2 dark:bg-white"
/>
{#if cqc_code}
<div class="m-auto">
<div class="flex-col flex justify-center">
<p class="mx-auto">Join by entering the following code</p>
<div class="flex flex-row gap-2 mx-auto">
{#each cqc_code as c}
<div class="flex flex-col">
<p class="text-center">{c}</p>
<span
style="background-color: {color_map[
c.toLowerCase()
]}; width: 2rem; height: {c.toLowerCase() == c ? '2' : '4'}rem"
/>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<p class="text-3xl text-center ">{$t('words.pin')}: {game_pin}</p>
<div class="flex justify-center w-full mt-4">
<ul class="list-disc pl-8">
{#if players.length > 0}
{#each players as player}
<li>
<span
class="hover:line-through"
on:click={() => {
kick_player(player.username);
}}>{player.username}</span
>
<!-- <button>{$t('words.kick')}</button>-->
</li>
{/each}
{/if}
</ul>
</div>
{#if players.length > 0}
<div class="flex justify-center w-full mt-4">
<button
class="ml-4 admin-button"
id="startGame"
on:click={() => {
socket.emit('start_game', '');
}}
>{$t('admin_page.start_game')}
</button>
</div>
{/if}
</div>
+8 -53
View File
@@ -11,11 +11,12 @@
import { navbarVisible } from '$lib/stores';
import type { PlayerAnswer, Player } from '$lib/admin.ts';
import SomeAdminScreen from '$lib/admin.svelte';
import AudioPlayer from '$lib/play/audio_player.svelte';
import GameNotStarted from '$lib/play/admin/game_not_started.svelte';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import FinalResults from '$lib/play/admin/final_results.svelte';
import GrayButton from '$lib/components/buttons/gray.svelte';
import { page } from '$app/stores';
navbarVisible.set(false);
@@ -41,7 +42,6 @@
let success = false;
let dataexport_download_a;
let warnToLeave = true;
let play_music = false;
const connect = async () => {
socket.emit('register_as_admin', {
@@ -127,17 +127,6 @@
window.matchMedia('(prefers-color-scheme: dark)').matches);
}
const kick_player = (username: string) => {
socket.emit('kick_player', { username: username });
for (let i = 0; i < players.length; i++) {
console.log(players[i].username, username);
if (players[i].username === username) {
players.splice(i, 1);
break;
}
}
players = players;
};
let bg_color;
let bg_image;
let results_saved = false;
@@ -201,46 +190,12 @@
<p class="text-red-700">{errorMessage}</p>
{/if}
{:else if !game_started}
<div class="w-full h-full">
<AudioPlayer bind:play={play_music} />
<div>
<img
alt="QR code to join the game"
src="/api/v1/utils/qr/{quiz_data.game_pin}"
class="block mx-auto w-1/6 mt-12 dark:bg-white"
/>
</div>
<p class="text-3xl text-center ">{$t('words.pin')}: {quiz_data.game_pin}</p>
<div class="flex justify-center w-full mt-4">
<ul class="list-disc pl-8">
{#if players.length > 0}
{#each players as player}
<li>
<span
class="hover:line-through"
on:click={() => {
kick_player(player.username);
}}>{player.username}</span
>
<!-- <button>{$t('words.kick')}</button>-->
</li>
{/each}
{/if}
</ul>
</div>
{#if players.length > 0}
<div class="flex justify-center w-full mt-4">
<button
class="ml-4 admin-button"
id="startGame"
on:click={() => {
socket.emit('start_game', '');
}}
>{$t('admin_page.start_game')}
</button>
</div>
{/if}
</div>
<GameNotStarted
{game_pin}
bind:players
{socket}
cqc_code={$page.url.searchParams.get('cqc_code')}
/>
{:else}
<SomeAdminScreen
bind:final_results
@@ -0,0 +1,13 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<div>
<h1 class="text-center text-8xl marck-script mt-12">ClassQuizController</h1>
<div>
<p class="text-center">Play a quiz with a physical controller, not on a touchscreen!</p>
</div>
<p class="text-center text-4xl m-12">More infos will follow soon</p>
</div>
+6
View File
@@ -122,3 +122,9 @@ Holds the custom-field data, but is only set if the custom-field is enabled.
data: `{PLAYER_NAME} = {CUSTOM_FIELD_VALUE}`
## game:cqb:code:{cqc_code} [string]
{cqc_code} is the code used to join with a **C**lass**Q**uiz**C**ontroller
Only holds the game-pin