尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗You and your friends each get dealt one of the seven clubs that finished top of Serie A in 2025/26. From then on, everything your club does in 2026/27 — Serie A, Champions League, Europa League, Conference League, Coppa Italia and the Supercoppa Italiana — scores you points. Whoever's club has the best season wins.
Next.js App Router, deploys to Vercel as-is.
A player enters a name and the app deals them a club at random from the top seven.
Every club gets a first owner before any club gets a second, so a group of seven or fewer all end up with different teams.
Results are pulled from the football API and turned into points:
Every competition pays 3 for a win and 1 for a draw, plus +1 per goal scored and +2 for a clean sheet.
Scoring is flat across competitions on purpose. A club does not choose which cups it ends up in, so paying more for one than another would just be a second reward for finishing high last season — which the draw already reflects.
A knockout tie settled on penalties counts as a win for whoever went through, not a draw. Postponed and abandoned games are ignored until they are actually played.
Ties on points break by goal difference, then wins, then who joined first.
On top of match results, clubs earn one-off bonuses.
Statistical awards — 5 points each. Top scorer, top assister, most goals + assists, most saves, most clean sheets. These are contested between the clubs in this game, not against all of Europe — the Champions League top scorer is rarely a Serie A player, so a global comparison would leave every award unclaimed. Goals and assists are summed across all six competitions. If two clubs tie, both are paid; if two players at the same club tie, the club is paid once.
Cup progress. Only the furthest round reached is paid, not every round along the way:
| Reached | Points |
|---|---|
| Knockout playoff | 1 |
| Round of 32 | 2 |
| Round of 16 | 3 |
| Quarter-final | 5 |
| Semi-final | 7 |
| Final | 8 |
| Won the trophy | 10 |
In Europe, the top eight of the league phase go straight to the last 16 while ninth to twenty-fourth have to win a playoff to get there. Both end up in the same round, so qualifying directly is worth +3 on its own.
Serie A finish. Champions 12, top four 8, 5th–6th 5, 7th 3. Only the highest applies.
Bonuses that are still contestable show faded on the leaderboard and can be lost again before the season ends. Every number above is an environment variable, so you can rebalance the whole game without touching code — see .env.example.
Anything being played right now sits above the tabs, with a running score and clock — it's the most interesting thing on the page whatever you're reading. Then six tabs, each deep-linkable by URL hash:
| Tab | What's in it |
|---|---|
| Leaderboard | Players, clubs, points split into match and bonus, movement arrows since the last gameweek, and where each club's points came from |
| Serie A | The real league table, colour-coded by qualification and relegation band, with the clubs in play highlighted |
| News | A short report on every match your clubs have played, kept for the season, plus who is in form right now and the current manager |
| Players | Top scorers, assists, goals + assists and saves — among the clubs in this game |
| Fixtures | Recent results with the points they earned, and what's coming up |
| This week | Who gained, who climbed, result of the week, and a summary you can paste into the group chat |
| Rules | Every scoring value, including the bonuses |
Head-to-head countdowns sit at the very top whenever clubs in the game are about to play each other — the fixtures where one player's points come straight out of another's. Every such fixture in the round gets its own countdown and its own write-up, not just the next one.
Each write-up carries the real head-to-head record and last meeting (from ESPN), both clubs' league positions, and their leading scorers. Nothing is claimed that isn't in the data: before a ball is kicked it says so rather than quoting meaningless positions, and a missing record simply goes unmentioned. Named rivalries — the Derby della Madonnina, the Derby d'Italia — add a line of history, and are the only editorial part.
A round is four days by default, so a Friday-to-Monday Serie A weekend announces together.
Change it with DERBY_WINDOW_HOURS.
Any club's view. Click a club anywhere — leaderboard, league table, draw pool or the countdown — and you get its league position and form, a short history, honours, legends, current manager, its best players this season, and every result and remaining fixture across all six competitions, filterable by competition. It is public: anyone can inspect anyone else's club, no login.
On phones the leaderboard becomes cards rather than a ten-column table.
Chat sits in the bottom-left corner of every tab, with an unread count while it is closed. Whoever holds the player cookie posts as themselves and gets a ✓; anyone else picks a name from the list and posts without one. That is deliberate — it is a game between friends, not an account system, so claiming a name is possible and the tick simply says which messages came from that player's own browser. Messages are capped at 500 characters, one every couple of seconds per person, and the last 200 are kept.
Histories, honours, legends and managers are written by hand in src/lib/clubs.ts — no free API serves them. Everything else in the club view is live.
Managers are not hand-written. They come from Wikidata, which records who is in charge
and adds an end date when someone leaves, and are re-checked twice a day. This was not
optional: four of the seven hand-written managers were already wrong within weeks of being
typed. MANAGERS="110:Some Name" still overrides, for when Wikidata is behind the news.
If the draw pool ever changes to a club with no profile written, the view omits that section rather than breaking.
npx vercel
Or push the repo to GitHub and import it at vercel.com/new. The framework preset is Next.js and it is detected automatically.
In your Vercel project: Storage → Create Database → Upstash for Redis, then connect it
to the project. Vercel injects KV_REST_API_URL and KV_REST_API_TOKEN automatically.
Without it the app still runs, but players are held in memory and disappear whenever the serverless function is recycled. Fine for a local look around, useless for a real game.
There is no API key to configure. See .env.example for the optional knobs.
ESPN's public soccer API — no account, no key, no rate limit — via two endpoints:
…/soccer/ita.1/standings?season=2025 for the 2025/26 final table the draw uses.…/soccer/{slug}/scoreboard?dates=20260701-20270629&limit=1000 for results, once per
competition. One request returns an entire season, so a refresh costs six calls total no
matter how many clubs are being tracked.This was chosen because it is the only free source covering all six of these competitions for the current season. API-Football's free tier stops at 2024; football-data.org's free tier has no Coppa Italia, Supercoppa or Conference League.
The trade-off: this endpoint is undocumented and carries no stability guarantee. Every
response is parsed defensively, cached, and served stale rather than erroring if a call
fails, so a hiccup shows slightly old numbers instead of a broken page. If ESPN ever
changes it, GET /api/leagues will show you which of the six slugs stopped resolving, and
SERIE_A_TOP7 lets you pin the draw pool by hand in the meantime.
Caching: standings once a day, scoreboards every three hours, with a stale-while-revalidate layer and a single-flight lock so a burst of visitors is not a burst of upstream requests. A daily Vercel cron warms it all.
Needs Node 22.6+ — the tests run the TypeScript sources directly, with no build step.
npm install
npm run dev
No .env.local is required. Without Redis credentials it falls back to in-memory storage,
which is what you want for local poking.
Want to see a full leaderboard immediately instead of a table that has barely kicked off?
Set TRACK_SEASON=2025 and it scores the completed 2025/26 season instead.
npm test
Node's own test runner, no framework. The suites cover the parts that are easy to get quietly wrong: who a goal belongs to, and whether the generated prose ever contradicts the scoreline it is describing. They assert facts rather than exact sentences, because the wording is generated and pinning it would only test the grammar's current mood.
npm run typecheck
Match reports and head-to-head blurbs are generated, not filled in. src/lib/grammar.ts
is a small context-free grammar engine; src/lib/voice.ts holds the vocabulary — verbs,
evaluations, codas, venue phrases. Nothing in there is a finished sentence, so a report is
assembled per match rather than picked from a list: around 700 distinct openings across
the match shapes, and thousands of distinct whole reports.
Two things keep it sane. Each writer is seeded on the fixture id, so a given match always reads the same way — prose that reshuffled on every page load would read as a bug. And a writer remembers what it has already used, so one report works through a symbol's options before coming back to one, which is what stops three sentences in a row all saying "scored".
Only the wording is generated. Names, minutes, scores and points are passed in as values, and the vocabulary deliberately never claims anything the data does not support — no "deserved it", no "never looked like scoring". Where ESPN's event list covers only some of a match's goals, the report says so rather than quietly contradicting its own scoreline.
The exception is the rivalry notes in src/lib/hype.ts, which are written by hand. A
grammar would happily invent a reason two clubs dislike each other, and that is not the
kind of thing to make up.
Set OPENROUTER_API_KEY and the match reports and a short club-news note are written by
a model instead. Everything else is unchanged, including the facts: the model is given the
scorers, minutes, assists and real published headlines the app already has, and told to
add nothing.
It is a writer, not a source. It is never asked what happened, because a model asked about football will supply a plausible stadium, a plausible scorer and a plausible transfer, none of which a reader can check. Two guards sit behind that: the prompts list the facts and forbid anything else, and output mentioning a name that was not in the input is discarded rather than shown. That check was written against a real failure — one model volunteered the correct stadium nobody had given it, and called the away side "the hosts".
Everything degrades. No key, a failed call, a timeout, a spent budget or ungrounded output, and the locally generated prose is used instead. Reports are cached for months, so a match is written once; a per-request budget means a cold cache fills in over a few visits rather than stalling the first one. Writing a full season for seven clubs costs a few pence.
Transfers, injuries and club stories still come from ESPN's published feed, which is where the headlines on the News tab come from. The model summarises those headlines; it is not asked to recall transfer news of its own.
The same applies to the head-to-head previews and the weekly round-up. A preview is written from the head-to-head record, the live table, the last meeting and the leading scorers, plus the rivalry note where there is one — that note is researched history, so the model may draw on it but not extend it. The round-up is written from the table and what each owner gained. Neither is asked to predict anything.
The pasteable standings block is deliberately left alone. People copy it into a chat, and a format that moved around every week would be worse, not better.
Bottom right, next to the chat. Pick one of the seven clubs and ask it things — how the season is going, who has been scoring, who the manager is, how hard the next run of fixtures looks, what the news is.
It is a real conversation with a model, but every question goes up with that club's actual record, league position, fixtures, leading scorers, manager and published headlines attached, and the instruction to answer from those and say so otherwise. That last part is the point. Asked for a squad list it will tell you it does not have one, rather than reciting the squad from whenever its training data stopped — which is the failure that matters here, because a plausible wrong answer about your own club is indistinguishable from a right one.
Fixture difficulty is each opponent's actual league position and points. There is no difficulty score, because nobody publishes one, and it will not predict results.
Costs are capped here in a way they are not elsewhere, since the input is people rather than data: a length limit on questions, a few seconds' cooldown each, and a bounded history.
| Route | What it does |
|---|---|
GET /api/state | Everything the page renders: pool, leaderboard, results, fixtures |
POST /api/join | { "name": "..." } — deals a club and sets the player cookie |
POST /api/leave | Gives up your club and frees your name |
POST /api/refresh | Forces a re-read of results, rate limited to once a minute |
GET /api/cron | Daily warm-up, wired in vercel.json |
GET /api/club/{teamId} | One club's whole season — every result and fixture. Public |
GET /api/players | Individual stat leaders among the clubs in the game |
GET /api/chat | Recent messages, newest first |
POST /api/chat | { "text": "...", "as": "playerId" } — posts a message |
GET /api/leagues | Health check: confirms all six ESPN slugs still resolve |
Players are identified by an opaque token in an httpOnly cookie — no passwords, no
accounts. Clearing cookies means losing the club, which is why /api/leave exists.