桌面應用程式 · Python + pywebview

沃田Verdi

喝水這件事,用一座菜園記住。

喝一次水換一次澆水的機會,把菜養大可以賺金幣、擴建菜園。 重點是這條規則:今天能澆幾株菜,就看你今天喝了幾次水, 不是看你今天喝夠了沒。所以一次灌 2000ml 在這裡是沒用的。

  • 24 種作物
  • 148 個測試
  • 7,200 行
  • 完全離線
製作
賴子渝 Joe Lai
類型
課程專題
平台
Windows 單機
沃田主畫面:頂端顯示 340 金幣、連續 12 天、今日 1250/3460ml,下方八格菜園裡有成長中與已成熟的作物
實際執行畫面,非設計稿

動機與目標

提醒沒用,因為不理它也不會怎樣

市面上的喝水提醒 App 幾乎都是同一套做法:設個間隔、跳通知、按「已喝水」。 我自己載過幾個,最後都一樣,通知變成隨手滑掉的東西。 因為不理它也不會怎樣,那個按鈕按不按其實沒差。

會痛的提醒才有用。

所以這個專案想做的事很單純:讓「沒喝水」真的有一個看得到、 而且會愈來愈糟的後果。菜園剛好很適合,作物會枯萎, 而且是自己害死的,救不回來。

想做到的事

  • 讓「分次慢慢喝」比「一次喝很多」划算
  • 喝水量的建議都查得到官方出處,不自己亂編公式
  • 整支程式離線跑,健康資料只留在自己電腦裡
  • 程式關掉的期間,該枯萎的還是會枯萎

故意不做的事

  • 不做排行榜、不跟朋友比,健康不該拿來比賽
  • 不做花錢就能跳過喝水的道具(急救水、加速肥料本來有,後來砍掉)
  • 不上傳雲端、不用註冊帳號
  • 不講任何療效

功能介紹

喝一次水,澆一株菜

整套玩法就靠一條規則撐起來,其他設計都是為了讓這條規則站得住腳。

規律配額模擬器

這裡跑的就是 garden_logic.py 那套規則。 拉一下滑桿,看同樣的水量、兩種喝法會差多少。

一次算 250ml,總共 1500 ml

一樣喝 1500 ml,但只分 1 次喝完

分次慢慢喝

可澆 6

今日收成金幣 0

一口氣灌完

可澆 1

今日收成金幣 0

24 種作物,照台灣的實際菜價分四級

分在哪一級會影響三件事:要連續幾天才解鎖、要澆幾次才長大、收成拿多少金幣。 價錢是查農業部「當季好蔬果」來的,不是憑感覺排。

初階 0 天 3 10 金 28–71
中階 3 天 4 25 金 78–130
高階 7 天 5 50 金 142–260
頂階 14 天 6 100 金 330–1031

橫條的長度就是那一級的價錢範圍,四條共用同一把 0–1031 元的尺。 頂階的櫻桃一公斤 1031 元,初階的紅蘿蔔 28 元,差了快 37 倍。 差這麼多,所以頂階要連續 14 天才給解鎖。

實際畫面

下面這幾張都是程式跑起來直接抓的,不是設計稿。

提醒彈窗:一滴水的圖示,標題「該喝水囉」,兩個按鈕分別是「我喝了一杯水」與「稍後再說」
提醒彈窗時間到就跳出來,按了才能挑一格去澆
商店的種子分頁:頂階作物需連續 14 天解鎖,目前 12 天,五種作物售價皆為 400 金幣但購買鈕為停用狀態
商店還沒解鎖的作物,就算錢夠也種不下去
飲水紀錄頁:平均每日 1914ml、每天喝 6.6 次,下方是時段分布長條圖與最近七天的次數趨勢圖
喝水紀錄除了總量,也看你都幾點喝、隔多久喝
設定頁:體重 77 公斤、男性、26 歲、高度活動、季節自動判斷為夏季,每日目標 3460ml
設定改體重會重算目標,也可以自己填想要的數字

系統架構或流程圖

畫面是網頁做的,但整支程式沒有伺服器

pywebview 會開一個一般的視窗,把本機的 HTML 讀進來, 然後把 Python 的函式直接給 JavaScript 叫。 不用 HTTP、不用設連線字串,也沒有後端要架。

