Files
snowy-yang 92b1a628b1 网页端新增 Canvas 二维农场地图
为什么:地块和牲畜原本是响应式卡片列表,信息全但没有"农场"的形状感。
改成一张俯视像素风地图作主视觉:农田按 4 列排成方阵(地块多自动改 6 列),
土壤颜色随水分深浅变化、作物随生长进度放大、成熟发金光、缺水画干裂;
牧场围栏里牲畜随机漫步,待收产出挂金色气泡、没喂食标 💢;
已拥有的建筑在底部建筑带列队;五种天气各有天色,雨/暴雨有雨滴动画。
悬停出提示框,点地块即播种/浇水/收获、点牲畜即喂食/收取——全部复用
既有 openCropPick/act() 流程,卡片面板原样保留作精确操作入口。

实现要点:画布用逻辑坐标绘制、按容器宽 × devicePixelRatio 缩放,
高分屏不糊、窄屏等比缩;命中检测在逻辑坐标上自己算。
服务端零改动——state 里的 plots/animals/buildings 现成够用。

修复一个真机才抓得到的初始化 bug:动画循环首帧同步执行,原顺序
"先绑定后布局"让首帧在未初始化的字段上 TypeError,rAF 注册被一起吞掉,
表现为画布全白但点击命中正常(命中不经过画布)。改为先布局再绑定,
并给 mapStep/mapDraw 加空值护栏。

真浏览器端到端验证:登录、canvas 坐标点击浇水(AP/水分/toast 全对)、
悬停提示、动画循环存活(云位移 5.24px/600ms)、截图目检渲染效果,
errors/console 干净。534 项测试全绿,ruff/pyright 无告警。
2026-09-25 22:24:08 +08:00

9.2 KiB
Raw Permalink Blame History

