8.2 KiB
Community tournaments
Signed-in users with active accounts can create a tournament at /tournaments and share its /tournaments/{id} page. The organizer chooses a published map with at least five distinct panorama locations, a difficulty, a scheduled start, a player cap from 2 to 32, and 1 to 10 tournament rounds. Every tournament round contains exactly five mini-rounds.
The organizer selects either a shared time budget across the five mini-rounds (the default), or a separate timer for each mini-round. Shared budgets accept 15–3000 seconds; per-mini-round timers accept 15–600 seconds. Both use 15-second increments. The shared clock starts when the first mini-round becomes playable, excludes initial loading, and keeps running through later loading and result screens. When it expires, all unplayed mini-rounds close with zero points. Difficulty settings are saved with the tournament, so later preset changes do not alter a running competition.
The schedule form displays the application server's timezone. Its local date/time input is interpreted in that zone, even when the organizer's browser uses another zone. The server validates future starts within 90 days; browser clocks do not decide when gameplay begins. Discord announcements include the server date/time and zone, plus Discord timestamps displayed in each reader's local time.
The organizer joins automatically. Registration and departures close at the scheduled start; the roster is then locked. At least two active registered players are required to start, otherwise the tournament is cancelled. An organizer can cancel their scheduled or running tournament. Scheduled settings can be changed without removing already registered players through a lower cap. Owners cannot leave their own event; cancellation is available instead.
All players receive the same locations within a mini-round. The server decides deadlines, validates guesses, calculates scores, and reveals results only for closed mini-rounds. Tournament pages show final standings at completion. Inactive accounts cannot keep playing or win. Tournament membership credentials remain on the server and ordinary multiplayer joins cannot bypass the tournament roster.
The latest eligible tournament winner is featured in first place on the global, all-difficulties leaderboard for seven days after completion. This does not alter their earned score or rewrite difficulty-specific rankings. A newer eligible winner takes over the featured position; expiry restores the ordinary ordering.
Discord announcements
An announcement is queued only when the organizer enables the Discord setting. Delivery becomes due 15 minutes before the scheduled start. If the setting is enabled or an event is created within that window, the next dispatcher run can announce it while registration remains open. Started, cancelled, expired, or opted-out events are not announced. Rescheduling an unsent event moves its announcement time. Each tournament has one durable announcement record; editing an already announced event does not ping the channel again.
Tournament posts use the existing server-only DISCORD_BOT_TOKEN. The destination is fixed to news channel 1558048830770122772; no additional channel environment variable is needed. Only roles 1558053709554909205 and 1558052962817933322 are allowed to receive mention notifications. User names and tournament titles are escaped, and users, @everyone, and @here cannot add notifications.
The bot needs access to the destination channel with Send Messages and Embed Links. The selected roles must be mentionable, or the bot must have the relevant role-mention permission. Bot credentials must stay in server environment configuration and must never use a NEXT_PUBLIC_ variable.
Delivery uses two-minute database leases, a stable Discord nonce, bounded request timeouts, and up to eight attempts with backoff. Rate limits and temporary failures return work to the queue. Permanent configuration errors stop the attempt and save a safe error message. Discord deduplicates matching nonces only within its recent-message window; an unusually delayed ambiguous network response can still prevent an absolute exactly-once guarantee.
Runtime and deployment
Apply the checked-in migrations before deploying the application code, in filename order. The relevant additions are:
supabase/migrations/20261018075331_extended_difficulty_rules.sqlsupabase/migrations/20261018075348_tournaments.sqlsupabase/migrations/20261019044542_shared_tournament_deadline.sql
Use the project's normal controlled database migration process. If applying these through the Supabase SQL editor, run each complete file once, in that order, after the earlier project migrations. These files are schema changes, not application .env settings. Do not clear production data to repair a missing table. If PostgREST reports a stale schema cache after a successful migration, reload its schema cache with NOTIFY pgrst, 'reload schema';.
The migration creates the tournament tables, membership and private-room guards, the durable announcement outbox, and server-only RPCs. RLS is enabled and anonymous/authenticated direct access is revoked; the application uses its server-side Supabase secret to perform authorized actions. Keep the application and worker on the same schema version.
npm start starts the web application and companion worker. The worker calls tick_tournaments and drains due tournament announcements every 30 seconds, independently of panorama rendering. Keep that worker running: scheduling must continue even when nobody has the site open. When hosting only the web application, arrange a scheduled authenticated GET to /api/integrations/discord/dispatch at least once per minute with Authorization: Bearer <CRON_SECRET>. The endpoint rejects requests without the matching server secret and also advances tournaments before draining announcements.
A missing migration or database connection pauses scheduling; the worker retries and limits its warning to once every five minutes. Repair the schema/connectivity rather than relying on visitors to advance events. Failed Discord deliveries are stored in tournament_announcements; fix the bot configuration before retrying. Rescheduling or explicitly changing an unsent announcement setting can requeue it according to the migration's retry rules.
Verification
npm test includes tournament schema-validation, API, timer, and announcement tests. npm run typecheck -- --incremental false checks the server/client contracts. node scripts/test-tournaments-sql.mjs runs SQL integration tests against an isolated in-memory PostgreSQL-compatible fixture and requires the local test dependency at .data/sql-verification/node_modules/@electric-sql/pglite.
The SQL suite covers the 32-player limit, locked owner actions, registration, shared game engine, standings, announcement leases, and restricted direct database access. Announcement unit tests use fake Discord clients; they do not publish messages. Apply production migrations and verify a real opted-in event deliberately after deployment; no external test announcement is required during local development.
1.0.4.1 bug fix — October 19, 2026
Shared budgets above 600 seconds previously inherited the ordinary engine's shorter mini-round deadline. The shared-deadline migration makes each active mini-round use the persisted tournament-round deadline, including after transitions. Polling does not extend that deadline. Separate mini-round timers retain their existing behavior.
The migration was applied to the configured MineGeo production database. A production query confirmed the corrected function, invoker security, and server-only execution permissions. Regression coverage fails against the original function and passes with the correction. The complete tournament SQL suite, 179 unit tests, TypeScript check, and production HTTP/API/Storage smoke test passed; the smoke test completed and cleaned up a temporary five-round guest game. No application code changed, so this fix requires no web deployment or restart.
The Discord bug-fix update was posted with only the two requested role mentions. It contains no event announcement.