前端 web/ 8 個 HTML 頁面 garden / shop / history settings / reminder … 共用 icons / theme / toast 5,000 行 pywebview js_api 橋接 WebView2 API 層 main.py GardenAPI 31 個方法 純邏輯層(無副作用) garden_logic.py — 澆水/枯萎/連續天數 water_calculator.py — 飲水公式 reminder_scheduler.py — 間隔計算 輸入資料 → 輸出資料 不碰資料庫、不碰畫面 db.py SQLite · 7 張表 唯一碰資料庫的檔案 測試(pytest) 148 個 · 0.2 秒 不需要開視窗 呼叫 讀寫 直接呼叫
重點在中間那條線:核心規則都被拉出來寫成不碰資料庫、也不碰畫面的函式, 所以 148 個測試可以直接叫它們,0.2 秒就跑完。 後面講的那兩個 bug,就是在把規則拉出來的時候才發現的。

三個重要決定,都寫成 ADR 留著

每一份都寫清楚「為什麼不用那個看起來比較直覺的做法」, 免得以後自己好心把它改回去。

  1. ADR-0002

    澆水額度看次數,不看總量

    如果改成「看今天喝夠沒」,那一次灌 2000ml 就能把整座菜園澆完, 而這剛好就是最該防的喝法,遊戲跟健康目標就脫鉤了。

  2. ADR-0003

    沒開程式的期間,開起來再回頭算枯萎

    不養一個在背景一直跑的服務。開程式的時候,拿每一株的 last_watered_at 跟現在比,超過時限就算它枯了。 反正只有開程式才看得到菜園,結果一樣但簡單很多。

  3. ADR-0001

    資料庫放在 exe 旁邊,不要放解壓縮出來的資料夾

    PyInstaller 會把程式解壓到 sys._MEIPASS,程式一關就清空。 資料庫放那裡等於每次重開都忘記你是誰。這是真的踩過才知道的, 不是先想到的。

使用的 Package

第三方套件只有三個

資料庫用 Python 自己就有的 sqlite3,日期、JSON、執行緒也都是標準庫。 拿給別人跑不太會卡在裝環境。

pywebview

5.4

開一般的視窗把本機 HTML 讀進來,再把 Python 的函式給 JavaScript 叫。 選它是因為畫面可以用熟悉的 HTML/CSS 寫,又不必像 Electron 那樣包一整個瀏覽器進去。

pygame

2.6.1

只用到 pygame.mixer 這一個模組,讓背景音樂不受畫面影響, 換頁也不會斷掉。電腦沒有音效裝置的話它就安靜地不出聲,不會拖累其他功能。

PyInstaller

打包用

打包成一個 exe(43.9 MB),不用安裝,點兩下就開。 datas 會把整個 web/ 包進去,之後加圖檔也不用改設定。

語言模型

未使用

喝水量是公式算的,作物分級是查公開菜價來的。 這兩件事都有確定的答案,沒必要讓模型去猜,也不該讓它猜。

素材與資料來源

素材與資料來源及其授權
素材來源授權
作物插畫(24 種 + 2 張成長階段共用圖) OpenMoji CC BY-SA 4.0
介面字型 jf open 粉圓體 SIL OFL 1.1
背景音樂與四個動作音效 Pixabay Pixabay Content License
飲水量計算依據 衛生福利部國民健康署 公開衛教資料
作物分級的市價依據 農業部「當季好蔬果」 公開統計資料

目前成果

兩個藏很久的 bug,和把它們釘死的 148 個測試

這個專案我最想講的不是做了幾個功能,而是這兩個 bug。 它們的共同點是你操作一次根本看不出來,所以手動點來點去永遠測不到。

148 自動化測試
0.2 全部跑完
12 把 bug 改回去就會變紅的測試
52 枯萎時限全部試過的組合
缺陷一

連續天數根本不連續

garden_logic.py · calculate_next_streak()

原本只看「上次澆水那天不是今天」,完全沒去確認是不是昨天。 所以中斷三個月再回來澆一次,連續天數照樣加一。

照這樣算,一個月只開一次程式,14 個月就能解鎖要「連續 14 天」的頂階作物。 這樣哪裡叫規律?ADR-0002 最想守的那件事就這樣被繞過去了。

修正前 1/1 1 2/1 2 4/1 3 中斷 31 天 中斷 59 天 修正後 1/1 1 2/1 1 4/1 1 間隔不是 1 天 → 重設
一樣是澆三次,改之前會累加到 3,改之後每次都歸零重算
缺陷二

