iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Claude AI

奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲系列 第 23 篇

Day 23:敵軍波次平衡調校:用 Claude 協助遊戲數值設計與難度曲線分析

  • 分享至 

  • xImage
  •  

claude_23

系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code / Claude Cowork / Day 11 的 make sim(加 -csv)/ 自訂指令 /sim
今日進度:data/ 從 v0.1 調到 v0.2,三種建造策略在灰原邊境都能通關;順手修掉模擬器一個「同 seed 不同結果」的 bug

前言

Day 22 之後前後端是一體的了,但遊戲還不好玩。Day 11 的 v0.1 只做了一件事:把第一關從「0% 通關」救到「能通關」,代價是通關時平均只剩 4–5 命,而且火砲流 0%。六關裡只有第一關被認真看過。

今天不寫功能,只調數字。工具是 Day 11 的模擬器加一個 -csv 輸出,再讓 Claude Cowork 把曲線畫出來。開始前先講一個原則:數值調整必須是「改一個數字、跑 200 場、看曲線」的迴圈,憑手感改十個數字再打一場,永遠不知道是哪個數字起了作用。

claude_23_diagram_01

機器給數字,人做決定——這個迴圈今天跑了五輪。

一、-csv,以及第一個意外

cmd/sim/main.go 加一個 flag:每波結束時記一列 level,config,seed,wave,lives,gold,陣亡的那一波也記(lives=0),曲線才看得到斷崖在哪。它刻意不用 Day 11 的 worker pool,逐場循序跑——Trace 回呼要拿到 seed,而且 csv.Writer 不是 goroutine-safe:

for _, c := range cfgs {
	for seed := 0; seed < runs; seed++ {
		cfg := c
		cfg.Trace = func(wave, lives, gold int) { row(c.Name, seed, wave, lives, gold) }
		res := sim.Run(cat, lv, cfg, iㄨㄜnt64(seed))
		if !res.Won {
			row(c.Name, seed, res.WavesCleared+1, 0, res.GoldLeft)
		}
	}
}

三組 × 200 場一關 1.7 秒,不值得為它加鎖。

意外在跑第二次的時候出現:兩份 CSV 應該一模一樣(seed 固定),diff 卻有 23 場不同。我把 diff 貼給 Claude Code,它的第一個問題是「w.towers 是 map 嗎?」——是。Go 的 map 迭代順序刻意隨機,兩座塔搶同一個目標時誰先開火、誰浪費一發都會變。Day 10 才踩過同一種雷(Route 取最大值),Day 11 的 TestRunIsDeterministic 沒抓到是因為測試場景只有一座塔。修法是改成依 slot 排序後迭代;順便把測試改成三座塔。這本來就該是 Day 11 定案的規則——塔一律依 slot index 迭代,不是 map 給什麼順序就用什麼順序。沒有這個修正,接下來所有「前後對照」的數字都是雜訊。

二、把 CSV 交給 Cowork

Prompt(給 Claude Cowork,指定 docs/balance/ 資料夾)
「讀 sim-v01-*.csv(六關、三組配置、每波剩餘生命與金幣)。每關畫一張『波次 × 平均剩餘生命』折線圖,三組配置三條線;另外列出每關『掉最多生命的波次』與『陣亡時平均剩餘金幣』。輸出 curve-v01.png 與 report-v01.md,不要給我建議數值,只要事實。」

「不要給我建議數值」是刻意的:我要它當分析師,不是設計師。灰原邊境的表(200 場平均;修正迭代順序後重跑,所以與 Day 11 的 5.0/4.0 命有零點幾的出入):

配置 第 1 波 第 2 波 第 3 波 第 4 波 第 5 波 結果
bolt-rush 20.0 19.8 16.8 10.8 5.1 100%
mixed 20.0 18.1 15.1 9.1 3.9 99.5%
splash 20.0 13.0 10.0 4.0 0(死於第 4 波) 0%

