feat: complete realtime challenge matchmaking
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user