桌面應用程式 · Python + pywebview

Verdi

今天為身體灌溉的水,是明天一起發的芽。

每喝一次水,就解鎖一次澆水機會。細心照顧蔬菜長大,收穫金幣,打造更大的菜園。 遊戲的核心規則很簡單:今天喝幾次水,就能澆幾次菜。。 真正被獎勵的,不是喝得越多,而是每天持續喝水的習慣。

  • 喝水換澆水的養成循環
  • 獎勵規律,不獎勵灌水
  • 依真實菜價分級的 24 種作物
  • 離線可用,資料留在自己電腦
  • 關掉程式,菜園照樣照時間走
製作
賴子渝 Joe Lai
核心套件
Python 3.12 · pywebview 5.4 · SQLite · pygame 2.6
平台
Windows 單機
程式的開始畫面,實機錄影

動機與目標

讓每一次喝水,都看得見回饋

喝水提醒 App 大多是同一套:設個間隔、跳通知、按「已喝水」。 按下去之後畫面沒有變化,所以那顆按鈕很容易就變成順手滑掉的東西。

看得見變化的提醒,才留得住。

所以,沃田把喝水的回饋設計成一座會成長的菜園。每喝一次水,就能獲得一次澆水機會; 細心照料後,蔬菜會慢慢長大、開花結果,最後還能收成並換取金幣,持續擴建自己的菜園。 使用者今天的每一次行動,都會立即反映在畫面上,打開程式就能看見自己的累積成果。

這樣的設計也十分符合使用情境。無論是讀書或工作時,大多數人都長時間坐在電腦前, 手機放在一旁往往容易被忽略;而桌面上的應用程式則始終位於視線範圍內, 讓使用者能在不打斷工作流程的情況下,隨時留意自己的喝水進度與菜園成長, 將健康習慣自然融入日常生活。

我想解決什麼問題

喝水這件事的難處不在「不知道要喝」,而在喝得不夠規律—— 忙起來忘了,想起來就一次灌一大杯。

既有的提醒工具多半只記「今天喝了多少」,於是一次灌 2000ml 跟分八次慢慢喝, 在紀錄上完全一樣。這樣一來,提醒能改變的行為其實很有限。

我想做的是讓喝水的節奏本身變成有回饋的事, 而且回饋要立刻看得到、累積得起來。

為什麼選這個主題

因為養成遊戲的節奏跟規律喝水天生吻合:作物要分次照顧才會長大, 一次澆再多水也沒有用——這正好就是喝水該有的樣子。

  • 行為對應得上喝一次水換一次澆水,規則單純到不用解釋
  • 回饋看得見成長階段、金幣、菜園格數都是具體的累積
  • 數字查得到出處飲水公式來自衛福部,作物分級來自農業部菜價
  • 情境很明確坐在電腦前的時候,桌面視窗比手機通知更難忽略

互動設計

喝一次水,澆一株菜。

這是整個系統最核心的互動規則,也是所有功能設計的基礎。 使用者每完成一次喝水紀錄,就立即獲得一次澆水機會, 讓每個健康行為都能即時轉化為遊戲中的成長回饋, 形成「喝水 → 澆水 → 成長 → 收成」的循環體驗。

互動規律配額模擬器

這裡跑的就是 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

這四條橫條都是用同一個 01031 元/公斤的價格範圍去畫, 所以可以直接看出四個等級之間的差距。 最高的是櫻桃,每公斤約 1031 元;最低的是紅蘿蔔,每公斤約 28 元, 兩者差了將近 37 倍。 所以頂級作物不會一開始就開放,而是要連續喝水 14 天之後才會解鎖。

24 種作物一覽

每一張卡上的價錢,就是農業部公布的市場零售價;種子價則是還沒解鎖時, 想先種它要付的金幣。

初階7 種

0 天解鎖3 階段收成 10 金市價 28–71 元/公斤

一開始就能種,3 次澆水就成熟,適合剛養成習慣的時候。

  • 紅蘿蔔 28 元 種子 20 金
  • 茄子 40 元 種子 20 金
  • 馬鈴薯 44 元 種子 20 金
  • 西瓜 53 元 種子 20 金
  • 香蕉 54 元 種子 20 金
  • 洋蔥 58 元 種子 20 金
  • 青椒 71 元 種子 20 金

中階7 種

連續 3 天4 階段收成 25 金市價 78–130 元/公斤

連續 3 天解鎖,需要 4 次澆水,收成是初階的 2.5 倍。

  • 小黃瓜 78 元 種子 60 金
  • 鳳梨 79 元 種子 60 金
  • 玉米 80 元 種子 60 金
  • 青花菜 100 元 種子 60 金
  • 哈密瓜 100 元 種子 60 金
  • 芒果 128 元 種子 60 金
  • 蘋果 130 元 種子 60 金

