我為 2026 年世界盃打造了一款足球預測遊戲。歷時一個月、104 場比賽,
最後產生一位冠軍。它運作良好,人們也玩了,沒有發生任何問題。

後來我把同一個應用指向國內聯賽:法甲,306 場比賽、34 個比賽日,
歷時九個月。同樣的資料模型、同樣的計分、同樣的模板。沒有崩潰,也沒有拋出例外。每個測試仍然是綠燈。

而應用程式內幾乎每一項產品決策瞬間都變成了錯誤。

以下是四個關鍵問題,因為它們都不是技術問題,也不會出現在測試套件裡。

1. 累積排行榜在十月就已底定

在賽事中,總積分榜就是整個遊戲。你玩四週,最終的排行榜就是故事。

在 34 個比賽日中,這個排行榜大約在第八週就不再是遊戲。開局表現良好的玩家已領先 60 分,十一月才加入的玩家在數學上已出局,其他人看到的則是一塊無法改變的計分板。產品本身仍然正常運作,只是已經沒有懸念。

解決方案不是更好的演算法,而是新增另一個時間單位:單一比賽日排行榜,讓每個週末都有自己的冠軍,另設季後榮譽榜計算每位玩家贏得多少個比賽日。積分與計分方式相同,只是切片方式不同。總排名第 14 的玩家仍有可能贏得本週末的比賽日,這才是讓他們週五回來的動力。

值得一提的是我「沒有」做的事:沒有重置、沒有讓步、沒有追分獎勵。在玩家正在使用的計分系統上做任何追溯性調整,都會摧毀對排行榜的信任,而排行榜正是整個資產。

2. 滑動 24 小時提醒變成 100 封電子郵件

提醒工作原本是為賽事設計:「如果玩家有未預測、且將在未來 24 小時內開踢的比賽,就寄信通知。」比賽每天零星進行,因此每天只會收到一封郵件,這樣是沒問題的。

法甲的一個比賽日從週五 20:45 持續到週日 20:45。同樣的工作若不修改,每週末就會寄出三封郵件給同一位玩家:週五一場、週六三場、週日五場。一季下來每位玩家約收到一百封郵件。那不是提醒,而是帶有額外步驟的垃圾郵件投訴。

這些郵件還會在第一場比賽當天早上送達,而聯賽的自然行為正好相反:你會一次把九場比賽全部填完,無論什麼時候想到。

因此該工作在同一個指令中現在有兩條完全不同的程式路徑。賽事仍使用滑動視窗;聯賽則改為「比賽日寄一封郵件」,在該比賽日第一場開踢前 24 至 48 小時觸發,並列出該比賽日所有仍開放的比賽。

我喜歡的部分是:沒有 reminder_sent 資料表。冪等性來自視窗本身。「第一場開踢時間落在 24 至 48 小時前」這個條件在每個行事曆日只會成立一次,而 cron 每天只執行一次。唯一會 legitimately 產生第二封郵件的情況,是延賽把第一場開踢時間又拖回視窗內,而行事曆異動正是你希望再次提醒的時候。

權衡寫在 crontab 的純文字註解裡:把 cron 頻率加倍,就會讓郵件也加倍。一行註解比一張資料表便宜,只要註解放在容易出錯的地方即可。

3. 「即將到來的比賽」不是「所有未來的比賽」

儀表板原本列出所有未來的比賽。在賽事中最多只有幾十張卡片,進度徽章顯示「12/18 已預測」,感覺可以完成。

在聯賽中則是 306 張卡片與「12/306」,感覺像作業。

現在清單限制在下兩個比賽日。微妙的地方在於「如何」挑選它們。我的第一版做法是對未進行比賽取 MIN(round_number)。這在任何真實聯賽中都是錯的,因為延賽很常見:比賽日 3 的一場比賽若在十一月重賽,儀表板就會被鎖定在比賽日 3 和 4,而隱藏真正的週末。視窗現在依據最近的開踢時間,而非回合數字,延賽的比賽會在新時段到來時自動重新出現。

4. 賽事中總有事可做

世界盃的四週是四週持續關注。九個月則不是。在聯賽的大部分時間裡,參與式產品必須與使用者「忘記它存在」競爭。

我針對的兩個時刻,正是應用程式原本已知卻丟棄的時刻:剛贏得比賽日的玩家(每週一位,驕傲高峰),以及剛完成預測下個比賽日的玩家(每季 34 次,每人一次,投入高峰後接著好幾天沒事)。這兩個時刻現在都會提供分享功能,在行動裝置上使用原生分享表單,並有 WhatsApp 備援,且連結到公開賽事頁面而非私人聯賽。

最後這項細節只花了一分鐘思考,卻值得這一分鐘:從行動分享表單分享聯賽邀請碼,意味它可能出現在公開貼文中,而讓陌生人進入私人排行榜並不是一項功能。

真正的教訓

資料模型是正確的。EventGamePrediction,以及原本就存在的 round_number 欄位。這些改動完全不需要任何 migration。

錯誤的是所有關於「節奏」的假設:使用者多久出現一次、一次競爭單位的時間長度、他們能預見多遠的未來、兩次有興趣的時刻之間要等多久。這些假設幾乎不會出現在你的 schema 中。它們散布在 cron 運算式、查詢限制、郵件條件,以及空白狀態文案裡,而這些正是當人們說「我們只需要支援新的賽事格式」時,沒有人會去看的地方。

如果你即將把一個運作中的產品重複用在更長或更短的時間尺度上,請在程式碼庫中搜尋時間相關的關鍵字:每一個 24 hours、每一個 setMaxResults、每一個「next」、「current」與「upcoming」。那才是你真正的差異。

這款應用是一款免費的預測遊戲,供朋友與同事使用,不涉及投注或金錢,使用 Symfony 建置,並透過 Mercure 提供 Turbo Streams 即時排行榜,支援 16 種語系。它位於 pronoarena.com,若想查看成果,法甲賽季將於 8 月 21 日開踢。