148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
import secrets
|
|
from datetime import timedelta
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from channels.layers import get_channel_layer
|
|
from django.db import transaction
|
|
from django.utils import timezone
|
|
from rest_framework.exceptions import ValidationError
|
|
|
|
from .models import BoardSession
|
|
|
|
BOARD_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
BOARD_TTL = timedelta(minutes=30)
|
|
DRAW_GUESS_TARGETS = (
|
|
"抛物线",
|
|
"三角形",
|
|
"勾股定理",
|
|
"质数",
|
|
"圆",
|
|
"正弦函数",
|
|
"分数",
|
|
"坐标系",
|
|
)
|
|
|
|
|
|
def _new_board_code():
|
|
for _ in range(20):
|
|
code = "".join(secrets.choice(BOARD_CODE_ALPHABET) for _ in range(6))
|
|
if not BoardSession.objects.filter(code=code).exists():
|
|
return code
|
|
raise ValidationError("暂时无法生成画板联机码,请稍后重试")
|
|
|
|
|
|
def broadcast_board(session_id, reason):
|
|
channel_layer = get_channel_layer()
|
|
if channel_layer is None:
|
|
return
|
|
async_to_sync(channel_layer.group_send)(
|
|
f"board_{session_id}",
|
|
{
|
|
"type": "board.state",
|
|
"reason": reason,
|
|
},
|
|
)
|
|
|
|
|
|
def board_payload(session, user):
|
|
is_host = session.host_id == user.id
|
|
reveal_target = (
|
|
session.mode == BoardSession.Mode.DRAW_GUESS
|
|
and (is_host or session.status == BoardSession.Status.COMPLETED)
|
|
)
|
|
return {
|
|
"session_id": session.id,
|
|
"code": session.code,
|
|
"mode": session.mode,
|
|
"mode_label": session.get_mode_display(),
|
|
"status": session.status,
|
|
"role": "host" if is_host else "guest",
|
|
"target": session.target if reveal_target else None,
|
|
"host": session.host.nickname,
|
|
"guest": session.guest.nickname if session.guest else None,
|
|
"host_score": session.host_score,
|
|
"guest_score": session.guest_score,
|
|
"expires_at": session.expires_at,
|
|
"websocket_path": f"/ws/v1/toolbox/boards/{session.id}/",
|
|
}
|
|
|
|
|
|
@transaction.atomic
|
|
def create_board(user, mode):
|
|
if mode not in BoardSession.Mode.values:
|
|
raise ValidationError({"mode": "不支持的画板联机模式"})
|
|
BoardSession.objects.filter(
|
|
host=user,
|
|
status=BoardSession.Status.WAITING,
|
|
).update(status=BoardSession.Status.CANCELLED)
|
|
return BoardSession.objects.create(
|
|
code=_new_board_code(),
|
|
mode=mode,
|
|
host=user,
|
|
target=(
|
|
secrets.choice(DRAW_GUESS_TARGETS)
|
|
if mode == BoardSession.Mode.DRAW_GUESS
|
|
else ""
|
|
),
|
|
expires_at=timezone.now() + BOARD_TTL,
|
|
)
|
|
|
|
|
|
@transaction.atomic
|
|
def join_board(user, code):
|
|
normalized = str(code or "").strip().upper()
|
|
if len(normalized) != 6 or any(
|
|
character not in BOARD_CODE_ALPHABET for character in normalized
|
|
):
|
|
raise ValidationError({"code": "画板联机码应为 6 位大写字母或数字"})
|
|
try:
|
|
session = (
|
|
BoardSession.objects.select_for_update()
|
|
.select_related("host", "guest")
|
|
.get(code=normalized)
|
|
)
|
|
except BoardSession.DoesNotExist as exc:
|
|
raise ValidationError({"code": "画板联机码不存在"}) from exc
|
|
if session.host_id == user.id:
|
|
raise ValidationError({"code": "不能加入自己创建的画板"})
|
|
if session.status != BoardSession.Status.WAITING:
|
|
raise ValidationError({"code": "画板联机码已失效或已被使用"})
|
|
if session.expires_at <= timezone.now():
|
|
session.status = BoardSession.Status.CANCELLED
|
|
session.save(update_fields=["status"])
|
|
raise ValidationError({"code": "画板联机码已经过期"})
|
|
session.guest = user
|
|
session.status = BoardSession.Status.ACTIVE
|
|
session.save(update_fields=["guest", "status"])
|
|
transaction.on_commit(lambda: broadcast_board(session.id, "joined"))
|
|
return session
|
|
|
|
|
|
@transaction.atomic
|
|
def submit_guess(user, session_id, raw_guess):
|
|
session = (
|
|
BoardSession.objects.select_for_update()
|
|
.select_related("host", "guest")
|
|
.get(id=session_id)
|
|
)
|
|
if session.guest_id != user.id:
|
|
raise ValidationError("只有猜题方可以提交答案")
|
|
if (
|
|
session.mode != BoardSession.Mode.DRAW_GUESS
|
|
or session.status != BoardSession.Status.ACTIVE
|
|
):
|
|
raise ValidationError("当前画板不接受猜题")
|
|
guess = str(raw_guess or "").strip()
|
|
if not guess or len(guess) > 40:
|
|
raise ValidationError({"guess": "请输入不超过 40 个字符的数学对象"})
|
|
correct = guess.replace(" ", "").lower() == session.target.replace(" ", "").lower()
|
|
if correct:
|
|
session.guest_score += 1
|
|
session.status = BoardSession.Status.COMPLETED
|
|
session.completed_at = timezone.now()
|
|
session.save(
|
|
update_fields=["guest_score", "status", "completed_at"]
|
|
)
|
|
transaction.on_commit(lambda: broadcast_board(session.id, "completed"))
|
|
return session, correct
|