Files
SimpleFarm/pyproject.toml
T
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

129 lines
5.9 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SimpleFarm —— 项目元数据与工具配置
#
# 这个文件是**唯一真源**:依赖、Python 版本、lint 规则、类型检查规则全都写在这里。
# 日常操作全部交给 uv,不需要手动激活虚拟环境:
#
# uv sync # 按 uv.lock 建好 .venv(含开发依赖)
# uv run main.py serve --daemon
# uv run ruff check . # 静态检查
# uv run ruff format . # 格式化
# uv run pyright # 类型检查
#
# 为什么写 package = false:这是一个**应用**,不是要发布到 PyPI 的库。
# 关掉打包之后,uv 只负责环境与依赖,不引入任何构建后端(hatchling/setuptools 都不需要),
# `python main.py` 与 `uv run main.py` 行为完全一致。
[project]
name = "simplefarm"
version = "2.0.0"
description = "终端农场经营游戏 + 小型多人游戏服务器:人玩 TUI,AI 走 HTTP + SSE 接口"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
keywords = ["game", "tui", "cli", "fastapi", "sse", "llm", "agent"]
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Topic :: Games/Entertainment :: Simulation",
]
# 运行依赖 —— 只有**服务端**需要。
# 终端界面、命令行客户端、规则机器人全部只用标准库,
# 所以 `uv run main.py solo` 这类离线玩法不需要装下面这两个。
dependencies = [
"fastapi>=0.110",
# ASGI 服务器本体。[standard] 带 httptools(更快的 HTTP/1.1 解析)、watchfiles 等;
# 它还会捎带一个 websockets——本项目只用 SSE、不用 WebSocket,那是 uvicorn 的
# 可选依赖,别误会成我们在用。
"uvicorn[standard]>=0.53.0",
# HTTP/2 的开关。uvicorn 自己不再内置 h2,它的 HTTP/2 实现建立在 zttp 上
# (uvicorn/protocols/http/zttp_h2_impl.py):配 http="zttp" + http2=True 之后,
# AutoZttpProtocol 会在**同一个端口**按 ALPN(TLS)或 h2c 前导字节(明文)
# 自动分流 HTTP/1.1 与 HTTP/2,应用代码一行都不用改。
"zttp>=0.0.33",
]
# 开发依赖(`uv sync` 默认就会装,用 `uv sync --no-dev` 可以跳过)。
[dependency-groups]
dev = [
"ruff>=0.8.0",
"pyright>=1.1.390",
]
[tool.uv]
package = false
# --------------------------------------------------------------------- ruff
# 职责二合一:`ruff format` 管格式,`ruff check` 管正确性与一致性。
# 之所以不用 Black + isort + flake8 三件套,是因为 ruff 一个二进制就覆盖了它们,
# 而且配置在同一次解析里生效,不会出现"格式化完 lint 又报"的拉锯。
[tool.ruff]
line-length = 100 # 贴着现有代码定:只有 18 行超 100,用默认的 88 会重排 122 行
target-version = "py310" # 与 requires-python 对齐
src = ["farm", "tools"]
# 文档里的示例代码是"给人看的排版",不交给格式化器管
extend-exclude = ["README.md"]
[tool.ruff.lint]
select = [
"E", # pycodestyle 错误
"W", # pycodestyle 警告
"F", # pyflakes:未使用的导入/变量、未定义的名字
"I", # isort:导入排序
"UP", # pyupgrade:用新语法替换过时写法
"B", # flake8-bugbear:容易踩坑的写法(可变默认值、裸 except、丢异常链……)
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"RET", # flake8-return:多余的 else / 多余的中间变量
"PERF",# 明显的性能坑(比如循环里做成员判断)
"RUF", # ruff 自带规则
]
# 为什么**没有**启用 PTH(flake8-use-pathlib):
# 这个项目的路径绝大多数是"字符串身份"——要塞进 subprocess 命令行、要写进 JSON
# 存档、要打印给用户看。全部换成 pathlib 只是把字符串包一层再拆一层,
# 换来 70 处纯风格改动,却掩盖了真正有价值的修复。路径运算本来就少,
# 现有 os.path 用法是合适的选择。
ignore = [
# RUF001/002/003 会把中文全角标点当成"含糊的 Unicode 字符"报出来。
# 这个项目注释与界面文案本来就是中文,全角括号是正确写法,不是错误。
"RUF001",
"RUF002",
"RUF003",
# PERF203 提示"循环里 try 有性能开销"。剩下两处都是**逐项容错**语义:
# 逐个牧场存档(失败要立刻上报,不能静默跳过)、收获全部时跳过失败的地块。
# 把 try 提到循环外会让"一项失败不影响其它项"这个行为消失——那是错的优化。
# 而且循环规模是牧场数 / 地块数级别(个位数),性能开销在这里无关紧要。
"PERF203",
]
[tool.ruff.lint.per-file-ignores]
# 内嵌的 HTML/JS 模板:整块写在一个字符串里,按 100 列硬拆反而更难读也更难改
"farm/server/webui.py" = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["farm"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto" # Windows/Linux 混用时不要强改行尾,避免整文件 diff
# ------------------------------------------------------------------ pyright
# 只做到"能进 CI 拦住真问题"的程度:
# - basic 模式抓得住 None 误用、参数个数/类型不符、属性不存在这类真 bug;
# - 不去追 Unknown 类型(本项目的快照是 dict[str, Any] 结构,追下去全是噪音)。
[tool.pyright]
include = ["farm", "tools", "main.py", "ai_player.py"]
exclude = [".venv", "**/__pycache__", ".ruff_cache", "saves", "run"]
pythonVersion = "3.10"
# 代码里有 termios / msvcrt 这类平台专属分支。只按当前平台分析,
# 会把另一侧的调用误报成"模块没有这个属性",所以让 pyright 同时考虑所有平台。
pythonPlatform = "All"
typeCheckingMode = "basic"
reportMissingTypeStubs = false # 让"没装 third-party 存根"保持安静
reportUnusedImport = true
reportUnnecessaryIsInstance = true
# __all__ 里写子模块名是 `farm/__init__.py` 惰性导出的正常写法,
# pyright 只认模块里真实存在的名字,这条对我们是纯噪音
reportUnsupportedDunderAll = false