枯萎時限比提醒間隔還短,變成一種死循環

garden_logic.py · _get_wither_limit_minutes()

枯萎時限是「提醒間隔 × 每級的寬限倍率」,然後再壓一個絕對上限。 問題是那個上限沒有再加一層下限保護

提醒間隔設 150 分鐘的話,頂階作物算出來是 min(150×1.5, 90) = 90 分鐘, 比提醒間隔本身還短。也就是程式還沒提醒你,菜就已經死了。

而枯萎會讓連續天數歸零,所以就變成 解鎖頂階 → 種下去 → 一定枯萎 → 資格又沒了,繞不出來。

30 90 150 180 0 100 150 提醒間隔(分鐘) 枯萎時限(分鐘) 必死區 提醒間隔本身 修正後:不低於一個間隔 修正前:封頂 90 分 交叉點
兩條線在間隔 90 分鐘的地方交叉。從那裡開始紅線(改之前)就掉到虛線底下, 意思是枯萎時限比提醒間隔還短,程式還沒提醒你菜就死了。 綠線加了下限保護,永遠在虛線上面。

為什麼要全部組合都試

第二個 bug 只有在特定組合才會出現,階級越高、間隔越長才踩得到。 所以枯萎時限是把 4 個階級 × 13 種提醒間隔,總共 52 組全部跑過, 不是挑幾個「看起來有代表性」的數字試試看。

測試自己也要被檢查

測試寫完之後,我把那兩個 bug 故意改回原本的錯誤寫法, 確認真的會有 12 個測試變紅。 如果把程式改壞了測試還是全綠,那這份測試根本沒在做事。

跑一次全部測試

> python -m pytest
148 passed in 0.19s

未來改進方向

接下來:把菜園搬上雲端

桌面版已經證明這套玩法行得通,接下來要處理的是「只能在自己那台電腦種菜」這件事。 雲端的幾個主要部分都寫好也試過可以跑,這是接著要走的方向。

進行中

電腦跟手機看到同一座菜園

規則引擎(Cloudflare Workers + D1)已經寫好也試過可以跑。 接下來要把桌面版的 SQLite 跟雲端的資料表接起來, 這樣同一座菜園在電腦跟手機上看到的就會一樣。

  • Cloudflare Workers
  • D1
  • React
規劃中

提醒也送到手機

桌面版最強的地方是坐在電腦前的時候會準時叫你, 但一離開座位就沒用了。加上 Web Push 之後, 離開電腦的那段時間也能接著提醒,兩邊剛好補起來。

  • Web Push
  • Service Worker
規劃中

把資料庫跟畫面的測試補起來

現在這 148 個測試都是在測不碰資料庫的函式。 會動到資料庫的(water_slotplant_crop) 跟畫面上的操作,目前還靠一份約 500 項的清單手動點,這部分想改成自動跑。

  • pytest fixture
  • Playwright
構想

讓菜園看起來更像菜園

現在作物就是換四張圖,一階一張。 想加天氣、不同季節的樣子,還有蟲害之類的事件, 讓「照顧」這件事有更多東西可以看。

  • 季節外觀
  • 隨機事件

Reference 或參考資料

參考資料

喝水量的每一個係數都查得到官方衛教出處,作物分級也查得到公開的菜價資料。

  • 衛生福利部|清涼消暑 聰明喝水笑喝呵 「體重 × 30ml」這個基本公式就是從這裡來的。
  • 國民健康署|對抗夏季,笑喝呵 活動量、季節要加多少水,還有每小時最多喝多少,都是看這份。
  • 國健署《國人膳食營養素參考攝取量》第九版草案 男女的差別是照建議量男性 2400ml、女性 2100ml 的比例去估的(那份建議是根據 2017–2020 台灣國民營養健康調查訂的)。
  • 農業部|當季好蔬果 24 種作物分四級就是照這裡的菜價排的,查的日期是 2026-07-18。
  • OpenMoji 作物的圖,CC BY-SA 4.0,可以改也可以再散布。
  • jf open 粉圓體 介面的字型,SIL Open Font License 1.1,可以包進程式裡。
  • pywebview 開視窗跟 js_api 怎麼用,都看這份官方文件。

這個程式算出來的喝水量只是給日常參考用的。每個人實際需要多少, 會因為身體狀況、天氣、有沒有生病而不一樣。如果有腎臟或心血管方面的疾病, 或是其他特殊狀況,請先問過醫生再調整。