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, avatar,
results, results,
admin, admin,
box_controller,
) )
from classquiz.socket_server import sio from classquiz.socket_server import sio
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks 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) 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(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(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) 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_color: str | None
background_image: str | None background_image: str | None
custom_field: str | None custom_field: str | None
question_show: bool = False
class GamePlayer(BaseModel): class GamePlayer(BaseModel):
username: str username: str
sid: str sid: str | None
class GameAnswer2(BaseModel): 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/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
import json import json
import random
import re import re
import uuid import uuid
from datetime import datetime from datetime import datetime
@@ -98,12 +99,31 @@ async def get_public_quiz(quiz_id: str):
return PublicQuizResponse(**quiz.dict()) 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}") @router.post("/start/{quiz_id}")
async def start_quiz( async def start_quiz(
quiz_id: str, quiz_id: str,
game_mode: str, game_mode: str,
captcha_enabled: bool = True, captcha_enabled: bool = True,
custom_field: str | None = None, custom_field: str | None = None,
cqcs_enabled: bool = False,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
): ):
try: try:
@@ -138,14 +158,19 @@ async def start_quiz(
custom_field=custom_field, custom_field=custom_field,
background_image=quiz.background_image, 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:{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_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
await redis.set( await redis.set(
f"game_in_lobby:{user.id.hex}", f"game_in_lobby:{user.id.hex}",
GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).json(), GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).json(),
ex=900, 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): 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 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): class _JoinGameData(BaseModel):
username: str username: str
game_pin: str game_pin: str
@@ -205,6 +225,9 @@ async def get_question_results(sid: str, data: dict):
session = await sio.get_session(sid) session = await sio.get_session(sid)
if session["admin"]: if session["admin"]:
redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") 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"] game_pin = session["game_pin"]
await sio.emit("question_results", redis_res, room=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_pin = session["game_pin"]
game_data = PlayGame.parse_raw(await redis.get(f"game:{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.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']}", game_data.json(), ex=7200)
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), 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))] 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) data.answer = ", ".join(answer_order)
if correct_answers == data.dict()["complex_answer"]: if correct_answers == data.dict()["complex_answer"]:
answer_right = True 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: elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.TEXT:
answer_right = False 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) diff = (time_q_started - now).total_seconds() * 1000 # - timedelta(milliseconds=latency)
# print(abs(diff) - latency, latency, abs(diff)) # 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 score = 0
if answer_right: 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)) 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) await redis.hincrby(f"game_session:{session['game_pin']}:player_scores", session["username"], score)
if answers is None: answer_data = AnswerData(
await redis.set( username=session["username"],
f"game_session:{session['game_pin']}:{data.question_index}", answer=data.answer,
AnswerDataList( right=answer_right,
__root__=[ time_taken=abs(diff) - latency,
AnswerData( score=score,
username=session["username"], )
answer=data.answer, answers = await set_answer(
right=answer_right, answers, game_pin=session["game_pin"], data=answer_data, q_index=int(float(data.question_index))
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}"))
player_count = await redis.scard(f"game_session:{session['game_pin']}:players") player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
if len(answers.__root__) == player_count: if len(answers.__root__) == player_count:
# await sio.emit( # 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}"), # await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"),
# room=session["game_pin"], # 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", {}) await sio.emit("everyone_answered", {})
+47 -5
View File
@@ -10,12 +10,21 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import Spinner from '$lib/Spinner.svelte'; import Spinner from '$lib/Spinner.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { createTippy } from 'svelte-tippy';
export let quiz_id; export let quiz_id;
let captcha_selected = false; let captcha_selected = false;
let selected_game_mode = 'kahoot'; let selected_game_mode = 'kahoot';
let loading = false; let loading = false;
let custom_field = ''; let custom_field = '';
let cqcs_enabled = false;
const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top-start',
allowHTML: true
});
onMount(() => { onMount(() => {
const ls_data = localStorage.getItem('custom_field'); const ls_data = localStorage.getItem('custom_field');
@@ -26,16 +35,17 @@
let res; let res;
loading = true; loading = true;
localStorage.setItem('custom_field', custom_field); localStorage.setItem('custom_field', custom_field);
const cqcs_enabled_parsed = cqcs_enabled ? 'True' : 'False';
if (captcha_enabled && captcha_selected) { if (captcha_enabled && captcha_selected) {
res = await fetch( 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' method: 'POST'
} }
); );
} else { } else {
res = await fetch( 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' method: 'POST'
} }
@@ -54,7 +64,9 @@
const data = await res.json(); const data = await res.json();
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
plausible('Started Game', { props: { quiz_id: id, game_id: data.game_id } }); 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> </script>
@@ -89,7 +101,7 @@
</label> </label>
</div> </div>
{#if captcha_selected} {#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"> <p class="w-1/3">
If enabled, Google's ReCaptcha will load in the browser of players. Only enable 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 if you really need it, since you need the consent of <b>EVERY</b> player to load
@@ -128,7 +140,7 @@
</p> </p>
</div> </div>
</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> <label class="mr-4">Custom Field</label>
<input <input
bind:value={custom_field} bind:value={custom_field}
@@ -136,6 +148,36 @@
placeholder="Phone Number or Email" placeholder="Phone Number or Email"
/> />
</div> </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 <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" 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 { navbarVisible } from '$lib/stores';
import type { PlayerAnswer, Player } from '$lib/admin.ts'; import type { PlayerAnswer, Player } from '$lib/admin.ts';
import SomeAdminScreen from '$lib/admin.svelte'; 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 { browser } from '$app/environment';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import FinalResults from '$lib/play/admin/final_results.svelte'; import FinalResults from '$lib/play/admin/final_results.svelte';
import GrayButton from '$lib/components/buttons/gray.svelte'; import GrayButton from '$lib/components/buttons/gray.svelte';
import { page } from '$app/stores';
navbarVisible.set(false); navbarVisible.set(false);
@@ -41,7 +42,6 @@
let success = false; let success = false;
let dataexport_download_a; let dataexport_download_a;
let warnToLeave = true; let warnToLeave = true;
let play_music = false;
const connect = async () => { const connect = async () => {
socket.emit('register_as_admin', { socket.emit('register_as_admin', {
@@ -127,17 +127,6 @@
window.matchMedia('(prefers-color-scheme: dark)').matches); 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_color;
let bg_image; let bg_image;
let results_saved = false; let results_saved = false;
@@ -201,46 +190,12 @@
<p class="text-red-700">{errorMessage}</p> <p class="text-red-700">{errorMessage}</p>
{/if} {/if}
{:else if !game_started} {:else if !game_started}
<div class="w-full h-full"> <GameNotStarted
<AudioPlayer bind:play={play_music} /> {game_pin}
<div> bind:players
<img {socket}
alt="QR code to join the game" cqc_code={$page.url.searchParams.get('cqc_code')}
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>
{:else} {:else}
<SomeAdminScreen <SomeAdminScreen
bind:final_results 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}` 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