高階5 種

連續 7 天5 階段收成 50 金市價 142–260 元/公斤

連續 7 天解鎖,5 次澆水才成熟,市價已經是初階的三到九倍。

  • 蒜頭 142 元 種子 150 金
  • 番茄 157 元 種子 150 金
  • 桃子 240 元 種子 150 金
  • 水梨 241 元 種子 150 金
  • 檸檬 260 元 種子 150 金

頂階5 種

連續 14 天6 階段收成 100 金市價 330–1031 元/公斤

連續 14 天解鎖,6 次澆水,收成 100 金——菜價最高的那一群。

  • 奇異果 330 元 種子 400 金
  • 草莓 396 元 種子 400 金
  • 葡萄 474 元 種子 400 金
  • 藍莓 656 元 種子 400 金
  • 櫻桃 1031 元 種子 400 金

每日目標怎麼算出來的

公式的每一項都查得到官方出處。拉拉看,數字會一項一項疊起來。

  1. 體重 70 × 302100
  2. × 性別係數 男性 1.02100
  3. + 活動量 中度+400
  4. + 季節 夏季+400
  5. 每日目標2900

性別係數只乘在體重基數上,不乘在加成。算完再取 max(結果, 1500) 做下限保護。 年齡不影響目標值,它負責切換安全上限—— 65 歲以上改成單次 150ml、每小時 800ml。 依據:衛福部、國健署(見 Reference)

實際畫面

下面這幾張都是程式跑起來直接抓的。

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

系統架構或流程圖

畫面採用網頁技術製作,但整支程式不需要伺服器

系統透過 pywebview 開啟一般桌面視窗,直接載入本機的 HTML、CSS 與 JavaScript, 再將 Python 函式提供給 JavaScript 呼叫。因此整體架構不需要經過 HTTP, 也不必設定連線字串,更沒有額外的後端服務需要部署。 對使用者來說,它就是一個可以直接開啟使用的桌面程式; 對開發端來說,則保留了網頁介面的彈性與 Python 的處理能力。

前端 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 秒就跑完。 後面講的那些機制細節,就是在把規則拉出來的時候一起看清楚的。

三個關鍵技術決策,都以 ADR 完整記錄

ADR(Architecture Decision Record)不只記錄最後採用的方案, 更說明為什麼沒有選擇看起來更直覺的做法。例如: 為什麼以「喝水次數」而不是「喝水總量」作為獎勵依據、 為什麼不採用常駐背景服務,以及為什麼資料庫要放在執行檔旁而非暫存目錄。 透過完整記錄決策背景、替代方案與取捨理由,即使日後回頭維護, 也能快速理解每項設計背後的思考脈絡。

  1. ADR-0002

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

    如果改成「看今天喝夠沒」,一次灌 2000ml 就能把整座菜園澆完。 綁在次數上,遊戲進度才會跟著規律走。

  2. ADR-0003

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

    不養一個常駐背景服務。開程式時拿每一株的 last_watered_at 跟現在比就好。 反正只有開程式才看得到菜園,結果一樣但單純很多。

  3. ADR-0001

    資料庫放在 exe 旁邊

    PyInstaller 會把程式解壓到 sys._MEIPASS,那是暫存區。 改用 sys.executable 所在的目錄,資料就穩穩留著。 這是實際打包測試時發現的。

使用的 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
飲水量計算依據 衛生福利部國民健康署 公開衛教資料
作物分級的市價依據 農業部「當季好蔬果」 公開統計資料

目前成果

148 個測試,把機制細節一項一項釘住

這個專案我最想講的是這一段。有些機制細節操作一次看不出來, 要跨好幾天、或特定設定組合才會顯現——這正是自動化測試最派得上用場的地方。

0 自動化測試
0.2 全部跑完
0 還原舊邏輯就會亮紅燈的測試
0 枯萎時限窮舉組合
機制一

讓連續天數真的連續

garden_logic.py · calculate_next_streak()

連續天數是頂階作物的解鎖門檻,所以它得真的代表「每天都有來」。 關鍵是那一個判斷:上次澆水是不是昨天

補上這個判斷之後,中斷幾天再回來就會重新起算, 「連續 14 天」這個門檻才名副其實,ADR-0002 想守的規律性也就守住了。

同時把這段邏輯從 water_slot() 裡抽出來寫成純函式, 跨月、跨年、閏年 2028-02-29 都能直接測,不必先偽造一整個資料庫狀態。