name, description, agent_created
name description agent_created
simplefarm-verify-and-ship Use when finishing ANY change to the SimpleFarm project (D:/SimpleFarm) — after adding or refactoring features, before declaring the task done or pushing to gitea.4i.hk. Covers the four-step delivery ritual — run the quality gate (ruff format/check + pyright + unittest), verify the change end-to-end on a REAL server with seeded saves, write memory notes, then commit and push. Triggers include "改完了", "验收", "跑测试", "提交推送", "推送到 gitea", or any turn where farm/*.py was edited. Also load it when a test fails after a behaviour change, when the server seems frozen, or when `git push` says "Failed to authenticate user". true

SimpleFarm:改完之后的验收与交付

任何一次改动,走完这四步才算做完。别跳过第 2 步——单元测试全绿不代表功能真的能跑 (本项目已经出现过:测试全绿,但真实服务器上 --pid-file pid.txt 直接崩、 非实时客户端轮询永远读到旧状态)。

第 1 步:质量门禁(唯一可信口径)

uv run ruff format . && uv run ruff check . && uv run pyright
uv run python -m unittest discover -s tests
uv run python tools/bench.py --repeat 5     # 只有碰了热路径才需要

预期:All checks passed! / 0 errors, 0 warnings / OK (skipped=35)。 skipped=35 是没装 httpx 时 TestClient 那批用例的正常跳过,不是失败。 bench 里带调用计数器,计数变红说明轻量路径被改回重路径了。

别用管道:本机 Git Bash 缺 head/tail/grep/ls/mkdir,任何 | 都会整条失败。 查看/删除文件一律写 Python 脚本。

第 2 步:真机端到端验收

单元测试覆盖不到的,是"服务器起来了吗、接口真的返回新字段了吗"。必须起真服务器验。

关键:服务器要用后台任务跑,别用 --daemon。 脱离父进程的守护进程会随工具调用 结束被回收,下一条命令就连不上了(表现为 ConnectionRefusedError)。用 run_in_background: true 跑前台服务器,验完再停掉。

# 1) 预置存档(SQLite,v2 起):想验"买得起"的路径,就先塞一行有钱的档。
#    python -c "
#    from farm.models import GameState
#    from farm.storage import save_state
#    st = GameState(); st.gold = 5000; st.level = 5
#    save_state(st, '.e2e-saves/farm.db', 'boss')"
#    字段缺失一律走默认值;老 JSON 档也可以直接丢进 --save-dir,启动时自动迁移。
# 2) 起服务器(tick 小一点,3 秒一天)
uv run main.py serve --port 8799 --tick 3 --save-dir .e2e-saves
# 3) 用 RemoteFarm 驱动,跑完删掉脚本和 .e2e-saves/

两个必须知道的坑:

  • RemoteFarm.snapshot() 是本地缓存,不发请求。 脚本里写 while True: client.snapshot() 会永远读到同一份旧数据——服务器在推进, 看起来却像卡死。非实时客户端必须自己调 client.refresh() 再读 snapshot()。 TUI/网页走 SSE 所以没这个问题。
  • 端口上可能有别人的服务器。 8765 是默认端口,用户自己(或另一个 checkout) 起的实例可能正在上面跑、里面还有玩家进度。动手前先看 /api/farms 里有没有 不认识的活跃牧场;有就换端口验,别 stop 它。判断一个实例是不是本工作区 的代码,看它的存档目录在不在这个项目里(saves/farm.db 的 mtime 动不动)。
  • 作物会缺水停长。 天气随机消耗水分(晴 1 / 多云 0 / 干旱 2), 等成熟的循环里要盯水分补浇,否则 120 秒也等不到成熟。这也顺带证明了 天气→浇水这条链真的在起作用。

验收脚本该断言什么:新字段真的出现在 /api/state 和 /api/catalog 里、 动作回执带新字段、错误码符合预期(如 nothing_to_sell / locked / unknown_building)。 错误码去 farm/engine.py 里核对,别猜。

动了存档/会话层必须加验一条:只走 HTTP(不连 SSE、0 客户端在线)做一个动作, 等 60 秒自动存档周期过去,直接查 saves/farm.db 那一行有没有更新。 单元测试抓不住 autosave 失效——2026-09-23 就漏过一次:dirty 被推送循环高频清零, autosave 60s 才看一眼,等于永不存盘,全量测试照样绿。

动了 webui 前端必须加验一条:API 正常 ≠ 页面能用(window.prompt 被 sandbox 屏蔽、中文用户名炸 fetch 头,都是 API 测不出来的)。真浏览器验收:

# 1) 起 headless Chrome(后台任务保活;--executable-path 直起会 exit 0 连不上)
uv run python -c "
import subprocess, time
subprocess.Popen([r'C:\Program Files\Google\Chrome\Application\chrome.exe',
  '--headless=new','--remote-debugging-port=9333',
  '--user-data-dir='+__import__('os').environ['TEMP']+r'\abchrome',
  '--no-first-run','about:blank'])
while True: time.sleep(3600)"
# 2) agent-browser 走 CDP 连接(每条命令都要带 --cdp)
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 open http://127.0.0.1:8765/
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 eval "..."   # 页面内点按钮、读 toast

注意:系统 Chromium(Chrome 153+ / Edge)--executable-path 模式起不来, 必须自己起进程再 --cdp 连;find text "登录" 会点到标题而非按钮, 用 eval 精确 document.getElementById('dologin').click()。

Canvas / rAF 动画类改动的额外坑(2026-09-25 踩过):动画循环的第一帧是同步 执行的——必须先跑完布局再绑定循环,否则首帧 TypeError 会把 requestAnimationFrame 的注册一起吞掉,表现为「画布全白但点击命中正常」 (命中是纯数学,不经过画布)。且 headless 截图空白 ≠ 没画:先 ctx.getImageData 读像素确认「画了没」,再放 rAF 计数探针 / 记录动画对象 位移确认「循环活着没」,两连才能定位。

第 3 步:写记忆

  • 当天日志追加到 .workbuddy/memory/YYYY-MM-DD.md(append-only)。
  • 长期约定改 .workbuddy/memory/MEMORY.md(原地编辑)。
  • 用 read_bytes() / write_bytes() 保 LF;同一个文件一条消息里改多处时, 写一个幂等补丁脚本(整文件读入 → 精确匹配替换 → 逐条打印 ALREADY/APPLIED/MISS), 跑完即删。不要在一条消息里对同一文件连发多个 Edit——会被静默覆盖。

第 4 步:提交并推送

git add -A && git commit -F - <<'MSG'
...多行中文 commit message...
MSG
NO_PROXY=gitea.4i.hk no_proxy=gitea.4i.hk GCM_INTERACTIVE=always git push origin main
  • NO_PROXY=gitea.4i.hk(大小写都要)是必需的。 环境代理对它返回 502, 直连秒通。缺了这个会误判成认证问题。

  • push 卡死几分钟不动 = system 级 credential.helper=helper-selector 在 等人点选择框(tasklist 里能看到 git-credential-helper-selector.exe)。 已修:global 里用空 helper 重置列表顶掉它,再依次挂 wincred 和 GCM:

    git config --global --get-all credential.helper
    # 应该是(顺序重要):空串 → wincred → GCM
    # "" 、 !"C:/Users/Administrator/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin/git-credential-wincred.exe"
    # 、 !"C:/Program Files/Git/mingw64/bin/git-credential-manager.exe"
    

    GCM 路径必须带 ! 前缀和引号(!"C:/Program Files/...")—— 否则空格截断成 C:/Program,报 "No such file or directory"。

  • GCM_INTERACTIVE=always 是必需的。 默认(非交互)下 GCM 直接拒绝 Cannot prompt because user interactivity has been disabled, 报错却显示成远端的 Failed to authenticate user,很容易误判成 token 失效。 加上它之后 GCM 会静默续期。

  • 还是失败就加 GCM_TRACE=1 重新推一次,日志会明确说 No existing credentials found 还是拿到了 token。别急着去翻 tea 的 credentials.json.enc(加密的,读不出来)。凭据库里有 git:https://refresh_token.gitea.4i.hk(GCM 的刷新令牌)。

  • 验证推送成功看 git ls-remote origin,不要看 git status -sb—— 沙箱会丢掉 .git/refs/remotes/** 新建目录的写入,status 会谎报 ## main...origin/main [gone]。ls-remote 的 sha 与 git rev-parse HEAD 一致就是推上去了,[gone] 是无害的本地缓存问题。

  • 被杀掉的 push 会留下 git.exe / git-credential-manager.exe 僵尸进程, 占住 GCM 的单例锁让后续 push 排队挂死。推送反复挂时先 taskkill /F /IM git.exe 等清场再试。

别踩的坑(本机特有)

  • os.makedirs(os.path.dirname(path)) 在 path 只有文件名时会崩(dirname 是空串)。 用 farm/server/daemon.py 的 ensure_parent_dir()。
  • 写文件必须显式保 LF(write_bytes 或 newline="\n"),别让 \n 被转成 \r\n。
  • 访问本机 HTTP 走 farm/net.py 的 open_url(),环境里有 HTTP_PROXY 但没有 no_proxy,直连会被代理拦成 502。
  • 端口自检用"能不能连上"判断,别用 bind;uvicorn 设了 SO_REUSEADDR, Windows 上第二个实例能绑上已被监听的端口并抢走新连接。