snowy-yang
|
4bc7729715
|
地图手感打磨:动作粒子 + 昼夜色温 + 成熟呼吸金光
为什么:地图功能已全通但缺"游戏手感"——点击有反馈(toast)却看不到
画面上的动静,时间流动也只在倒计时数字里。这一轮全部补上。
- 动作粒子特效(webui):act() 成功后在目标位置喷粒子 + 上浮表情小字
(浇水💧水花、播种🌱扬土、收获/收取✨金点、喂食🍽)。粒子受重力、
按寿命衰减,失败动作不触发(api() 返回 null 时 mapJuice 不执行)。
- 昼夜色温:一天的进度(1 - seconds_to_tick/tick_seconds)映射成
清晨淡金 → 正午不叠色 → 黄昏橙 → 入夜靛紫的半透明覆盖层,
让"40 分钟一天"在画面光色上也流动。tick_seconds 完整快照里有、
brief 里没有,靠 refreshFromBrief 的合并语义保留。
- 成熟作物金光改呼吸式(shadowBlur 随 sin 起伏),悬停可交互对象时
指针变手型。
验证:532 项测试全绿、ruff/pyright 干净;真机 headless Chrome 实测
粒子注入 23 枚 2 秒内衰减归零、失败动作不产生特效、dayTint 三时段
数值正确、截图目检清晨色调正常、控制台零报错。
|
2026-09-25 23:38:40 +08:00 |
|
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 |
|
snowy-yang
|
cb4c1e7800
|
feat(engine): 四座自动化设施——喷灌 / 收割机 / 售货机 / 磨坊
为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食
的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线:
浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口
→ 售货机趁高价清库存,人不在也能持续产出。
- config.BUILDINGS 注册 4 座:喷灌系统 2800 金(全量浇水)、
自动收割机 900 金 ×3(每天各收 2 块熟地,刻意不给经验——机器收的
不算劳动,给手动玩法留价值)、自动售货机 1200 金(市价 > 基准价才
出手、每天上限 5 份、越贵越先卖)、饲料磨坊 500 金(1 份最便宜的
作物磨 3 份饲料)。
- engine._run_buildings 扩成六段流水线,顺序即依赖:磨坊排在喂食器前,
早上磨的饲料当天就能喂上;收割机遇满仓停摆、作物留地(同喂食器
缺料停摆哲学——自动系统绝不硬来,更绝不自动花钱)。
- webui / build 动作 / catalog 全部由 config.BUILDINGS 驱动,零改动。
- 测试 +6(喷灌全量浇水、收割入库且无经验、满仓停摆留作物、磨坊挑
最便宜、磨坊→喂食器同日链路、售货机阈值与配额)。修复断言时确认
两个既有行为:引擎自带 START_FEED=5;advance_day 末尾会把动物 fed
复位("喂到了"的证据是 pending 累计而非 fed 本身)。
- 门禁:534 项 OK(skip 39)、ruff/pyright 全绿。
|
2026-09-25 21:05:27 +08:00 |
|
snowy-yang
|
698a5e497a
|
feat: 用户名登录——一账号一个牧场,登录后改 URL 也进不了别人的档
多用户之前是「知道牧场名就能玩」:?farm=boss 直接进别人家里。
现在网页端用用户名登录(用户选择:不设密码、一账号一个牧场,
用户名即身份——互信小圈子模型,防的是随手改 URL,不是蓄意攻击)。
* 新增 farm/server/auth.py:HMAC-SHA256 签名的 HttpOnly Cookie
(sf_auth,30 天,恒时比较防篡改)。密钥存 <save_dir>/secret.key,
重启不掉线,也不用建表存 token。
* app.py 三个认 ?farm= 的地方(session_of / SSE / POST /api/farms)
统一规则:有 Cookie 强制本人的牧场(?farm=/X-Farm-Id 被忽略),
无 Cookie 保持旧语义——bot/TUI/CLI 脚本零改动照常工作。
* 新路由:POST /api/login(全局 token 照常把它挡在门外)、
POST /api/logout、GET /api/me(刻意不 guard:首屏只问"登录没")。
* 网页:移除牧场名输入框,改为登录遮罩 + 用户徽章 + 退出按钮;
首屏先 whoami() 再决定连接还是弹登录。
测试 +13:Cookie 伪造/过期/跨密钥/垃圾串/非法用户名/密钥跨重启复用/
session_of 优先级(starlette Request 用 scope dict 直接构造,不需要
httpx)9 项纯逻辑档 + 4 项 TestClient 档。528 项 OK(skip 39)。
真机验收 16/16:登录/HttpOnly/?farm= 被忽略/动作落自己牧场/篡改
退回未登录/退出/非法名 400;跨重启 Cookie 仍有效且进度保留。
|
2026-09-23 21:20:25 +08:00 |
|
snowy-yang
|
d1692d7b10
|
docs: 记忆与验收技能跟上——存档改动必须真机验自动落盘
单元测试抓不住 autosave 失效(dirty 被高频清零这件事测试里复现
不了真机时序),所以技能里加一条:动了存档/会话层,必须 0 客户端
在线做一个动作、等 60s、直接查库那一行有没有更新。
|
2026-09-23 20:54:13 +08:00 |
|
snowy-yang
|
cb292f9f53
|
docs: 验收技能跟上 SQLite 存档——预置存档改塞库行,加了别动共享端口的提醒
|
2026-09-19 19:11:27 +08:00 |
|
snowy-yang
|
bc3ae96a38
|
refactor: 存档层从「一牧场一个 JSON」换成 SQLite(单库单表)
`saves/farm.db` 一张 farms 表,farm_id 主键,**一个牧场一行**。
接口签名不变(save_state / load_state / delete_save 只多了 farm_id 参数),
调用方只改了路径语义。
设计取舍:标量列 + 整份 payload。
* day/gold/level/ap/weather/forecast/net_worth 单独成列——外部工具或 AI
一条 SELECT 就能排榜、画曲线,不必先把每份存档读进内存;
* 完整状态仍整份存 payload JSON:游戏数据隔三差五长新字段,规范化成
几十张表意味着每加字段写一次迁移,而存档只做整体读、整体写,
所以结构化列管可查询,完整性交给 JSON——加字段不用改表结构。
崩溃安全升级:写入包在事务里。原来的"临时文件 + os.replace"能挡进程
崩溃,挡不住写到一半掉电;SQLite 的日志能。读取失败照旧降级开新档。
向后兼容:SAVE_VERSION=2 / MIN_SAVE_VERSION=1 不变。老 JSON 存档由
migrate_json_saves() 在服务器启动时自动导入:同名已存在就跳过(宁可不
迁不覆盖),导入成功原文件改名 *.json.migrated 留底,先写库再改名
保证幂等;坏档一律跳过——迁移不许有能力阻止服务器启动。
异常边界:StorageError = sqlite3.Error,刻意不是 OSError 子类——
调用方原来的 except OSError 全部跟着改,否则写盘失败会穿透出去。
性能实测:PRAGMA journal_mode=WAL 是持久属性,建库时设一次就够,
每次连接重设白花 2.2ms;save_state 约 6.9ms/次,大头是 synchronous=FULL
的 fsync——不省,存档的价值就是不丢档。bench 热路径无回归。
顺手修一个真 bug:connect() 建表失败时连接会泄漏,Windows 上句柄没关
就删不掉文件,而调用方正是靠"读失败→开新档"容错的。补 close。
测试:StorageTestCase 重写(往返/版本/损坏降级/多牧场隔离/迁移),
test_server 断言 *.json 的用例改查库。513 项全过,ruff/pyright 全绿。
端到端:预置 v1/v2 老档起真服务器验 22 项——自动迁移、字段还原、
list_farms() 排榜、重启后进度分毫不差,全过。
|
2026-09-19 19:04:30 +08:00 |
|
snowy-yang
|
6268283fc6
|
chore: 存一个「改完怎么验收怎么交付」的技能,省得下次重新踩
本次踩到两个纯靠猜出不来的坑,都写进技能里了:
* `RemoteFarm.snapshot()` 是本地缓存不发请求,非实时脚本轮询会一直读到同一份
旧数据——服务器在推进却看起来像卡死,排查花了十几分钟。
* `git push` 报远端 `Failed to authenticate user` 其实是本地 GCM 拒绝交互
(`Cannot prompt because user interactivity has been disabled`),
加 `GCM_INTERACTIVE=always` 就好;再不行 `GCM_TRACE=1` 看日志。
验证推送成功要看 `git ls-remote`,`git status -sb` 的 `[gone]` 是沙箱
丢 `.git/refs/remotes/**` 写入导致的假象。
技能放在 .workbuddy/skills/simplefarm-verify-and-ship/,覆盖四步交付流程:
门禁 → 真机端到端验收(预置存档 + 后台任务起服务器)→ 写记忆 → 提交推送。
|
2026-09-19 17:06:15 +08:00 |
|
snowy-yang
|
cf24cb43f1
|
feat: 加天气 / 谷仓 / 建筑三套系统,收获改为入库 + 择时卖出
动手前先修掉了两处「死机制」,新增的三个系统各自激活一个:
* 天气:小麦 2 天成熟、初始水分 2、每天只掉 1 点,等于永远不需要浇水。
现在水分消耗由天气决定(晴 1 / 多云 0 / 干旱 2,雨天反补 3),而
`state.forecast` 把明天的天气提前告诉你——「今晚浇不浇」成了能算的决策。
5 种天气、权重合计 100、同种不连出;暴风雨会毁未熟作物,温室免疫。
* 谷仓:市场波动原本纯装饰(收获当场按市价变现,没有择时空间)。现在收获与
收取只进 `state.inventory`,靠新增的 `sell` 换金币,价格波动才真正参与决策。
仓满时 `_store()` 抛 `granary_full` 且不改状态,作物原样留在地里。
* 建筑:新增 `build`,买的是「以后不用天天手动做」——水井自动浇干地、
喂食器自动喂食(只吃库存饲料,绝不自动花钱)、温室、谷仓扩容。
每天在 `advance_day` 里先于生长执行,同类越造越贵(×1.6)。
存档升到 `SAVE_VERSION=2`,配 `MIN_SAVE_VERSION=1` 向后兼容,旧档缺的新字段
一律走默认值,不作废玩家手里的档。`advance_day(weather=...)` 是留给测试的
确定性入口。
顺带修两个真 bug:
* `daemon.py` 三处 `makedirs(os.path.dirname(path))`:`--pid-file pid.txt`
这种只给文件名的用法会让 dirname 变成空串,启动阶段直接崩。抽出
`ensure_parent_dir()` 兜底。
* `RemoteFarm.snapshot()` 是本地缓存,非实时客户端轮询会一直读到旧数据,
看起来像服务器卡死。补 docstring 指明要自己调 `refresh()`。
质量门禁:ruff format/check 全绿、pyright 0 错误、503 项测试 OK(skip 35)、
bench 无回归(brief 19.5µs / snapshot 48.5µs / POST full=1 400µs)。
另起真实服务器(--tick 3 + 预置存档)跑了 38 项端到端验收,全过。
|
2026-09-19 17:02:33 +08:00 |
|
snowy-yang
|
a92c29ead2
|
docs: 记录发布到 gitea.4i.hk 的仓库地址与推送认证方式
远端仓库:https://gitea.4i.hk/hhhge/SimpleFarm(public,main)。
关键坑:非交互 shell 里必须 GCM_INTERACTIVE=always,否则 GCM 会因为「用户交互被禁用」
直接拒绝,git 报 could not read Username / Failed to authenticate user,看起来像认证配错了。
另记:tea 的 OAuth token 能建仓、能调 API、能加 SSH 公钥,但不能签发访问令牌
(POST /users/{u}/tokens 需要 basic auth)。
|
2026-09-19 13:33:48 +08:00 |
|
snowy-yang
|
450655bebf
|
chore: 补记本机两个仓库/环境坑(Git Bash 缺 cat、.gitignore 的 /_* 规则)
提交多行 message 时用 git commit -F <文件>,不要用 $(cat <<EOF) —— 本机没装 cat,
git 会因此认为消息为空并 abort。
根目录以 _ 开头的文件被 .gitignore 的 /_* 静默忽略:临时脚本因此不会污染 git status,
但放错名字的正经文件也会被吞掉。
|
2026-09-19 12:56:07 +08:00 |
|
snowy-yang
|
f8e093c98f
|
feat: 实时通道改 SSE、服务器换 uvicorn+zttp、节奏改 40 分钟一天,并重建测试与优化热路径
三件事凑成一次不兼容变更:WebSocket 通道、Hypercorn 服务器、「手动推进一天」都消失了。
实时通道:WebSocket -> SSE
- hub 改为 SSE 订阅池,GET /api/stream 单向推送 welcome/state/day/notice/bye,
250ms 节流 + 15s 心跳注释;写操作一律走 HTTP POST /api/actions,不再有 WS 回执。
- 停机三处一起修才干净:close_all() 发 bye + timeout_graceful_shutdown 兜底 +
客户端 stop() 直接 shutdown 底层 socket(光 close() 会等读线程的锁,最长卡到下次心跳)。
- 网页版改 EventSource + fetch;tools/ws_demo.py 换成 tools/sse_demo.py。
服务器:Hypercorn -> uvicorn + zttp 的 HTTP/2
- 同一端口同时说 HTTP/1.1 与 HTTP/2。uvicorn 0.53 不再内置 h2,必须装 zttp 并显式
http="zttp",否则 Config.load() 直接 raise。
- 信号接管改成子类覆盖 Server.handle_exit(0.53 没有 install_signal_handlers 可覆盖),
不调 super()、不写 _captured_signals,退出时不重放信号;第二次信号强制退出。
- 入口启动前用 daemon.port_busy() 自检端口:Windows 上 SO_REUSEADDR 会让第二个实例
「绑上」已监听端口并抢走新连接,所以判断「能不能连上」而不是「能不能 bind」。
- 修掉 clear_pid_file 的 Windows 真 bug:文件还开着就 os.remove 会失败且被
except OSError 吞掉,导致每次正常退出都留 stale pid 文件。
节奏:一天 15 秒 -> 40 分钟,并整体移除「休息」
- DEFAULT_TICK_SECONDS=2400,MAX_TICK_SECONDS 放宽到 86400(旧上限会把 40 分钟夹回去)。
- engine.sleep / SLEEP_COOLDOWN / /api/sleep / TUI 的 [N]休息 / 网页版按钮全部删掉,
时间只能自然流动;机器人改为 wait_for_next_day(timeout),CLI 加 bot --wait。
- 时长显示统一走 models.format_duration(),不再出现 2399.6s 这种写法。
键盘:修复方向键/空格/Tab/ESC 全部静默失效
- keys.py 的具名键统一小写并成为唯一来源,tui.py 一律引用 keys.XXX 而不手写字符串
(历史 bug 就是 Windows 侧产大写 UP、TUI 里写小写 up,字母键正常所以很难发现)。
- 顺带修 POSIX 路径一次 os.read 只取首字节、其余按键静默丢弃的老毛病。
测试:重建 500 项
- test_engine 110 / test_actions 35 / test_server 131(skip 35) / test_tui_keys 75 /
test_bot 36 / test_client 113。未安装的第三方依赖用 skipUnless 自动跳过,其余照跑,
所以是全绿而不是靠跳过伪装。
- 被测试逼出两处真隐患并修掉:RemoteFarm._normalize() 必须「先补 scheme 再削尾斜杠」
(顺序反了畸形端点会溜到系统代理、报一个和真实原因无关的 502);StateView 空视图要
预置 plots/animals,否则连不上服务器时读 state.plots 直接 AttributeError。
优化:先量后改,依据与前后数字都在 tools/bench.py
- 新增 engine.brief_source():brief/summary/动作回执只要十几个标量,不再为它们序列化
整局;to_dict 里 net_worth 只算一次。brief -76%、summary -77%、leaderboard(32) -78%、
GET /api/world -31%、POST /api/actions?full=1 -16%。
- 拆掉三个巨型函数:create_app 475->32 行(_Services + _register_*)、
build_parser 与 available_actions 分组、__main__.main 160->7 行。
- 消除重复:等级门槛 5 处 -> _is_locked/_require_level/_newly_unlocked;hub 的
broadcast/broadcast_all/close_all -> _push_many;daemon.probe/world -> _get_json;
main.py 的 _add_endpoint_args/_leaderboard_lines。
- tools/bench.py 作为长期性能守卫(含调用计数器),谁把轻量路径改回 snapshot() 会立刻报警。
门禁:ruff format/check 全绿、pyright 0 错误、500 项测试 OK(skip 35)。
|
2026-09-19 12:55:45 +08:00 |
|
snowy-yang
|
47dd818360
|
docs: 记录规则生效的探针反证过程
复核 ruff/pyright 规则是否真的被加载:排除所有可能覆盖 pyproject 的配置文件,并用故意违规的探针文件确认精确命中。
新知识:X | None 的规则码在新版 ruff 是 UP045 而非 UP007;SIM105 对 try 体含 return 的情况有意不报,所以'没报出来'不等于'规则没启用'。
|
2026-09-18 21:32:46 +08:00 |
|
snowy-yang
|
52004571c8
|
docs: 记录工程化改造(uv + ruff + pyright)的决策与踩过的坑
日志补记下半场:三件套的落地细节、行宽为何定 100、PTH 为何不开、pyright 两个关键开关;新增三条可复用环境坑(write_text 会把 LF 变 CRLF、同文件多次 Edit 互相覆盖、git status 与 git diff 不一致的判别)。
项目长期约定同步更新:运行方式改为 uv 单一可信来源,新增代码风格约定与质量门禁命令。
|
2026-09-18 21:28:20 +08:00 |
|
snowy-yang
|
a7725f8812
|
chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前,
先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。
此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
|
2026-09-18 21:12:40 +08:00 |
|