只看「不是今天」 1/1 1 2/1 2 4/1 3 中斷 31 天 中斷 59 天 加上「是不是昨天」 1/1 1 2/1 1 4/1 1 間隔不是 1 天 → 重新起算
一樣澆三次水,差別就在那一個判斷:連續天數是否重新起算。
機制二

幫枯萎時限加上底線保護

garden_logic.py · _get_wither_limit_minutes()

枯萎時限是「提醒間隔 × 每級的寬限倍率」,再壓一個絕對上限。 階級越高越嚴格:倍率 ×3 / ×2.5 / ×2 / ×1.5,上限 240 / 180 / 120 / 90 分鐘。

這裡再加一層下限保護,讓時限永遠不低於一個完整的提醒間隔。 提醒間隔設 150 分鐘時,頂階作物就從 min(150×1.5, 90) = 90 變成 max(90, 150) = 150 分鐘。

這樣一來,不論提醒間隔怎麼設定,程式一定會先提醒你,你也一定來得及照顧它。 max(min(間隔 × 倍率, 上限), 間隔)——一行就守住了。

30 90 150 180 0 100 150 提醒間隔(分鐘) 枯萎時限(分鐘) 提醒間隔本身 加上下限保護:不低於一個間隔 只用絕對上限:封頂 90 分 交叉點
兩條線在間隔 90 分鐘處交叉。加上下限保護後(綠線), 時限永遠待在虛線之上——不論提醒間隔設多長,都來得及照顧。

還有五個,都寫在 docstring 裡

這個專案有個習慣:踩到什麼就順手寫進註解, 連當初為什麼會這樣、後來怎麼處理都留著。以後回來看就不用重新推理一次。

db.py

資料庫穩穩留在 exe 旁邊

PyInstaller 解壓的 sys._MEIPASS 是暫存區,改用 sys.executable 的目錄。

db.py

剛種下的苗,當天就能照顧

新苗的 last_watered_date 留空,這樣它不會被當成「今天已經澆過」。

db.py

重置今日飲水後,配額跟著歸零

連同當天澆過的紀錄一起清乾淨,配額就不會算成負數。

main.py

提醒彈窗留住自己的結果訊息

澆完水先不關視窗,等訊息看完再由前端收掉。

main.py

階段數一起回傳給前端

每一格都帶上 stages,畫面就能完整顯示「第 N / M 階段」。

為什麼要全部組合都試

枯萎時限的特性跟階級與間隔都有關,所以直接把 4 個階級 × 13 種提醒間隔,共 52 組全部跑一遍, 而不是挑幾個「看起來有代表性」的數字。窮舉才不會剛好跳過某個組合。

測試自己也要被檢查

測試寫完之後,把兩段舊邏輯還原回去重跑, 確認真的會有 12 個測試亮紅燈,再改回來全綠。 這樣才知道這份測試真的在做事。

跑一次全部測試

> python -m pytest
148 passed in 0.25s

未來改進方向

接下來想做的四件事

桌面版已經把核心玩法跑順了,接下來想讓它在更多情境下也好用。

進行中

改用 Windows 原生通知

現在的提醒是程式自建的視窗。改成系統原生的 Toast Notification 之後, 程式最小化的時候也能準時提醒,更貼近「坐在電腦前」的使用情境。

  • Windows Toast
規劃中

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

現在 148 個測試涵蓋的是不碰資料庫的純函式。 接著想用 pytest fixture 搭配記憶體型 SQLite, 讓 water_slot()plant_crop() 也能自動測。

  • pytest fixture
  • in-memory SQLite
規劃中

無障礙再往前一步

字級可調、鍵盤操作支援。靜音鈕已經從「雙擊喇叭」改成看得見的按鈕—— 這種「功能在、但不容易被發現」的地方,是接下來要優先檢查的類型。

  • 字級調整
  • 鍵盤操作
構想

讓菜園更有看頭

現在作物是一階一張圖。想加上季節外觀與隨機事件, 讓「照顧」這件事有更多可以看、可以期待的變化。

  • 季節外觀
  • 隨機事件

Reference

參考資料

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

  • 衛生福利部|清涼消暑 聰明喝水笑喝呵 「體重 × 30ml」這個基本公式的出處。
  • 國民健康署|對抗夏季,笑喝呵 活動量、季節的加成,還有單次 200ml、每小時 1000ml 的安全上限。
  • 國健署《國人膳食營養素參考攝取量》第九版草案 性別係數依建議量男性 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 的用法,都看這份官方文件。

這個程式算出來的喝水量供日常參考。每個人實際需要多少, 會因身體狀況、天氣、有無疾病而不同。如有腎臟或心血管方面的疾病, 或其他特殊狀況,請先諮詢醫師再調整。

THANKS FOR WATCHING 賴子渝 JOE LAI