289 lines
10 KiB
Python
289 lines
10 KiB
Python
import uuid
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
|
|
|
|
class Question(models.Model):
|
|
class Track(models.TextChoices):
|
|
BEGINNER = "beginner", "入门"
|
|
STANDARD = "standard", "标准"
|
|
ADVANCED = "advanced", "进阶"
|
|
OPEN = "open", "Open"
|
|
|
|
slug = models.SlugField(unique=True)
|
|
track = models.CharField(max_length=16, choices=Track.choices)
|
|
tags = models.JSONField(default=list, blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
def __str__(self):
|
|
return self.slug
|
|
|
|
|
|
class QuestionVersion(models.Model):
|
|
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="versions")
|
|
version = models.PositiveIntegerField()
|
|
prompt = models.TextField()
|
|
answer = models.CharField(max_length=200)
|
|
explanation = models.TextField(blank=True)
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(fields=("question", "version"), name="unique_question_version")
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.question.slug} v{self.version}"
|
|
|
|
|
|
class Contest(models.Model):
|
|
class Kind(models.TextChoices):
|
|
REALTIME = "realtime", "实时 1v1"
|
|
DAILY = "daily", "今日挑战"
|
|
PRACTICE = "practice", "单人闯关"
|
|
WEEKLY = "weekly", "主题周赛"
|
|
|
|
class Status(models.TextChoices):
|
|
DRAFT = "draft", "草稿"
|
|
PUBLISHED = "published", "已发布"
|
|
CLOSED = "closed", "已结束"
|
|
|
|
slug = models.SlugField(unique=True)
|
|
title = models.CharField(max_length=120)
|
|
kind = models.CharField(max_length=16, choices=Kind.choices)
|
|
track = models.CharField(max_length=16, choices=Question.Track.choices)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.DRAFT)
|
|
duration_seconds = models.PositiveIntegerField(default=60)
|
|
starts_at = models.DateTimeField(null=True, blank=True)
|
|
ends_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
|
|
class ContestQuestion(models.Model):
|
|
contest = models.ForeignKey(Contest, on_delete=models.CASCADE, related_name="contest_questions")
|
|
question_version = models.ForeignKey(QuestionVersion, on_delete=models.PROTECT)
|
|
order = models.PositiveIntegerField()
|
|
points = models.PositiveIntegerField(default=100)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(fields=("contest", "order"), name="unique_contest_question_order")
|
|
]
|
|
ordering = ["order"]
|
|
|
|
|
|
class RealtimeMatch(models.Model):
|
|
class MatchType(models.TextChoices):
|
|
RANDOM = "random", "随机匹配"
|
|
CHALLENGE = "challenge", "联机码约战"
|
|
|
|
class Status(models.TextChoices):
|
|
WAITING = "waiting", "等待对手"
|
|
ACTIVE = "active", "进行中"
|
|
COMPLETED = "completed", "已完成"
|
|
CANCELLED = "cancelled", "已取消"
|
|
|
|
class GameKind(models.TextChoices):
|
|
QUIZ = "quiz", "口算竞速"
|
|
SUDOKU = "sudoku", "数独 Timerun"
|
|
TWENTY_FOUR = "twenty_four", "24 点竞速"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
contest = models.ForeignKey(Contest, on_delete=models.PROTECT)
|
|
match_type = models.CharField(
|
|
max_length=16,
|
|
choices=MatchType.choices,
|
|
default=MatchType.RANDOM,
|
|
)
|
|
challenge_code = models.CharField(max_length=8, unique=True, null=True, blank=True)
|
|
game_kind = models.CharField(
|
|
max_length=20,
|
|
choices=GameKind.choices,
|
|
default=GameKind.QUIZ,
|
|
)
|
|
player_one = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="matches_as_player_one"
|
|
)
|
|
player_two = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.PROTECT,
|
|
related_name="matches_as_player_two",
|
|
)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.WAITING)
|
|
player_one_rating = models.PositiveIntegerField()
|
|
player_two_rating = models.PositiveIntegerField(null=True, blank=True)
|
|
winner = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.PROTECT,
|
|
related_name="won_matches",
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField(null=True, blank=True)
|
|
started_at = models.DateTimeField(null=True, blank=True)
|
|
completed_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(
|
|
fields=(
|
|
"contest",
|
|
"match_type",
|
|
"game_kind",
|
|
"status",
|
|
"player_one_rating",
|
|
"created_at",
|
|
),
|
|
name="matchmaking_mode_idx",
|
|
)
|
|
]
|
|
|
|
|
|
class ContestAttempt(models.Model):
|
|
class Status(models.TextChoices):
|
|
ACTIVE = "active", "进行中"
|
|
SUBMITTED = "submitted", "已提交"
|
|
EXPIRED = "expired", "已超时"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
contest = models.ForeignKey(Contest, on_delete=models.PROTECT, related_name="attempts")
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contest_attempts")
|
|
match = models.ForeignKey(
|
|
RealtimeMatch, null=True, blank=True, on_delete=models.PROTECT, related_name="attempts"
|
|
)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.ACTIVE)
|
|
score = models.PositiveIntegerField(default=0)
|
|
correct_count = models.PositiveIntegerField(default=0)
|
|
answer_count = models.PositiveIntegerField(default=0)
|
|
duration_ms = models.PositiveIntegerField(default=0)
|
|
question_order = models.JSONField(default=list, blank=True)
|
|
submission_key = models.CharField(max_length=80, null=True, blank=True)
|
|
started_at = models.DateTimeField(auto_now_add=True)
|
|
submitted_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=("user", "submission_key"), name="unique_user_contest_submission"
|
|
)
|
|
]
|
|
ordering = ["-started_at"]
|
|
|
|
|
|
class ContestAnswer(models.Model):
|
|
attempt = models.ForeignKey(ContestAttempt, on_delete=models.CASCADE, related_name="answers")
|
|
contest_question = models.ForeignKey(ContestQuestion, on_delete=models.PROTECT)
|
|
submitted_answer = models.CharField(max_length=200)
|
|
is_correct = models.BooleanField()
|
|
elapsed_ms = models.PositiveIntegerField()
|
|
answered_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=("attempt", "contest_question"), name="unique_attempt_question_answer"
|
|
)
|
|
]
|
|
|
|
|
|
class RatingHistory(models.Model):
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="rating_history")
|
|
match = models.ForeignKey(RealtimeMatch, on_delete=models.PROTECT, related_name="rating_changes")
|
|
rating_before = models.PositiveIntegerField()
|
|
rating_after = models.PositiveIntegerField()
|
|
delta = models.SmallIntegerField()
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class LeaderboardSnapshot(models.Model):
|
|
contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
|
|
track = models.CharField(max_length=16, choices=Question.Track.choices)
|
|
period = models.CharField(max_length=40)
|
|
rankings = models.JSONField(default=list)
|
|
generated_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class CheatFlag(models.Model):
|
|
class Status(models.TextChoices):
|
|
OPEN = "open", "待处理"
|
|
DISMISSED = "dismissed", "已忽略"
|
|
CONFIRMED = "confirmed", "已确认"
|
|
|
|
attempt = models.ForeignKey(ContestAttempt, on_delete=models.CASCADE, related_name="cheat_flags")
|
|
reason = models.CharField(max_length=120)
|
|
evidence = models.JSONField(default=dict)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.OPEN)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class MathGameAttempt(models.Model):
|
|
class Kind(models.TextChoices):
|
|
SUDOKU = "sudoku", "数独"
|
|
TWENTY_FOUR = "twenty_four", "24 点"
|
|
|
|
class Difficulty(models.TextChoices):
|
|
EASY = "easy", "入门"
|
|
STANDARD = "standard", "标准"
|
|
HARD = "hard", "进阶"
|
|
|
|
class Status(models.TextChoices):
|
|
ACTIVE = "active", "进行中"
|
|
COMPLETED = "completed", "已完成"
|
|
FAILED = "failed", "未通过"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="math_game_attempts",
|
|
)
|
|
match = models.ForeignKey(
|
|
RealtimeMatch,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.PROTECT,
|
|
related_name="game_attempts",
|
|
)
|
|
kind = models.CharField(max_length=20, choices=Kind.choices)
|
|
difficulty = models.CharField(
|
|
max_length=16,
|
|
choices=Difficulty.choices,
|
|
default=Difficulty.STANDARD,
|
|
)
|
|
puzzle = models.JSONField(default=dict)
|
|
solution = models.JSONField(default=dict)
|
|
submission = models.JSONField(default=dict, blank=True)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.ACTIVE)
|
|
score = models.PositiveIntegerField(default=0)
|
|
duration_ms = models.PositiveIntegerField(default=0)
|
|
hints_used = models.PositiveSmallIntegerField(default=0)
|
|
submission_key = models.CharField(max_length=80, null=True, blank=True)
|
|
started_at = models.DateTimeField(auto_now_add=True)
|
|
submitted_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=("user", "submission_key"),
|
|
name="unique_user_math_game_submission",
|
|
),
|
|
models.UniqueConstraint(
|
|
fields=("match", "user"),
|
|
name="unique_user_realtime_math_game",
|
|
),
|
|
]
|
|
indexes = [
|
|
models.Index(
|
|
fields=("kind", "status", "-score", "duration_ms"),
|
|
name="math_game_ranking_idx",
|
|
)
|
|
]
|
|
ordering = ["-started_at"]
|