Skip to content

feat: implement CCW contests#14

Open
rohit-h11 wants to merge 18 commits into
devfrom
CCW_Contests
Open

feat: implement CCW contests#14
rohit-h11 wants to merge 18 commits into
devfrom
CCW_Contests

Conversation

@rohit-h11

Copy link
Copy Markdown

@RsbhThakur RsbhThakur changed the title feat: stage 1 - contest infrastructure, real-time presence gateway & background workers feat: implement CCW contests Jun 21, 2026
@RsbhThakur

Copy link
Copy Markdown
Contributor

feat: CF Sync Engine & Validation Matrix (Stage 2) - Complete

Resolves #5

Overview

This PR implements the complete Codeforces Sync Engine and Validation Matrix as defined in Stage 2. It integrates perfectly with the Base Infrastructure and Redis Pub/Sub layers created in Stage 1 to provide a robust, rate-limited, and fault-tolerant submission validation backend for the Elite contests.

Key Implementations 🚀

  • Codeforces API Adapter (cf-api.ts): Built a robust HTTP fetcher with keep-alive networking, prefetching support, and response parsing.
  • Background Worker Engine (cfSyncWorker.ts): Set up a dedicated BullMQ worker (cf_sync_queue) that processes incoming submission checks asynchronously off the main Node.js event loop.
  • Validation Matrix Engine: Strictly evaluates Codeforces submissions based on:
    • Exact matching of Author Handle and Problem ID.
    • Precise Timestamp boundaries (startTime <= subTimestamp <= endTime).
    • Graceful Fallback Logic: Properly prioritizes valid OK (AC) verdicts, but elegantly falls back to reporting WRONG_ANSWER or TIME_LIMIT_EXCEEDED if the user attempted the problem during the contest but failed (correctly ignoring ACs they got outside the contest window).
  • Resilience & Rate Limiting:
    • Implemented Redis atomic operations (set NX EX) to strictly enforce a 60-second cooldown per user/room sync, returning HTTP 429.
    • Circuit Breaker Pattern: If Codeforces APIs rate limit us or go down, the BullMQ worker catches the failures, initiates an exponential backoff (1s, 2s, 4s), and eventually gracefully emits a cf_unavailable error instead of crashing.
  • Real-time SSE Fan-out: Integrated directly with the SSE routing to push sync.queued, sync.detected, and sync.failed events to individual user streams via Redis Pub/Sub.

Testing & Verification ✅

  • Automated E2E Testing: Verified the BullMQ queues, Codeforces fetching, and Redis publishUser events end-to-end using integration scripts.
  • Matrix Edge Cases: Simulated invalid timestamps, fake problem IDs, and shadowing AC/WA behaviors—the Matrix evaluated all correctly.
  • Fault-Tolerance: Successfully simulated Codeforces downtime to trigger the Circuit Breaker back-offs.
  • Build Passing: TypeScript checks passed with zero errors (npm run build fully verified).

Note for Handoff

@RsbhThakur RsbhThakur linked an issue Jun 21, 2026 that may be closed by this pull request
9 tasks
@ronits2407

Copy link
Copy Markdown
Contributor

Stage 3: The Room Engine (Blitz Format)

Overview

Stage 3 implemented the core real-time game loop for the Blitz contest format. It seamlessly connects MongoDB for persistent storage, Redis for high-speed ephemeral state tracking, BullMQ for background tasks, and Server-Sent Events (SSE) for real-time frontend updates.

Key Components Implemented

1. Room Creation (src/app/api/contests/rooms/route.ts)

  • Problem Selection Engine: Fetches a user's globally solvedProblems array and performs a MongoDB $nin aggregate query to ensure players are never assigned Codeforces problems they have previously solved.
  • State Scaffolding: Writes the foundational documents to MongoDB (ContestRoom, ContestProblemSet, ContestTeam) and initializes the high-speed Redis Hash room:<id>:state.

2. SSE Gateway Sync (src/app/api/events/route.ts)

  • Zero-Downtime Reconnects: Overhauled the SSE stream initialization so that the moment a client connects or reconnects, the gateway queries Redis and immediately fires a room.state_sync event. This guarantees clients always have the latest scores and problem states without polling.

3. Readiness Handshake (src/app/api/contests/rooms/[id]/ready/route.ts)

  • Participant Validation: Ensures only authorized members of a room can signal readiness.
  • State Machine Transition: Tracks readiness using Redis sets. Once all players are ready, the room state seamlessly transitions to active, the start time is stamped, the first problem is revealed, and a room_timeout job is thrown into the BullMQ reconciliation queue.

