iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Claude AI

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

Day 06:Monorepo 還是分離倉?專案架構決策與 Claude Code 的專案記憶(CLAUDE.md)設定

  • 分享至 

  • xImage
  •  

claude_06

系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code(/init、CLAUDE.md、settings.json、自訂 slash command)
今日進度:Monorepo 骨架建立,Claude Code 專案記憶與第一版 /ultracode 就位

前言

昨天畫定的邊界留下一個具體問題:data/ 前後端都要讀,還有 Terraform、文件、設計稿——這些要放一個 repo 還是拆開?今天先做這個決策,然後做更重要的事:教 Claude Code 認識這個專案。

Claude Code 每開一次 session 都是一張白紙,對專案的理解全靠 CLAUDE.md 這類「專案記憶」餵進去,這份記憶的品質直接決定接下來 24 天的產出。

一、Monorepo:因為契約只能有一份

考量 Monorepo 多 repo
data/*.json 單一來源 天然單一 要 submodule 或發套件同步
改契約同時動前後端 一個 PR 多個 PR,順序依賴
Claude Code 的上下文 一份 CLAUDE.md 看全貌 各自記憶,跨端要切換

單人、30 天、資料契約是核心——Monorepo 沒有懸念。唯一的代價是 CI 要寫 path filter,避免改前端也觸發後端部署,Day 16 處理。

定案的結構是根目錄十二個項目(另有 .gitignore 等隱藏檔),前兩天的 data/docs/ 直接搬進來;完整目錄樹寫在 docs/adr/0004-monorepo.md,之後每加一個頂層目錄都要回去更新它。順帶定一條規矩:docs/ 今天只有 gdd.mdadr/story/design/ 這類「之後才會用到」的空目錄一律不先開——git 本來就不追蹤空目錄,開了只會讓目錄樹說謊。

claude_06_diagram_01

一個細節:go:embed 不能引用 module 之外的檔案,而 data/backend/ 外面,所以後端不複製 data/,改用環境變數 GAMEDATA_DIR 指過去,本機是 ../data。上雲之後交付物是一個容器映像,backend/Dockerfile建映像時data/ 複製進 /var/task/data——build context 因此是 repo 根目錄。前端的 Vite alias @data 只在 vitest 與 e2e fixture 用;遊玩時的塔與關卡資料跟 API 要,不進 bundle。

二、/init 產出,再手工修成「我的」CLAUDE.md

在根目錄開 claude,輸入 /init。它產出的 CLAUDE.md 內容正確但偏「描述現況」,Claude 讀了不知道該怎麼做事。我保留結構,把每一段重寫成祈使句:

# Emberhold 薪火要塞

奇幻塔防,資料驅動、三條劇情路線。Vue 3 + Canvas(frontend)、Go(backend)、
Terraform(infra/terraform)。遊戲資料唯一來源在 `data/`,前後端都讀它,不要複製。

## 常用指令
- `make dev`:同時啟動 api(:8080)與 web(:5173)
- `make test`:`go test ./...` + `bun run test`
- `make smoke`:起一支無 AWS 的 API,走完契約測試
- `make sim LEVEL=ashfield`:跑波次模擬
- `make image`:建 Lambda 容器映像(linux/arm64,含 data/);`make push-image` 推 ECR
- `make plan`:terraform plan(apply 只能由我手動執行)

## 硬規則
- 命名全部原創:世界觀、塔、敵人、關卡、角色不得影射任何既有作品。
- 改 `data/*.json` 格式前先改 `docs/gdd.md`;劇情資料改完要重新產生 `docs/story/tree.md`。
- Go:Clean Architecture,依賴只能由外向內(adapter → usecase → domain);
  錯誤用 `fmt.Errorf("...: %w", err)` 包裝;handler 不含業務邏輯。
- Go:一份 binary 兩種模式。`AWS_LAMBDA_FUNCTION_NAME` 有值就交給 Lambda adapter,
  否則起 `net/http` 監聽 `:8080`。兩邊共用同一個 chi router,不得分岔。
- Go:`SAVES_TABLE` 為空就用記憶體倉儲,有值才連 DynamoDB。本機開發不需要任何 AWS 資源。
- Vue:`<script setup lang="ts">`;引擎層 `src/engine/` 不得 import Vue 或 Pinia。
- 先寫失敗的測試再修 bug;改動 ≤ 3 個檔案時不要開新抽象。
- Commit 用 Conventional Commits(feat/fix/infra/docs/chore)。

## 目前進度
Day 6:骨架完成,尚無業務程式碼。ADR 0001–0005 在 `docs/adr/`。

make smokemake image 要 Day 16 才跑得動,但「常用指令」從今天起只增不改;硬規則裡的 docs/story/tree.md 也還不存在。「目前進度」我每天收工更新一行,讓隔天的 Claude 一開 session 就知道進度。

寫完做了一個很便宜的驗收:開一個全新 session,不給說明,只問「如果我要新增一種塔,你會改哪些檔案、用什麼順序?」正確答案是先改 docs/gdd.md、再改 data/towers.json、最後才是程式碼。第一次 Claude 直接從 frontend/src/engine/ 開始改——那時 CLAUDE.md 只寫「資料驅動」四個字。補上那條硬規則再問一次,回答就與預期完全一致。這個「新 session 抽考」之後每次改 CLAUDE.md 都做一次,成本兩分鐘。

frontend/CLAUDE.mdbackend/CLAUDE.md 各十幾行,寫該端的約定與「不要做的事」:前端是「HUD 用 DOM,不要在 Canvas 上畫文字 UI」,後端是「domain 層不得 import 任何外部套件」「新 handler 要有對應的 httptest」。Claude Code 依當前工作目錄自動合併載入,兩邊不會互相干擾。

三、settings.json:放行常用、封鎖危險、存檔自動格式化

"permissions": {
  "allow": ["Bash(go test:*)", "Bash(go vet:*)", "Bash(bun run test:*)",
            "Bash(terraform plan:*)", "Bash(make test:*)", "Bash(make sim:*)"],
  "deny":  ["Bash(terraform apply:*)", "Bash(terraform destroy:*)", "Bash(git push:*)",
            "Bash(aws lambda update-function-code:*)", "Bash(aws s3 sync:*)",
            "Read(./.env)", "Read(./.env.*)"]
},
"hooks": {
  "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [
    { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh" } ] } ]
}

allow 放行的是唯讀或可重複執行的指令;deny 封鎖的是不可逆的操作:terraform applydestroy、直接把程式碼推上 Lambda 或 S3、git push,這些只由我親手執行或走 CI。.env 也擋掉——裡面會有 AWS credentials 與帳號 id,Claude 不需要看。沒列在兩邊的(如 git commit)會逐次詢問:常用的不打擾、危險的擋死、其餘的問我。之後幾天它會長成十八條 allow 與十一條 deny,多的是 terraform validatemake smoke*.tfstate 黑名單。

hooks 是今天最實用的設定。PostToolUse 在每次 Edit/Write 之後執行 format.sh:從 stdin 讀 hook 傳來的 JSON、用 jqtool_input.file_path.gogofmt -w.ts.vue.cssbunx prettier --write.tfterraform fmt,最後一律 exit 0——格式化失敗不該擋下編輯。

四、/ultracode 第一版:先有骨架,Day 20 再進化

這是我自己放在 .claude/commands/ultracode.md自訂指令,不是 Claude Code 的官方功能。動機很單純:每次除錯我都重複同一套流程——重現、假設、驗證、修——卻每次都要重講一遍:

---
description: 標準化除錯流程:重現 → 假設 → 驗證 → 最小修復 → 回歸
argument-hint: <bug 描述或 issue 連結>
---
你現在進入 ultracode 模式。目標:$ARGUMENTS

嚴格依序執行,每步完成後簡短回報再進下一步:
1. 先寫一個會失敗的最小測試重現問題,不得先改實作。
2. 列出至少 3 個互斥的假設,說明各自如何驗證。
3. 逐一驗證假設,記錄證據。
4. 針對確認的根因做最小修復,不順手重構。
5. 跑 `make test`,貼出前後對照。
6. 輸出報告:根因、修復、影響範圍、後續建議。

為什麼不寫進 CLAUDE.md?因為 CLAUDE.md 是「每次都生效」的規則,除錯流程只在除錯時需要;寫進去會讓 Claude 連寫新功能都先列三個假設。同樣的道理,.claude/agents/ 今天先放兩個空殼 go-reviewer.mdvue-reviewer.md,只寫 namedescription 與允許的 tools,Day 20 才補審查提示。

第一版很陽春,步驟 3 是序列驗證;Day 20 會加入 subagent 平行驗證與信心分數,把它變成這個系列的差異化賣點。

五、Makefile 與本機開發:為什麼不需要資料庫

GAMEDATA_DIR ?= ../data
dev:
	@$(MAKE) -j2 api web
api:
	cd backend && GAMEDATA_DIR=$(GAMEDATA_DIR) PORT=$(PORT) go run ./cmd/api
web:
	cd frontend && bun run dev
test:
	cd backend && go test ./... && cd ../frontend && bun run test
plan:
	cd infra/terraform/envs/prod && terraform plan

target 名稱與 CLAUDE.md 的「常用指令」一字不差,Claude 讀到 make test 就能直接執行。

這裡沒有 docker-compose.yml,以後也不會有。Day 5 選了 Lambda + DynamoDB 之後,本機跑起來只靠兩個環境變數,這是全系列「本機零依賴」承諾的來源——不過「零依賴」指的是,不是make image 要有 docker:

claude_06_diagram_02

AWS_LAMBDA_FUNCTION_NAME 是 AWS runtime 自己注入的,我們永遠不設它——「本機」的定義就是「這個變數是空的」,不必再發明一個 ENV=local。兩個開關交叉出四象限,長期只走「本機+記憶體」與「Lambda+DynamoDB」兩格,其餘留給除錯。關鍵是圖中央那個匯流點——兩條路拿到的是同一個 router。這條規則今天就寫進 CLAUDE.md,Day 8 蓋 router、Day 13 換倉儲、Day 16 上 Lambda,全都會回頭指這張圖。

小結

今天沒有遊戲邏輯,但建立三層「記憶」:CLAUDE.md 記規則、settings.json 記權限與自動化、/ultracode 記流程。踩雷一則:/init 產出的 CLAUDE.md 太像 README,重寫成祈使句後產出品質明顯不同。可複製要點:CLAUDE.md 寫規則不寫介紹,deny 清單保護不可逆操作,重複流程寫成指令,每天更新一行進度。 明天是第一週的週記,我要做一個實驗回答 Day 2 留下的問題。

今日產出

  • [x] Monorepo 十二項骨架、docs/adr/0004-monorepo.mdPITFALLS.md
  • [x] CLAUDE.md(根、frontend/backend/
  • [x] .claude/settings.json.claude/hooks/format.sh.claude/commands/ultracode.md(v1)
  • [x] Makefiledevtestsimplan

明日預告

Day 07:【週記】Day 1-6 回顧——Claude Design 產出的原型能直接餵給 Claude Code 嗎?


上一篇
Day 05:Vue 3 + Go:技術選型的取捨,為什麼前後端分離是這場戰役的兵法
系列文
奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言