chore: 工程化改造前的基线快照

在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前,
先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。

此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
This commit is contained in:
snowy-yang committed 2026-09-18 21:12:40 +08:00
commit a7725f8812
35 files changed
+11871

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""把 TUI 渲染结果输出到终端,用于检查排版与配色。
不进入交互循环,也不读取键盘——只是把某一时刻的完整画面画出来。
用法:
python tools/preview_tui.py # 使用默认的中等规模农场(本地引擎)
python tools/preview_tui.py --width 100 # 指定终端宽度
python tools/preview_tui.py --color # 输出带 ANSI 颜色
python tools/preview_tui.py --scene empty # 初始状态
python tools/preview_tui.py --scene rich # 后期大农场
python tools/preview_tui.py --scene menu # 播种菜单
python tools/preview_tui.py --url http://127.0.0.1:8765 --farm alice
# 渲染**真实服务器**上的牧场
"""
from __future__ import annotations
import argparse
import contextlib
import io
import os
import random
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from farm.engine import FarmEngine # noqa: E402
from farm.tui import TuiApp # noqa: E402
SCENES = ("empty", "mixed", "rich", "menu", "help")
def build_remote(url: str, farm: str, token: str | None) -> tuple[object, TuiApp]:
"""连到真实服务器,取一份现状来渲染。"""
from farm.client import RemoteFarm
client = RemoteFarm(endpoint=url, farm=farm, token=token, realtime=False)
app = TuiApp(client, use_color=True, autosave=False,
server_label=f"{client.farm} @ {client.host}")
return client, app
def build_engine(scene: str) -> tuple[FarmEngine, TuiApp]:
random.seed(42)
engine = FarmEngine(tick_seconds=15)
app = TuiApp(engine, use_color=True, autosave=False)
if scene == "empty":
return engine, app
# ---- 制造一个"进行中"的农场
engine.state.level = 2
engine.state.gold = 340
engine.state.feed = 4
engine.plant(1, "wheat")
engine.water(1)
engine.plant(2, "carrot")
engine.plant(3, "wheat")
engine.plant(4, "carrot")
engine.advance_day()
engine.advance_day()
engine.buy_animal("chicken")
engine.feed(1)
if scene == "rich":
engine.state.level = 4
engine.state.gold = 2400
engine.state.feed = 22
for i in range(2):
engine.state.gold = 99999
engine.buy_plot()
engine.plant(5, "corn")
engine.plant(6, "tomato")
engine.water(5)
engine.state.gold = 2400
engine.buy_animal("sheep")
engine.buy_animal("cow")
engine.advance_day()
engine.advance_day()
engine.add_log("收获 2 份小麦(丰收!),+31 金币", "good")
engine.add_log("升级到 Lv.4!奖励 240 金币,解锁:南瓜", "good")
if scene == "menu":
# 找第一块空地,把播种菜单打开
empty = next((p.index for p in engine.state.plots if p.is_empty), 0)
app.plot_cursor = empty
app._open_crop_menu()
if scene == "help":
app.mode = "help"
return engine, app
def main() -> int:
parser = argparse.ArgumentParser(description="渲染一帧 TUI 画面")
parser.add_argument("--width", type=int, default=112, help="终端宽度")
parser.add_argument("--height", type=int, default=32, help="终端高度")
parser.add_argument("--scene", choices=SCENES, default="mixed", help="场景")
parser.add_argument("--color", action="store_true", help="保留 ANSI 颜色")
parser.add_argument("--url", default=None,
help="连到真实服务器渲染(例如 http://127.0.0.1:8765)")
parser.add_argument("--farm", default="default", help="--url 模式下要看的牧场")
parser.add_argument("--token", default=None, help="--url 模式下的访问令牌")
args = parser.parse_args()
os.environ["COLUMNS"] = str(args.width)
os.environ["LINES"] = str(args.height)
if args.url:
engine, app = build_remote(args.url, args.farm, args.token)
state = engine.snapshot()
print(f"# 来自真实服务器 {args.url} · 牧场 {args.farm} · 第 {state.get('day')} 天 · "
f"金币 {state.get('gold')} · 在线 {state.get('online')}\n")
else:
engine, app = build_engine(args.scene)
# 预览脚本没有跑 Scheduler,这里手动给个倒计时,让画面更接近真实运行状态
if engine.next_tick_at is None and not engine.paused:
import time as _time
engine.next_tick_at = _time.time() + 8.4
app.use_color = args.color
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
app._render()
output = buffer.getvalue()
# 去掉清屏、光标控制与行尾清理符,预览里只保留可见内容
output = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", output)
print(output)
return 0
if __name__ == "__main__":
sys.exit(main())
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""WebSocket 通道演示脚本:证明"服务器推、客户端拉/发"这一对方向都通。
它同时是一个"外部客户端"的样板——完全不知道游戏内部结构,只知道协议:
连接 → 收 welcome(含状态)→ 发一个动作 → 收 result 回执 → 收 state 推送
用法:
python tools/ws_demo.py --url http://127.0.0.1:8765 --farm demo --seconds 8
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import urllib.parse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def ws_url(endpoint: str, farm: str, token: str | None) -> str:
parts = urllib.parse.urlsplit(endpoint if "://" in endpoint else "http://" + endpoint)
scheme = "wss" if parts.scheme == "https" else "ws"
query = {"farm": farm, "name": "ws-demo"}
if token:
query["token"] = token
return f"{scheme}://{parts.netloc}/ws?{urllib.parse.urlencode(query)}"
async def run(endpoint: str, farm: str, token: str | None, seconds: float) -> int:
import websockets
url = ws_url(endpoint, farm, token)
print(f"连接 {url}")
async with websockets.connect(url, open_timeout=8) as socket:
welcome = json.loads(await socket.recv())
state = welcome.get("state", {})
print(f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · "
f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个")
async def sender() -> None:
"""通过 WebSocket 发一个动作,等回执。"""
await asyncio.sleep(0.3)
request = {"type": "action", "id": 1, "action": "sleep", "params": {}}
print(f"→ action:{json.dumps(request, ensure_ascii=False)}")
await socket.send(json.dumps(request))
deadline = asyncio.get_event_loop().time() + seconds
send_task = asyncio.create_task(sender())
counts: dict[str, int] = {}
try:
while asyncio.get_event_loop().time() < deadline:
try:
raw = await asyncio.wait_for(socket.recv(), timeout=seconds)
except asyncio.TimeoutError:
break
message = json.loads(raw)
kind = message.get("type", "?")
counts[kind] = counts.get(kind, 0) + 1
if kind == "result":
print(f"← result(id={message.get('id')}):{message.get('message')} "
f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]")
elif kind == "state":
snapshot = message.get("state", {})
print(f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · "
f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · "
f"在线 {message.get('online')}")
elif kind == "day":
print(f"← day:进入第 {message.get('day')} 天,事件 {message.get('events')}")
elif kind == "notice":
print(f"← notice:{message.get('text')}")
else:
print(f"← {kind}")
finally:
send_task.cancel()
print(f"\n消息统计:{counts}")
if counts.get("result", 0) < 1 or counts.get("state", 0) < 1:
print("✗ WebSocket 通道没有按预期工作(result / state 消息缺失)")
return 1
print("✓ WebSocket 双向通道正常:动作可上行,状态会主动下行")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="WebSocket 通道演示")
parser.add_argument("--url", default="http://127.0.0.1:8765")
parser.add_argument("--farm", default="ws-demo")
parser.add_argument("--token", default=None)
parser.add_argument("--seconds", type=float, default=8.0)
args = parser.parse_args()
return asyncio.run(run(args.url, args.farm, args.token, args.seconds))
if __name__ == "__main__":
sys.exit(main())