# SimpleFarm 项目长期约定 ## 是什么 终端农场经营游戏 + 小型多人游戏服务器(v2.0)。游戏跑在服务端进程里, TUI 是纯客户端,同时开放 HTTP + WebSocket 接口给 AI/脚本调用(服务端不内置 AI)。 ## 运行方式(v2.0 起由 uv 统一管理) - **单一可信来源是 `pyproject.toml`**,`.venv/` 由 uv 创建;`uv.lock` **要提交**(可复现构建依据)。 加/删依赖走 `uv add` / `uv remove`,不要手工编辑依赖表,也不要再列 `pip install`。 - 所有命令带 `uv run ` 前缀:`uv run main.py serve --daemon`,网页 ,文档 /docs - 玩:`uv run main.py play --farm 名字`;单机:`uv run main.py solo` - AI/脚本入口:`main.py actions | do <动作> '' | batch | state --json | watch | bot` - 测试:`uv run python -m unittest discover -s tests`(97 项) - 质量门禁(改动后都应跑一遍): `uv run ruff check .` / `uv run ruff format .` / `uv run pyright` - 不用 uv 的人:`requirements.txt` 是 `uv export` 的产物,只读、不要手工改。 README 与 main.py 顶部保留了一段"没装 uv?把 `uv run ` 去掉即可"的兜底说明,**别删**。 ## 架构约定(改动时必须遵守) - **分层**:`farm.engine` 只管规则,不认识网络/终端;`farm.tui` 不认识 socket; `farm/server/*` 不认识终端。新增玩法动作只改 `farm/server/actions.py` 一张表。 - **接口同构**:`farm.client.RemoteFarm` 必须与 `FarmEngine` 同名同语义, 这样 `tui.py` 不需要区分本地/远程。加动作时两边都要补。 - **多牧场**:一个 farm = 一个玩家 = 一份 `saves/<名字>.json` + 一个引擎 + 独立时钟; 时钟由**一个** `FarmClock` 线程统一驱动,不要给每个牧场开线程。 - **存档类动作**:`save` / `reset` 必须由会话(FarmSession)处理, 不能直接落到 `engine.save()`(会写默认路径、覆盖别人的档)。 - **实时通道**:WebSocket(`/ws`)是唯一实时通道,推送节流 250ms,消息带自增 `seq`。 写操作优先走 WS(带 `id` 回执与 `snapshot`),只有 ws_closed/timeout/network 才退回 HTTP 重试;**业务错误必须原样抛出,不能重试**。 ## 环境禁忌(本机特有) - Bash 的 coreutils 残缺(`ls`/`head`/`tail`/`grep` 缺失),**不要用管道**,会整条命令失败; 查看/删除文件一律写 Python 脚本。 - 环境设了 `HTTP_PROXY` 但没有 `no_proxy`:任何本机 HTTP 访问都要走 `farm/net.py` 的 `open_url()`(回环直连),否则会拿到 502 并误判为"服务器没起来"。 - Windows 下守护进程的 pid 必须由**服务进程自己**写(见 `farm/server/__main__.py`), 父进程的 `Popen().pid` 不可信。 - **写文件必须指定行尾**:Windows 上 `pathlib.write_text()` 会把 `\n` 转成 `\r\n`, 破坏本仓库"统一 LF"的约定(`.gitattributes` 已声明 `eol=lf`)。 用脚本改文件时一律 `read_bytes()/write_bytes()`,或 `write_text(..., newline="\n")`。 - **同一文件在一条消息里多次 Edit 会被静默覆盖**(只落地一部分,随后报出"幽灵"错误)。 批量改同一文件时改用**幂等 Python 补丁脚本**:整文件读入 → 精确匹配替换 → 逐条打印 `ALREADY/APPLIED/MISS`,跑完即删。判断"已应用"要 `if new in text`, 不要比对两行文本(会出现 new 是 old 子串的假阳性)。 - `git status` 报 M 但 `git diff` 为空,是索引 stat 缓存过期,不是真差异; `git add -A`(内容相同时等于无操作)即恢复干净。 ## 代码风格约定(ruff / pyright 已固化在 pyproject.toml) - 行宽 **100**;字符串统一双引号;`isort` 把 `farm` 视为第一方。 - 类型标注用 PEP 585/604 写法(`dict[str, int]` / `X | None`),不要 `Dict` / `Optional`。 - 吞异常要显式:`contextlib.suppress(...)` 优先于 `try/except/pass`。 - 异常链:转换后重抛用 `raise ... from exc`;有意隐藏原始上下文用 `from None`(**不要省略**,会触发 B904)。 - 路径保持 `str`(不要为了 lint 而转 `Path`);`farm/__init__.py` 的惰性 `__all__` 导出是刻意设计。 - 改完代码的标准动作:`uv run ruff format . && uv run ruff check . && uv run pyright && uv run python -m unittest discover -s tests` ## 相关文档 - 项目入口文档:`README.md`(接口表、WS 协议、边界处理、设计取舍全在里面) - 当天工作记录:`.workbuddy/memory/2026-09-18.md`