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
|
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
|
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
|
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
|
c414db4217
|
fix: 用户可见的操作提示统一为 uv run 前缀
项目由 uv 管理,依赖装在 .venv。此前各处的提示语仍写作 `python main.py ...`,用户若在未激活虚拟环境时照抄,会因缺少 fastapi 而失败——这是正确性问题,不只是措辞。
涉及:main.py(12 行,含 prog 与各条 hint)、ai_player.py(5 行)、farm/client.py、farm/server/__main__.py(3 行,含 prog)、farm/server/__init__.py、farm/server/daemon.py、README(4 处示例与目录树注释)。
刻意保留 main.py:26 与 README:49 的「没装 uv?」兜底说明,那里 `python main.py` 是正确写法。
验收:ruff 全通过 / format 26 文件无改动 / pyright 0 错误 / 97 项测试通过。
|
2026-09-18 21:25:52 +08:00 |
|
snowy-yang
|
d7a7107b0a
|
chore: 引入 pyright 类型检查清零 + README 工程化章节
pyright(basic 模式)从 15 errors / 10 warnings 降到 0/0:
- pyproject: pythonPlatform="All"(termios/msvcrt 双平台分支不再误报),
reportUnsupportedDunderAll=false(__init__ 惰性导出属正常写法)
- keys.py: 捕获 sys.stdin 局部变量 + 提前返回,消除 fileno() 的 None 收窄告警
- server/app.py: lifespan 改用 @asynccontextmanager 并标注 AsyncIterator[None]
- server/actions.py: 显式标注 result: dict[str, Any],消除 isinstance 冗余报错
- server/__main__.py: 用 importlib.util.find_spec 探测 websockets
- tests/test_server.py: 同上,uvicorn 探测改为 find_spec
- tools/preview_tui.py: TYPE_CHECKING 下引入 RemoteFarm,修正返回类型标注
- farm/tui.py: 取消未使用变量,统一 round/取整写法
其他:
- README: 新增第 11 节「工程化:uv + ruff + pyright」,快速开始全部改用
uv sync / uv run;原第 11 节起顺延为 12
- requirements.txt: 由 uv export --no-dev --no-hashes 重新生成(锁定版本)
- 清理临时日志文件
验收:ruff check 全通过 / ruff format 26 文件无改动 / pyright 0 错误 /
unittest 97 项全部通过
|
2026-09-18 21:24:30 +08:00 |
|
snowy-yang
|
a7725f8812
|
chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前,
先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。
此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
|
2026-09-18 21:12:40 +08:00 |
|