4. Blitz Advancement Cycle (src/lib/workers/cfSyncWorker.ts)

  • Real-Time Submissions Hook: Hooks directly into the Stage 2 Codeforces validation matrix. When a valid "AC" (Accepted) submission is detected from the CF API, it triggers the internal state machine.
  • Score Calculation: Calculates the solve-time latency and dynamically awards points using Redis ZINCRBY.
  • Event Fanout: Emits room.score and room.advance to all connected clients, shifting the currentProblem index forward automatically.

5. Match Reconciliation (src/lib/workers/reconciliationWorker.ts)

  • Tie-Breaker Logic: Processes ended rooms via a background BullMQ job. Compares final scores and uses aggregate solve-time latencies (from room:<id>:solve_times) to decisively break ties.
  • Data Persistence: Writes the ephemeral Redis match records back into durable MongoDB ContestSubmission documents and gracefully deletes the temporary room:<id>:* Redis keys to save memory.

6. Auto-Forfeit Integration (src/lib/presenceListener.ts)

  • TTL Inactivity Timeout: Connects to the Redis keyspace listener from Stage 1. If a user disconnects and fails to reconnect within the 90-second TTL grace period, the listener trips the forfeit flag and immediately executes a room_forfeit job via the reconciliation worker.

Testing & Verification Performed

A complete internal verification script (scripts/test-stage3.ts) was authored and executed to validate the database layers and state machine transitions.

  • [Pass] Database Setup & Teardown: Successfully initialized test documents in MongoDB and seamlessly cleaned them up.
  • [Pass] Room Creation (Problem Pool Selection): Tested POST /api/contests/rooms. The system accurately filtered past solved problems and populated the Redis room:<id>:problems list and state hashes.
  • [Pass] Readiness Transition: Successfully pinged the ready endpoint with localized dev headers (x-test-user-id). The engine gracefully handled the first readiness flag, and upon the second player's readiness ping, flipped the room status to active, stamped the start time startTime, generated readyCount: '2', and revealed the first problem.
  • [Pass] Strict Typing & Compilation: Ensured compatibility across the latest Node Redis v4 API requirements (correctly formatting object structures for xAdd, dropping parseInt() around natively numerical zScore responses, and enforcing strictly-typed spread arguments across rPush and sAdd). The Next.js 16 environment build tsc checks passed flawlessly.

@ronits2407 ronits2407 linked an issue Jun 21, 2026 that may be closed by this pull request
10 tasks
@DefineAditya

Copy link
Copy Markdown
Contributor

Stage 4: Arena Format (Simultaneous Reveal & Atomic Claims)

Overview

Stage 4 introduces the Arena contest format. Unlike Blitz where problems are revealed sequentially, Arena reveals all problems simultaneously. It allows participants to independently choose which problem to tackle. To prevent race conditions when two players attempt to solve the same problem at the exact same time, a Lua-scripted atomic claim mechanism was integrated directly into Redis.

Key Components Implemented

1. Simultaneous Reveal & Initialization (src/app/api/contests/rooms/[id]/ready/route.ts)

  • Mode Branching: When a room transitions to active, the engine explicitly checks state.type. If the mode is "arena", it iterates through all problems in the room:<id>:problems list and sets their revealedAt timestamps simultaneously.
  • Decoupled State: Arena rooms explicitly skip setting the currentProblem index inside the room:<id>:state Hash, strictly separating the tracking mechanisms between Blitz and Arena.

2. Atomic Lua Arbitration Script (src/lib/redis.ts)

  • Mathematical Fairness: Created a raw Lua script wrapper claimProblem(redis, locksKey, problemId, teamId, cfTimestamp) executed via redis.eval().
  • Race Condition Mitigation: Because Lua executes sequentially within Redis, the script atomically checks if a lock exists in the room:<id>:locks Hash.
  • Reclaim Logic: If another worker successfully acquired the lock a millisecond earlier, but the new incoming request has a strictly smaller Codeforces timestamp (cfTimestamp), the Lua script brutally reclaims the lock and returns reclaimed|<oldTeamId>. This gracefully handles network/worker latency by guaranteeing the historically earlier timestamp always wins.

3. Arena End Conditions & Score Management (src/lib/workers/cfSyncWorker.ts)

  • Architectural Isolation: Wrapped all Arena-specific logic inside an if (state.type === 'arena') branch, perfectly insulating the existing Blitz logic from any side-effects.
  • Score Adjustments: Dynamically deducts points from an opponent if the Lua script triggers a reclaim, ensuring the total scores always accurately reflect the true locks.
  • Organic Completion (All Locked): Following every successful AC, the worker checks the total number of held locks. Once Object.keys(locks).length === problems.length, the worker skips the remaining time limit, flips the room to completed, broadcasts room.end, and forcefully triggers the reconciliationQueue.

