12 KiB
Vash Esports
Automated osu! matches and tournaments. Players queue or sign up, and Vash hosts the osu! lobby over IRC: it invites players, runs the roll, bans, picks, and tiebreakers, starts maps, reads the results, updates ratings, and closes the lobby. Referees can step in at any point, but nothing waits on them.
Features
- Live match dashboard. A second-screen page with a live scoreboard, the map pool board (bans, picks, winners), turn prompts that pop up with a countdown, live scores while a map plays, a result reveal after every map, the lobby chat, and a timeline. Alt-tabbed players get a chime, a blinking tab, and an optional browser notification when it is their turn.
- Configurable match procedure. Best of any length, bans per team (also split into rounds, alternate or snake order), pick-or-ban choice for the roll winner, tiebreaker selection (ban down, random, re-roll, opening roll), freemod score multipliers, join windows, action timeouts with auto-roll / random pick / forfeited or random ban, no-show concede or cancel, early-disconnect restarts, and ties for even series.
- Referee controls. Pause and resume (the timer keeps its remaining time), force start, abort and replay or return the map to the pool, undo a pick, skip or extend a timer, act for a team, correct a map winner (ratings follow), forfeit, and cancel.
- Spectating. Anyone can watch any match live, signed in or not. The Live page shows every match being played, updating in place, with what is coming up and recent results.
- Predictions and pick'ems. Spectators call the winner (10 points) and the exact score (5 more) until the first map starts, and see the crowd split and the rating odds. Tournaments get a pick'em page per bracket with a leaderboard, and the site leaderboard has a Predictors tab.
- OpenSkill ratings. Every rating has an uncertainty. New players settle within a few matches, and in team matches each player moves by how much the system knows about them. Corrections undo a match's rating changes exactly.
- Stream overlays and an auto-director. Broadcast channels follow one match, a tournament, or whatever is most exciting, and render a customizable 1920×1080 overlay made of drag-and-drop components. A Windows agent adds live scores from the osu! tourney client through tosu and drives OBS. A container streams a channel to Twitch, YouTube, Facebook, X, Instagram and TikTok at once (or relays OBS), records every broadcast to storage, and plays it live and afterwards on the site with chapters per match. See streamer/README.md.
- Quick queue for 1v1 to 4v4, matched by rating, with a pool picked by rating.
- Tournaments with registration, qualifiers, groups, single and double elimination, scheduling, and full branding: banner, icon, accent color, the osu! forum post (BBCode), prizes, links, and a player showcase.
- Test lab in dev and staging: play or watch full matches against simulated players, in a simulated lobby or a real osu! lobby over IRC with 0, 1 or 2 real sides (bots play inside Vash and never appear in osu!).
- Several osu! accounts host lobbies together; capacity is four lobbies per normal account.
- Discord result posts and stream announcements.
How a match runs
web actions ─┐
IRC events ──┼─▶ match actor ─▶ reduce(state, action) ─▶ commit to Postgres ─▶ publish snapshot (websocket)
deadlines ───┘ (one per live match, pure, one transaction, └▶ lobby commands
actions run in order) no I/O revision-checked (!mp map, chat, …)
-
src/lib/server/osu/procedure.tsis the whole match procedure as a pure reducer: state + action → new state + lobby effects. Timers are deadlines in the state, so a restart resumes the same decision. It is exhaustively unit tested. -
src/lib/server/match/runtime.tskeeps one actor per live match. It turns BanchoBot lines, web requests, and deadlines into actions, commits, publishes, and runs the effects. When IRC misses the finish line, it recovers the result from the osu! API. -
src/lib/server/osu/irc.tsis real Bancho IRC, one connection per account, on a small client over Bun's TCP sockets (irc-client.ts: login, pings, rate limits, reconnect and rejoin).src/lib/server/osu/simulator.tsimplements the same lobby interface in-process, with BanchoBot's exact wording, so the same parser and runtime are exercised in tests and dev.fake-bancho.tsputs an IRC server in front of the simulator, so tests also play whole matches over real sockets, with split packets and dropped connections. -
src/lib/server/match/store.tsloads a match in a handful of queries and commits each change in one transaction, with a revision check. -
src/lib/match-view.tsis the snapshot the match page receives on load and on every change. The page never refetches. -
src/lib/server/broadcast.tsruns broadcast channels: the director (src/lib/broadcast/director.ts) picks each channel's match, and overlays get one payload per change on thechannel:<id>topic. Streamer agents connect to/ws/agent.
The server leans on Bun instead of packages: Bun.SQL under Drizzle, Bun.S3Client for R2, Bun TCP sockets for IRC, and bun build --compile for the streamer agent. One websocket endpoint (/ws) carries every live topic: match:<id>, live, and channel:<id>.
Long-lived state (IRC connections, the simulator, actors, timers) lives in src/lib/server/singleton.ts and src/lib/server/services.ts. A Vite reload replaces services and resumes match actors. Restart bun run dev after changing the IRC client or provider: those connection objects survive HMR and keep their original methods until a process restart.
For a streaming PC that connects to the existing site, follow the Windows + WSL setup.
Getting started
Prerequisites: Bun 1.3+, PostgreSQL 16+.
git clone https://git.stanrunge.dev/stan/esports.git
cd esports
cp .env.example .env # fill in DB_URL, BETTER_AUTH_SECRET, OSU_CLIENT_ID/SECRET at least
bun install
bun run db:migrate # apply scripts/migrations in order
bun run db:seed # dev only: seeded players, teams, pools, a branded tournament
bun run dev # http://127.0.0.1:3102
On an empty database, create the base tables first with bun run db:push, then run the migrations.
Testing without osu!
With BANCHO_MODE=simulator (the default outside production):
- Open the dashboard. The Simulation lab card starts a 1v1 against a bot, or a bot-vs-bot match from 1v1 to 4v4.
- On the match page, Sim lab controls map speed, network chaos (split lines, delayed scores, a dropped finish line), which teams the bots play for, and each player: join, leave or disconnect, ready,
!roll, next score, or type anything in the lobby (!pick NM1,!ban HD2,!choose ban). - Play as copies a 10-minute link that signs a private window in as a seeded player, so you can play both sides yourself. From the terminal:
bun scripts/dev-login.ts dev-player-03.
Automated tests run the reducer, the parser, the simulator, whole matches through the runtime with fake timers, and whole matches over IRC against the fake Bancho:
bun run test:unit --run
Scripts
| Command | Description |
|---|---|
bun run dev |
Dev server with HMR on 127.0.0.1:3102 |
bun run build |
Production build (SENTRY_AUTH_TOKEN= to skip uploads) |
bun run start |
Production server with the match websocket |
bun run check |
Type-check with svelte-check |
bun run test:unit |
Unit tests (Vitest) |
bun run test:streamer |
Streamer agent tests (bun test) |
bun run streamer |
Run the streamer agent from source |
bun run streamer:build |
Build dist/vash-streamer(.exe) for Linux and Windows |
bun run db:migrate |
Apply pending SQL migrations |
bun run db:migrate:dev |
Back up vash_dev, then migrate it |
bun run db:seed |
Seed the dev database |
bun run db:studio |
Drizzle Studio |
Database changes
Schema lives in src/lib/server/db/schema.ts. Every change also gets an idempotent SQL file in scripts/migrations/ (0005-…sql, and so on). bun scripts/migrate.ts applies the pending files in order, in one transaction, and records them in schema_migration; --status lists them. The staging and production deploys take a verified pg_dump and then run it. Migrations only add: they never drop or rewrite existing users, teams, pools, or match history.
Environments
| dev | staging | prod | |
|---|---|---|---|
| URL | esports-dev.vash.software | esports-staging.vash.software | esports.vash.software |
| Runs as | bun run dev in tmux, :3102 |
Docker, /opt/esports-staging, :3101 | Docker, /opt/esports, :3100 |
| Database | vash_dev | vash_staging | vash |
| Lobbies | simulator | simulator (set BANCHO_MODE=irc to use real IRC) |
osu! IRC |
| Title and badge | (DEV) |
(STAGING) |
none |
The environment comes from APP_ENV, or from ORIGIN when that is unset. Dev and staging sit behind Cloudflare Access and are marked noindex.
Deployment
Work on staging and push. Gitea runs the unit tests, builds git.stanrunge.dev/stan/esports:<sha>, backs up the staging database, migrates it, and starts the image at /opt/esports-staging. After checking staging, run the Deploy production action: it backs up and migrates the production database and starts the same image at /opt/esports.
Deploys refuse to switch images while /api/health reports live matches, unless FORCE_DEPLOY=1. Before a deploy, use Drain before restart on /admin: no new lobbies open, the queue pauses, and live matches play to the end. After a restart, the runtime rejoins the lobbies it was hosting and continues; a lobby it cannot find pauses its match for a referee.
Environment files are /etc/esports-staging/staging.env and /etc/esports/prod.env.
Discord
The bot ("Vash Esports", CesarBot) only posts: results to DISCORD_MATCH_CHANNEL_ID with no pings, and stream announcements to DISCORD_STREAM_CHANNEL_ID. /admin shows whether it is connected, whether it can post in each channel, an Add bot to server link, and a Send test message button. Several environments can share the token; give dev and staging a private channel.
Contributing
Conventional commits are enforced by git hooks (feat:, fix:, chore:, refactor:, docs:). Pre-commit runs Prettier and ESLint on staged files; pre-push runs the type check and unit tests.
License
All rights reserved. This source code is publicly available for reference and educational purposes. See the project maintainers for contribution or usage inquiries.