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
@@ -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)