4. Robust Time Limits (src/lib/workers/reconciliationWorker.ts)

  • Timeout Handling: Upgraded the reconciliationWorker to properly handle partial completions (where the time limit expires before all Arena problems are claimed).
  • Fixed Latent Blitz Bug: Discovered and fixed a bug where timeouts would terminate the database room silently without kicking players out. The worker now calculates final scores from the active Redis ZSET and explicitly broadcasts the room.end SSE payload to properly shut down both Arena and Blitz rooms upon timeout.

Testing & Verification Performed

Authored dedicated test suites completely detached from Docker/Live Redis using redis-memory-server:

  • [Pass] High-Concurrency Race Conditions (scripts/test-arena-claims.ts):
    Simulated 10 concurrent async workers attempting to claim the exact same problem with heavily jittered network delays. The Lua script flawlessly arbitrated the claims, executing multiple "reclaims" and successfully awarding the final lock to the worker with the lowest Codeforces timestamp.
  • [Pass] Early End Condition (scripts/test-arena-end-conditions.ts):
    Simulated an active Redis state transitioning from partially locked to fully locked, asserting the cfSyncWorker explicitly bypassed the remaining time limit to wrap up the database.
  • [Pass] Timeout Condition (scripts/test-arena-end-conditions.ts):
    Triggered a mock timeout inside the reconciliationWorker, verifying that the script successfully gathered partial points and correctly emitted room.end to notify the UI of the final leaderboard.
  • [Pass] Regression Check:
    Confirmed state.type fallback and isolated code blocks. Verified that Blitz mode initialization and currentProblem iteration remain completely unbroken.

@RsbhThakur RsbhThakur linked an issue Jun 22, 2026 that may be closed by this pull request
8 tasks
@Priyangshu-Mandal

Copy link
Copy Markdown

Stage 5: Team Multi-User Logic (1v1 & 3v3 Support)

Overview

Stage 5 introduces team-based multiplayer support for contest rooms. The architecture now supports both individual (1v1) and team (3v3) matches through a unified team abstraction layer. Teams coordinate via Redis Sets to ensure all members are ready before the room starts, and any team member's submission increments the shared team score. The implementation maintains complete backward compatibility with existing 1v1 functionality—internally, 1v1 matches are treated as 1-member teams.

Key Components Implemented

1. Team Size Validation & Flexible Configuration (route.ts)

  • Strict Enumeration: Teams must have exactly 1 or 3 members. No other sizes are allowed (enforced at schema validation layer via enum: [1, 3] in ContestTeam model).
  • Consistency Requirement: All teams in a room must have identical sizes—mixing 1v3 or 2v3 configurations are rejected with 400 Bad Request, guaranteeing fair match structure.
  • Dynamic Team Initialization: Room creation dynamically maps all teams into Redis structures (team:<teamId>:users Sets) regardless of team count, enabling future support for 4+ team tournaments without code changes.

2. Team-Based Ready Check Coordination (route.ts)

  • Granular Readiness Tracking: Instead of counting individual ready users, the system now checks team-level readiness. When a user POSTs /ready, the endpoint:
    1. Resolves which team the user belongs to via Redis team:<teamId>:users Set membership lookup
    2. Gathers all team members and checks if all have posted ready
    3. Marks the team as ready in room:<id>:teams_ready Set only when all members are ready
    4. Checks if all teams are now ready; only then transitions room to active
  • 60-Second Team Withdrawal: If a team doesn't fully ready within 60 seconds, a BullMQ team_ready_timeout job is enqueued. The reconciliation worker withdraws the incomplete team, removes it from the room, broadcasts team.withdrawn event, and ends the room if only 0 or 1 teams remain (no valid match).
  • Idempotent Operations: Posting ready multiple times (same user, same room) is safely idempotent—the Set prevents duplicates, and team-ready status is re-evaluated without side effects.

3. Team-Aware Score Aggregation (route.ts + cfSyncWorker.ts)

  • Implicit Team Resolution: The sync endpoint accepts an optional teamId parameter. If omitted, it automatically resolves which team contains the submitting user by iterating through team:<teamId>:users Sets, eliminating the need for clients to track team IDs.
  • Atomic Team Score Increment: Any team member's accepted submission increments the shared team score in room:<id>:scores SortedSet via Redis ZINCRBY (atomic operation). Individual attribution is preserved—each ContestSubmission document stores both userId (who solved it) and teamId (whose score increased).
  • Arena Claim Compatibility: Lua lock format seamlessly extends to teams: lock value is teamId|cfTimestamp. Timestamps are compared per-team, ensuring earlier submissions win claims within each team's context, while Blitz continues sequential problem reveal.