曲線圖把兩件事看得很清楚:三條線在第 3→4 波、第 4→5 波各掉一截,是兩個斷崖不是一個;splash 那條線從第 2 波就開始掉,代表它的問題在開局,不在後期。這種形狀的判讀我用表格要看一分鐘,看圖只要一眼——Cowork 在這裡做的是「把 3,000 列 CSV 變成一張圖」這種我做得到但懶得做的事,而懶得做的事就會一直不做。

報告裡最有價值的是我沒要求的交叉比對:splash 陣亡時平均還有 62 金幣沒花。回頭看 Day 11 的建造佇列——火砲二級要 200 金,佇列卡在那裡等錢,第 4 波 10 隻疾風掠奪者就衝過去了。火砲流 0% 有一半不是火砲弱,是「玩家」太固執。這個發現讓我把 preset 也當成待調的對象。

另一個資料來源是 Day 13 的戰鬥稽核 item(SK = BATTLE#…,Query GSI1 WHERE GSI1PK = "DAY#<台北日期>" 就能撈出當天全部場次):上線 7 天累積 23 場真人紀錄(我自己與兩位朋友),樣本太小不能當依據,但它們在 v0.1 通關時剩餘生命的中位數 5,跟模擬器 mixed 的 3.9 在同一個量級。這至少證明模擬器沒有跟真人脫節——Day 17 用同配置對過一次,這是第二次。

三、v0.2:改了什麼、為什麼

每一項都是「改、跑、看」一輪;五輪、每輪不到兩秒模擬加十分鐘看數據。

項目 v0.1 → v0.2 理由
石殼巨獸 armor 0.5 → 0.45 物理塔對它太無力,第 3 波起每隻要 8 發弩箭
火砲 cost(L1/L2/L3) 125 / 200 / 300 → 110 / 180 / 260 早蓋火砲不該等於自殺;L3 幾乎沒人升得起
火砲 L1 cooldown 2.2 → 2.0 早蓋火砲的射速追不上前期怪
火砲 splash(L1/L2/L3) 40 / 48 / 56 → 48 / 56 / 64 整條階梯往上推一格——只改 L1 會讓 L1 splash 撞上原本的 L2
霜語塔 L1 cost 90 → 80 減速塔要在第 2 波前蓋得起
秘法塔 L1、L2 cooldown 1.5 / 1.4 → 1.3 / 1.2 mixed 的 DPS 明顯輸 bolt-rush,違反「魔法打硬皮」的設計
灰原邊境第 4 波疾風掠奪者 10 → 8 隻 第 4 波是每組都掉 5–6 命的斷崖
splash preset 火砲只升到 L2、弩塔先升 不再卡在等 300 金

石殼巨獸的 armor 一動,frontend/src/engine/damage.test.ts 那條期望值也要跟著改:takeDamage(30, 'physical') 從 30 × 0.5 = 15 變成 30 × 0.55 = 16.5 → round → 17。動資料就會動測試,這是 v0.2 當天真的發生的事,不是事後補寫。

有一件事我刻意沒動:塔的 damage 區間。傷害是 Day 3 資源三角的核心,四種塔的 DPS 比例一動,六關全部要重跑;改 cost 與 cooldown 影響的是「什麼時候買得起、多久打一次」,範圍小得多,而且玩家感受得到的是「這關能不能過」而不是「弩箭傷害 10 還是 11」。調數值的順序應該是先動關卡、再動價格與射速、最後才碰傷害。

一個反直覺的中間結果:我試著把第 5 波的石殼巨獸提前登場(delay 24 → 14),想讓高潮更緊湊,bolt-rush 從 13.2 命掉到 5.6。原因是巨獸與疾風掠奪者同時到場,弩塔用 first 策略全去打走最遠的巨獸,掠奪者全漏。這種交互作用手算不出來,只有模擬器看得到;我改回 24。

v0.2 的結果,同樣 200 場:

配置 通關率 平均剩餘生命 平均剩餘金幣 第 4 → 5 波掉血
bolt-rush 100%(5.1 → 13.3 命) 13.3 108 0.4
mixed 100%(3.9 → 11.2 命) 11.2 51 1.8
splash 0% → 99.5% 2.5 129 6.6

mixed 追到 bolt-rush 附近、splash 從不可能變成「打得過但很緊」——這是刻意的:火砲的主場是第 5 關黑岩隘口那種長路徑、大群體的地圖,在單路徑的第一關本來就不該是最佳解。資源三角的意思是沒有一種塔在每一關都對。

六關全部跑過一輪後的 mixed 通關率:灰原邊境 100%、霧河渡口 96.5%、斷橋古道 88.0%、棄守修道院 79.5%、黑岩隘口 91.0%、薪火王城 73.5%(餘燼之路 boss 血量 2000 時 86%)。斷橋古道是 splash 的 0% 關——霧翼是飛行單位,火砲打不到,這次我不調,它就是設計。

四、/sim:把迴圈變成一個指令

今天跑了三十幾次 cd backend && make sim,每次都要改 flag、開 CSV、算平均。所以做了 .claude/commands/sim.md——跟 Day 6 的 /ultracode 一樣,/sim 是我自己寫的 slash command,不是 Claude Code 內建功能:

---
description: 跑六關 × 三組配置的模擬並輸出 CSV,摘要難度斷崖,提出最多三項數值調整建議
argument-hint: [level id 或 all]
allowed-tools: Bash(make sim:*), Bash(go run ./cmd/sim:*), Read, Write
---
對 $ARGUMENTS(預設 all)逐關執行 `go run ./cmd/sim -level <id> -runs 200 -csv docs/balance/sim-<id>.csv`。
讀每份 CSV,輸出一張表:關卡 × 配置 × 通關率 × 掉最多生命的波次 × 陣亡時剩餘金幣。
標出「任一配置單波掉 ≥ 5 命」的斷崖。最多提出三項調整建議,每項附預期影響與理由。
**不要修改 `data/` 底下任何檔案**;建議由我決定後手動套用。

最後一行是這個指令的靈魂。數值是設計,設計要有人負責。「最多三項」也是刻意的:Claude 一次提十項建議時,我會全部套用然後不知道是哪一項起了作用——這正是前言說的手感式調法,只是換成 AI 的手感。三項以內,我才有力氣一項一項驗。

跑一次 /sim all 約 12 秒模擬加 40 秒讀 CSV,輸出一張六關的表與最多三條建議。第一次跑它就指出斷橋古道的 splash 0%,並「建議」把霧翼改成非飛行——這正是我不想要的那種建議,也是為什麼決定權要留在人手上。

順手把 data/VERSION 填上 v0.2;API 啟動 log 補印 dataVersion=v0.2,這樣「今天是 v0.2」不是我嘴上說說,是一行能查證的 log。

小結

今天最大的收穫不是 v0.2 的數字,是三個關於「怎麼調數值」的方法:一、迴圈要短到不會偷懶(1.7 秒一輪);二、分析與決策分開——Cowork 給事實、Claude Code 給建議、我做決定;三、玩家模型(preset)也是數值的一部分,0% 不一定是塔的錯。還有一個老教訓再犯一次:map 迭代順序。Day 10 學過的東西,Day 11 換個地方又寫出來,測試太弱才沒抓到。

明天把眼睛從遊戲移到伺服器:上線之後它出事了,誰會知道?

今日產出

  • [x] cmd/sim/main.go 加 -csv;internal/sim/sim.go 修正塔的迭代順序、TestRunIsDeterministic 改三座塔
  • [x] data/towers.json、enemies.json、levels/ashfield.json v0.2;cmd/sim/presets.go 修正 splash 順序
  • [x] docs/balance/{sim-*.csv,curve-v01.png,curve-v02.png,report-v01.md}(Cowork 產出)
  • [x] .claude/commands/sim.md;data/VERSION 填上 v0.2、啟動 log 補 dataVersion

明日預告

Day 24:監控告警上線:CloudWatch 與雲端預警系統,讓伺服器不再無聲陣亡。


上一篇
Day 22:前後端聯軍:API 串接與跨域戰爭,Vue 呼叫 Go 服務的實戰筆記
下一篇
Day 24:監控告警上線:CloudWatch 與雲端預警系統,讓伺服器不再無聲陣亡
系列文
奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言