为什么:snapshot() 同时压在 SSE 每 250ms 推送和 full=1 动作回执两条
高频路径上,而它的成本大头是重复求值——profile 显示单次快照
1,128,001 次函数调用里近半是属性链重复计算同一份配置数据。
- Plot.to_dict:grow_days 原来经 4 个属性各求值一遍(本体 + days_left
+ is_ripe + stage),16 块地就是 64 次配置表查询。改为算一次局部
复用;stage 的实现体拆到 _stage(grow),规则本体仍只有一份。
- Animal.to_dict:info 配置表原来查 4 次、cycle int() 两遍,改取一次。
- GameState.to_dict:weather_info 3 键 2 次查询;granary_free 原来把
granary_capacity 和 storage_used 整个重算,改算一轮复用。
行为零变化(规则本体全在原属性里,532 项测试全绿)。
基准(tools/bench.py --repeat 5,最坏形态 16 地 + 8 牲畜):
场景 优化前 优化后
engine.snapshot() 50.1 µs 44.2 µs -12%
session.snapshot() 51.0 µs 41.9 µs -18%
build_state_message() 50.9 µs 43.3 µs -15%
engine.available_actions 52.7 µs 48.7 µs -8%
单次 snapshot 函数调用数 1,128,001 → 588,001(-48%),本次数字已记入
bench.py docstring 作长期参考。重复计数器全 OK(to_dict/snapshot/
net_worth 每请求仍各 1 次,无回归)。
82 lines
5.7 KiB
Markdown
82 lines
5.7 KiB
Markdown
|
||
## 2026-09-25 四座自动化设施
|
||
|
||
- 用户要「添加自动化设施」,AskUserQuestion 给了 4 个候选,用户全选:
|
||
喷灌系统 / 自动收割机 / 自动售货机 / 饲料磨坊。
|
||
- 设计上沿用既有建筑哲学:**只替代行动点动作,绝不自动花钱**。
|
||
`_run_buildings` 扩成六段流水线(顺序即依赖):
|
||
水井 → 喷灌 → 收割机 → 磨坊 → 喂食器 → 售货机。
|
||
磨坊刻意排在喂食器前——早上磨的饲料当天就能喂上;
|
||
收割机遇满仓停摆、作物留地(同喂食器缺料停摆)。
|
||
- 数值:喷灌 2800/限1/全量浇水;收割机 900/限3/每天各收 2 块熟地、
|
||
**刻意不给经验**(机器收的不算劳动,给手动玩法留价值);
|
||
售货机 1200/限1/市价>基准价才卖、每天上限 5 份、越贵越先卖
|
||
(_earn 复用 sell 的入账逻辑);磨坊 500/限1/1 份最便宜的作物磨 3 份饲料
|
||
(FEED_UNIT_PRICE=6,小麦 14 磨 18 金的料,微赚但吃掉作物)。
|
||
- 架构红利:config.BUILDINGS 驱动一切(build 动作、catalog、网页建筑区、
|
||
可用动作推荐),新增建筑 engine 之外零改动。
|
||
- 测试 +6,途中确认两个既有行为:引擎自带 START_FEED=5(feed 断言要先清零);
|
||
advance_day 牲畜阶段末尾会把 fed 复位(「喂到了」的证据是 pending 累计,
|
||
鸡 cycle=1 当天喂到就 pending=1)。
|
||
- 门禁 534 项 OK(skip 39)、ruff/pyright 全绿;提交 cb4c1e7 已推送。
|
||
服务器已重启到新代码,catalog 8 座建筑齐全。
|
||
|
||
## 2026-09-25 Canvas 二维农场地图
|
||
|
||
- 用户问「能不能做成二维的」,AskUserQuestion 三选一:CSS 农田视图 /
|
||
Canvas 像素农场 / 网页+TUI 都改,用户选 **Canvas 像素农场**。
|
||
- 实现:全在 webui.py(服务端零改动,state 里现成的 plots/animals/buildings
|
||
够用)。页首插一张全宽 canvas 卡片:逻辑坐标绘制(456×152 起,随地块数变)、
|
||
实际像素 = 容器宽 × dpr,命中检测自己算(逻辑坐标 = clientX × MAP.w/rect.width)。
|
||
农田 4 列方阵(>12 块改 6 列)土壤颜色随水分插值、作物 emoji 随生长放大、
|
||
成熟金光、干裂画裂纹;牧场围栏里牲畜随机漫步回弹、待收产物金泡、没喂 💢;
|
||
底部建筑带(owned>0 才画);天气五種各有天色+雨滴/云/烈日。
|
||
交互复用 openCropPick/act():空地点击弹选种、熟地收获、缺水浇水、
|
||
牲畜收取优先于喂食。下方卡片面板全保留(地图负责沉浸,卡片负责精确)。
|
||
- **踩坑:rAF 循环首帧同步执行**。updateMap 原顺序 bind→layout,首帧
|
||
`f.ts` 在 MAP.field=null 上 TypeError,把 requestAnimationFrame 注册一起吞掉,
|
||
表现是「画布全白但点击命中正常」(命中是纯数学不经过画布)。
|
||
修复:layout→sync→bind→fit 顺序 + mapStep/mapDraw 空值护栏。
|
||
排查手法:截图空白先别信,`getImageData` 读像素 + 探针 rAF 计数 + 云位移
|
||
三连定位「画了没」和「循环活着没」。
|
||
- 真机验收:登录、canvas 点击浇水(AP 4→3、水分 2→3、toast)、
|
||
悬停 tooltip、截图目检全过;errors/console 干净。534 项测试全绿。
|
||
|
||
## 2026-09-25 移除行动点系统
|
||
|
||
- 用户拍板:「谁家浇水还要体力啊 体力删掉 都已经有时间了」——时间
|
||
(40 分钟一天、只能自然流动)本身就是唯一节奏阀门,AP 是重复惩罚。
|
||
- 连根拔的触点:config(START_AP/AP_COST)、models(ap/max_ap 字段,旧档
|
||
payload 的 ap 键 from_dict 自然忽略)、engine(_spend_ap + 8 调用点 +
|
||
每日恢复)、bot(AP 分支)、client/TUI/main/README/规则文本。
|
||
- **SQLite 老库迁移**:ap 是标量 NOT NULL 列,connect() 里
|
||
`PRAGMA table_info` 发现即 `ALTER TABLE farms DROP COLUMN ap`
|
||
(Python 3.13 的 SQLite ≥3.35 支持)。实测 saves/farm.db 自动迁完。
|
||
- 网页端:删"行动点"统计卡;"下一天"倒计时改**客户端本地走秒**
|
||
(render 记 tickLeft/tickStamp 基点,1s 定时器补 SSE 推送间隙);
|
||
字体栈补 MiSans/HarmonyOS Sans/PingFang、正文 15px、数字
|
||
tabular-nums、canvas 小字加粗。
|
||
- 测试 -2(AP 专属用例没了)改 5,532 项全绿;提交 15d290a 已推送。
|
||
真机:老库迁移 ✅、无行动点卡 ✅、倒计时 39:04→39:02 走秒 ✅。
|
||
|
||
## 2026-09-25 地图手感打磨(4bc7729)
|
||
|
||
- **动作粒子**:MAP.fx 数组,act() 成功后 mapJuice(action, params)——
|
||
地块号/牲畜 id 反查画布坐标喷粒子(重力+寿命衰减)+ 上浮表情浮字
|
||
(浇水💧播种🌱收获✨喂食🍽)。api() 失败返回 null 天然不触发。
|
||
- **昼夜色温**:dayTint(prog) 按一天进度叠半透明层(清晨淡金→正午无→
|
||
黄昏橙→入夜靛紫)。tick_seconds 只在完整快照里、brief 没有,
|
||
靠 refreshFromBrief 的 Object.assign 合并语义存活,兜底 2400。
|
||
- 成熟金光改呼吸式(sin 起伏 shadowBlur);悬停可交互对象指针变手型。
|
||
- 验收注意:粒子视觉链路可用 mapJuice 直接注入测(真动作要看地块状态
|
||
凑巧——全满时浇水会走失败路径,正好验证"失败不触发")。
|
||
- **沙箱进程回收边界(重要)**:WorkBuddy 会话里启动的进程都会被环境
|
||
回收——后台任务(跑 15m~1h 后静默死、日志无错误)、甚至
|
||
`--daemon` 的 DETACHED_PROCESS 守护进程(沙箱 Job 对象 kill-on-close
|
||
连 breakaway 进程一起杀)都活不长。**长期挂机必须让用户自己在系统
|
||
终端跑** `uv run main.py serve`(或 `--daemon`),那才是真正独立的进程。
|
||
会话内只做临时拉起 + 自动存档(60s 周期)兜底损失。
|
||
|
||
|
||
|