4. MongoDB Schema Extensions (ContestTeam.ts + ContestSubmission.ts)

  • Mandatory teamSize Field: ContestTeam now requires teamSize: { enum: [1, 3] }, documenting team composition at persistence layer and enabling future analytics queries (e.g., "fetch all 3v3 matches").
  • Submission Team Attribution: ContestSubmission.teamId field added (optional for backward compatibility with pre-team submissions). Stores which team earned the points, enabling team-level leaderboards and replay analysis.
  • Tournament Readiness: Optional roundId and contestId fields on ContestTeam are reserved for future tournament bracket integration without schema breaking changes.

5. Robust Error Handling & Authorization

  • Team Membership Validation: CF sync worker explicitly verifies userId ∈ team:<teamId>:users before processing submissions. Returns 403 "User is not part of any team" if membership check fails.
  • Ready State Enforcement: Attempting to ready in a room where the user is not a participant returns 403 "User is not part of any team". Prevents unauthorized access.
  • Invalid State Rejection: Prevents ready calls on already-active or completed rooms (400 "Room is not waiting").

Testing & Verification Performed

Executed comprehensive test suites with all 24 critical tests passing (100% pass rate):

  • [Pass] Room Creation (test-suite-1.mjs): 4/4 tests verified 3v3 room creation, 1v1 backward compatibility, and team size validation (rejecting 1v2 and 2-member teams).
  • [Pass] Team Ready Coordination (test-suite-2.mjs): 2/2 tests confirmed all team members must ready before room starts, and unauthorized users are rejected (403).
  • [Pass] Submission Sync & Aggregation (test-suite-3.mjs): 2/2 tests validated CF submission queuing and implicit userId→teamId resolution.
  • [Pass] Arena Claims (test-suite-4.mjs): 3/3 tests verified team-based problem locking, timestamp race scenarios, and multiple solves per team.
  • [Pass] MongoDB Persistence (test-suite-5.mjs): 2/2 tests confirmed ContestSubmission and ContestTeam schema structures exist and are accessible.
  • [Pass] 1v1 Regression (test-suite-6.mjs): 3/3 tests explicitly validated backward compatibility—1v1 rooms behave identically to pre-team implementation.
  • [Pass] Edge Cases (test-suite-7.mjs): 4/4 tests verified idempotency, invalid room state rejection, unauthorized access blocking, and graceful 404 handling.
  • [Pass] Performance & Concurrency (test-suite-8.mjs): 4/4 tests demonstrated stable concurrent operations (84ms for 3 syncs, 293ms for 6 ready calls), no race conditions on Redis ZINCRBY, no memory leaks.

Bug Fixes & Improvements Applied

✅ Fixed: ContestProblemSet Unique Index Conflict

  • Issue: E11000 duplicate key error prevented creating multiple rooms from the same contest.
  • Root Cause: contestId field had unique: true, but teams reuse problems from same contest.
  • Solution: Removed unique constraint; added roomId sparse index for per-room problem isolation.

✅ Fixed: BullMQ Invalid Job ID Format

  • Issue: Job IDs containing colons (e.g., timeout:roomId) violated BullMQ constraints.
  • Solution: Changed to hyphen format: timeout-roomId, ready-timeout-roomId-teamId.

✅ Fixed: Hardcoded Team Array Indices

  • Issue: Code assumed exactly 2 teams via [teams[0], teams[1]], breaking with 3+ team tournaments.
  • Solution: Changed to dynamic mapping: teams.map(t => t._id.toString()).

✅ Fixed: Inflexible Room Validation

  • Issue: Endpoint only accepted teams.length === 2, preventing future multi-team support.
  • Solution: Changed to teams.length >= 2, enabling scalability.

Backward Compatibility ✅

  • 1v1 rooms continue to work identically (internally using 1-member teams)
  • Existing API contracts preserved
  • MongoDB schema extensions are additive (optional fields)
  • Zero breaking changes

…ion wizard (Stage 6A)

