Vibe Coding 第二篇,解決《小熊諾樂GO!香港版》的隨機化問題
遊戲只由一名香港人嘔心瀝血開發,目的在於讓香港人找回香港的感覺,並且可以讓新一代通過遊戲學習香港地理和各區特色。
Vibe Coding 第一篇, 一個香港夢: 與 AI 共建小熊諾樂GO! | by Lorentz Yeung | Apr, 2025 | Medium
新挑戰:可預測的隨機性
作為《小熊諾樂GO!香港版》的開發者,我已經和 AI 合作了大約三週,將這個以火車為主題的棋盤遊戲帶入生活。但是我發現了一個新問題:遊戲內的問題和事件的隨機化不如預期那樣隨機。遊戲有 440 條 trivia 問題,黃色車站,和與季節性有關的隨機事件等,這些全都需要依靠隨機去選擇出來,因為我希望每場遊戲都能讓玩家感覺到新鮮感。更重要的是,透過隨機問題,希望年輕的玩家能從遊戲認識到香港的特色。然而,現在玩家卻不斷只看到相同的問題 — 如 QIDs 1、100、233、301 — 出現得太頻繁。事件的隨機性也感覺不對,雖然因為選項較少而較不明顯。因此我寫下了這篇日誌,記錄了我如何與 AI 一起解決這個問題。
罪魁禍首:PRNG 和可預測模式
問題源自 Python 的 random
模組,這是一個本質上確定性的偽隨機數生成器 (PRNG)。如果沒有多樣化的種子,它會生成相似的序列。在我的遊戲中,我首先在啟動時和每個遊戲會話開始時各洗牌問題列表一次,但選擇仍使用 question_count % len(all_questions)
。如果每個遊戲開始時 question_count 重置為 0,我就總是從洗牌後列表的開頭挑選。如果由於 PRNG 種子相似,洗牌在會話之間變化不大,all_questions
的早期元素 — 如索引 0、1、2 — 會保持大致相同。因此,玩家反复看到相同的問題(例如 IDs 1、100、233、301)。黃色車站的事件使用 random.choice()
挑選,也面臨同樣的問題。
第一個解決方法:雙重洗牌
我最初的修復方法很簡單:洗牌兩次。代碼如下:
all_questions = [(cat, q) for cat in questions for q in questions[cat]]
random.shuffle(all_questions)
custom_print(f"已將 all_questions 洗牌一次...")
在會話開始時:
random.shuffle(all_questions)
custom_print(f"為新遊戲會話洗牌 {len(all_questions)} 個問題")
我認為更多洗牌等於更多混亂,對吧?錯了。相同的問題還是頻繁出現。PRNG 種子默認與系統時鐘綁定,變化不足,而我的選擇方法將我鎖定在列表的前端。
第二個解決方法:用納秒播種
接下來,我使用 time.time_ns()
提升種子的精確度到納秒級:
random.seed(time.time_ns())
random.shuffle(all_questions)
custom_print(f"使用種子 {time.time_ns()} 洗牌 {len(all_questions)} 個問題")
這有些幫助 — 種子更獨特 — 但選擇仍依賴 question_count
。列表前端的項目仍占主導地位,440 個問題的龐大,但洗牌幅度不足以打破模式。
突破:隨機索引挑選
我的 vibe coding 夥伴 Grok 3 出場了。它指出真正的缺陷:如果我總是按可預測的方式循環,為什麼要洗牌?於是我改用 random.randint()
來挑選問題:
question_idx = random.randint(0, len(all_questions) - 1)
category, question_data = all_questions[question_idx]
砰 — 現在每個問題每次都有 1/440 的機會被選中,無論列表順序如何。納秒種子保持 PRNG 在會話間的新鮮度,重複現象大致消失了,但仍有跡可尋。
人性化的轉折:自定義種子風格
Grok 建議使用完整的 time.time_ns()
,但我想按自己的方式來。我設計了這個:
random.seed((time.time_ns() % 10**9) / 1000)
random.shuffle(all_questions)
custom_print(f"使用種子 {(time.time_ns() % 10**9) / 1000} 洗牌 {len(all_questions)} 個問題")
只取納秒時間戳的最後 9 位(例如 823074000)並除以 1000(得到 823074.0),因為我發現最後嗰個位的前六個位的數字變化最大。於是這樣就給了我一個有趣的浮點種子,仍然適用於 random.seed()
。感覺很動態,測試中也保持了不可預測性。
修復事件
黃色車站的事件也需要調整。原本是:
event_list = seasonal_events[current_season]
event_tuple = random.choice(event_list)
我將其與問題的修復對齊:
event_idx = random.randint(0, len(event_list) - 1)
event_tuple = event_list[event_idx]
custom_print(f"為 {current_season} 季節選中事件索引:{event_idx}")
使用相同的自定義種子,遊戲內的事件,大致上都隨機出現了,沒有特別偏向某一個事件。
最終配方
解決方案由三個要素組成:自定義種子 ((time.time_ns() % 10**9) / 1000
)、random.randint()
用於挑選,以及日誌確認其有效。沒有了可預計重複性 — 只有純粹的、充滿活力的隨機性。
Vibe Coding 的實踐
我在玩遊戲時發現了可預計的隨機性 — AI 無法「感覺」到這一點。我調整了洗牌和種子變化,然後 Grok 帶著 random.randint()
介入,解決了技術問題。我們一起解決了隨機化問題。
為何人類依然主宰
AI 很出色,但它不能在沒有我玩遊戲的情況下發現遊戲「感覺不對」。由於我的數據科學背景,我之前就遇到過隨機化問題,所以我知道PRNG的成因和下一步該往哪個方向走。我推動了願景方向,Grok 完善了它。我相信這就是 vibe coding 的魔力 — 人類的經驗和願景與 AI 的力量相遇。
總結
修復《小熊諾樂GO!》的創造是一個難忘的旅程。今次這個問題,從雙重洗牌到自定義種子與索引挑選系統,現在每次玩這個遊戲都帶來滿足感。如果你正在編碼某樣東西,相信你的直覺,與 AI 一起 vibe,然後發布它!
免費下載香港開發遊戲:Lorye Go! 首頁
GitHub 儲存庫:entzyeung/LoryeGo
Windows 或者 Mac版本安裝方法:遊戲安裝方法
香港人,開心遊戲吧!快跳上車,一起感受這股熱血氛圍!
支持作者
如果覺得這篇文章有用,請考慮透過 PayPal小額贊助 捐款支持我!
如果你享受這段旅程,或只是想感謝我將香港的熱鬧混亂化為遊戲的一部分,歡迎透過PayPal小額贊助 或 成為GitHub贊助者。你的支持讓火車繼續奔馳、程式碼持續流動,也讓每位玩家的熱情氛圍永不止息。
謝謝你!