feat: complete realtime challenge matchmaking

This commit is contained in:
2026-08-09 03:07:38 +08:00
parent b6fc2431ca
commit 821311f4ad
11 changed files with 767 additions and 31 deletions
+159
View File
@@ -15,9 +15,14 @@ from contest.models import (
RealtimeMatch,
)
from contest.services import (
cancel_waiting_match,
create_challenge,
finalize_match,
find_match,
join_challenge,
match_payload,
normalize_answer,
refresh_match_state,
start_attempt,
submit_attempt,
)
@@ -62,6 +67,16 @@ def daily_contest(db):
return contest
@pytest.fixture
def realtime_contest(daily_contest):
daily_contest.kind = Contest.Kind.REALTIME
daily_contest.slug = "realtime-with-question"
daily_contest.title = "联机测试赛"
daily_contest.duration_seconds = 60
daily_contest.save(update_fields=["kind", "slug", "title", "duration_seconds"])
return daily_contest
@pytest.mark.parametrize(
"raw, expected",
[
@@ -201,3 +216,147 @@ def test_find_match_同分玩家配对并由_finalize_match_唯一结算_rating(
assert first.rating == 1016
assert second.rating == 984
assert RatingHistory.objects.filter(match=active).count() == 2
@pytest.mark.django_db
def test_challenge_code_创建者和加入者通过联机码进入同一场(realtime_contest):
first = User.objects.create_user(
username="challenge_owner",
password="StrongPass_2026",
nickname="房主",
)
second = User.objects.create_user(
username="challenge_guest",
password="StrongPass_2026",
nickname="访客",
)
waiting = create_challenge(first, realtime_contest)
active = join_challenge(second, waiting.challenge_code.lower())
owner_payload = match_payload(active, first)
guest_payload = match_payload(active, second)
assert len(waiting.challenge_code) == 6
assert active.id == waiting.id
assert active.match_type == RealtimeMatch.MatchType.CHALLENGE
assert active.status == RealtimeMatch.Status.ACTIVE
assert active.attempts.count() == 2
assert owner_payload["opponent"]["nickname"] == "访客"
assert guest_payload["opponent"]["nickname"] == "房主"
assert owner_payload["attempt"]["questions"] == guest_payload["attempt"]["questions"]
@pytest.mark.django_db
def test_random_match_不会加入联机码约战(realtime_contest):
owner = User.objects.create_user(
username="private_owner",
password="StrongPass_2026",
nickname="约战房主",
)
random_player = User.objects.create_user(
username="random_player",
password="StrongPass_2026",
nickname="随机玩家",
)
challenge = create_challenge(owner, realtime_contest)
random_match = find_match(random_player, realtime_contest)
assert challenge.status == RealtimeMatch.Status.WAITING
assert random_match.id != challenge.id
assert random_match.match_type == RealtimeMatch.MatchType.RANDOM
@pytest.mark.django_db
def test_realtime_submit_双方结束前不泄露答案且结束后结算(realtime_contest):
first = User.objects.create_user(
username="fair_player_one",
password="StrongPass_2026",
nickname="公平玩家一",
)
second = User.objects.create_user(
username="fair_player_two",
password="StrongPass_2026",
nickname="公平玩家二",
)
match = join_challenge(
second,
create_challenge(first, realtime_contest).challenge_code,
)
first_attempt = match.attempts.get(user=first)
second_attempt = match.attempts.get(user=second)
first_result = submit_attempt(
first,
first_attempt.id,
[{"order": 1, "answer": "42"}],
"fair-submit-one",
)
active_payload = match_payload(match, first)
assert "correct_answer" not in first_result["questions"][0]
assert active_payload["status"] == RealtimeMatch.Status.ACTIVE
assert active_payload["attempt"]["status"] == ContestAttempt.Status.SUBMITTED
submit_attempt(
second,
second_attempt.id,
[{"order": 1, "answer": "0"}],
"fair-submit-two",
)
match.refresh_from_db()
completed_payload = match_payload(match, first)
assert match.status == RealtimeMatch.Status.COMPLETED
assert completed_payload["result"]["winner"] == "self"
assert completed_payload["attempt"]["questions"][0]["correct_answer"] == "42"
assert completed_payload["opponent"]["score"] == 0
@pytest.mark.django_db
def test_realtime_timeout_未提交玩家自动过期并完成比赛(realtime_contest):
first = User.objects.create_user(
username="timeout_one",
password="StrongPass_2026",
nickname="超时玩家一",
)
second = User.objects.create_user(
username="timeout_two",
password="StrongPass_2026",
nickname="超时玩家二",
)
match = join_challenge(
second,
create_challenge(first, realtime_contest).challenge_code,
)
RealtimeMatch.objects.filter(id=match.id).update(
started_at=timezone.now() - timedelta(seconds=61)
)
refreshed = refresh_match_state(match.id)
assert refreshed.status == RealtimeMatch.Status.COMPLETED
assert not refreshed.attempts.filter(status=ContestAttempt.Status.ACTIVE).exists()
@pytest.mark.django_db
def test_challenge_owner_可取消等待中的联机码(realtime_contest):
owner = User.objects.create_user(
username="cancel_owner",
password="StrongPass_2026",
nickname="取消房主",
)
waiting = create_challenge(owner, realtime_contest)
cancelled = cancel_waiting_match(owner, waiting.id)
assert cancelled.status == RealtimeMatch.Status.CANCELLED
with pytest.raises(ValidationError, match="失效"):
join_challenge(
User.objects.create_user(
username="late_guest",
password="StrongPass_2026",
nickname="迟到访客",
),
waiting.challenge_code,
)