- Update Mongoose models (CustomContest, ContestPreset, ContestRoom, ContestRound, ContestStanding) to support bracket formats, description, team size constraints, and status states (draft, registration, active, completed).
- Create administrative APIs for contest presets (/api/contests/presets) and specific preset interactions including retrieval, updates, and archival.
- Build premium dark-themed preset management view (/admin/contests/presets) with functional modals for CRUD and archiving.
- Implement multi-step Tournament Creation Wizard (/admin/contests/new) with step validation server actions and a live progress tracker.
- Design Solo/Team registration API endpoint (/api/contests/[id]/register) that performs validation checks and triggers background solved history prefetching.
- Create status transition API endpoint (/api/contests/[id]/status) to manage the tournament state chain and emit updates via SSE.
- Setup a prefetch worker job handler in cfSyncWorker.ts to resolve Codeforces solved lists in the background.
- Integrate preset management and tournament wizard cards directly onto the Admin dashboard home page (/admin).
- Add automated verification test script (scripts/test-stage6a.ts) covering the test plan gates.
- Resolve Next.js 15+ dynamic route signature compatibility and parameter types.
@Ayan-Bain

Ayan-Bain commented Jun 24, 2026

Copy link
Copy Markdown

Stage 6A Implementation

Resolves #9


1. Schema & Model Enhancements

  • CustomContest.ts:
    • Configured format to support 'bracket' knockout structures.
    • Restricted the status enum exclusively to ["draft", "registration", "active", "completed"].
    • Relaxed timing constraints by making startTime, endTime, and durationSeconds optional.
    • Added new fields: description, teamSize (1 or 3), registrations subdocument array, registrationSettings (deadline, type, max participants), and bracketSettings (third-place playoff toggle, seeding method).
    • Configured indexes for { format: 1, status: 1 }.
  • ContestPreset.ts:
    • Added archived: Boolean field (defaulting to false) to filter active templates in the creator interface.
  • ContestRoom.ts:
    • Added bracketPosition: String (nullable) to record tree positions (e.g. '3PO' for third-place playoff) without impacting standalone match query filters.
  • ContestRound.ts:
    • Added bracketLevel: String (e.g., 'semifinal') to tag match round segments.
  • ContestStanding.ts:
    • Added aggregate tournament performance variables: wins, losses, draws, and eliminated (defaulting to 0/false).

2. API Endpoints

  • src/app/api/contests/presets/route.ts:
    • GET: Returns active presets. Supports ?includeArchived=true query.
    • POST (Admin only): Inserts new presets. Enforces unique names.
  • src/app/api/contests/presets/[id]/route.ts:
    • GET: Retrieves preset by ID.
    • PUT (Admin only): Modifies configuration values.
    • PATCH (Admin only): Archives or restores presets.
  • src/app/api/contests/[id]/register/route.ts:
    • POST: Enrolls user/team for tournament registration.
    • Handles solo (1v1) and team (3v3) registration flows.
    • Verifies registration deadlines, player verified CF handle requirements, and enqueues solved_prefetch BullMQ worker tasks.
  • src/app/api/contests/[id]/status/route.ts:
    • PATCH (Admin only): Updates status values sequentially through the chain: draft -> registration -> active -> completed.
    • Emits updates via Server-Sent Events (SSE).

3. UI Component Additions

  • Administration Landing (/admin):
    • Added navigation cards directing admins directly to Contest Presets and Create Tournament.
  • Contest Presets Dashboard (/admin/contests/presets):
    • Premium table listing active and archived presets.
    • Modals supporting creation, editing, and toggle archiving operations.
  • Tournament Creation Wizard (/admin/contests/new):
    • Step-by-step progress tracking circles connected by solid, center-aligned lines.
    • Form validation on steps (Basic Info, Registration, Preset Selection, Bracket/Seeding Settings, Preview).
    • Connects to backend server actions to validate and commit tournament documents.

4. Background Workers & Verification

  • cfSyncWorker.ts:
    • Added a handler block for solved_prefetch jobs, calling the Codeforces integration API to cache user solve histories in Redis (solved:<cfHandle>) upon registration.
  • scripts/test-stage6a.ts:
    • Created an automated test suite matching the specifications of the Stage 6A test plan.

- Bracket generation (generateBracket) with seeding, bye handling, round/room creation
- Bracket advancement (advanceWinner) with auto-seeding to next round
- Round completion detection (checkRoundCompletion) with SETNX race lock
- Manual walkover (processWalkover) with audit trail
- Bracket snapshot (getBracketSnapshot) for SSE broadcast
- API routes: POST /api/contests/:id/bracket/generate, POST /api/contests/rooms/:id/walkover
- Reconciliation worker bracket advancement hook
- ContestRoom 'pending' status for empty bracket room slots
- CustomContest winnerName field for display
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment