feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档
- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay 只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续 运行并记 BLOCKED;写本批次 release-manifest.json 后聚合 - run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理); soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数) - 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试 - forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别 - 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求 - .gitignore 排除本地场景配置、凭据文件与运行输出 离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS, node 夹具测试 PASS。未联网运行,未重建候选包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# forest_viewpoints_test —— MAP-02 机位夹具校验与自动候选挑选(不加载地图)。
|
||||
# godot --headless --path project --script forest_viewpoints_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Viewpoints = preload("res://testing/forest_viewpoints.gd")
|
||||
|
||||
const BOUNDS := {"base": Vector2(1049600, 0), "size": Vector2i(2, 2)}
|
||||
var _fail := 0
|
||||
var _done := false
|
||||
|
||||
func _ck(ok: bool, message: String) -> void:
|
||||
if not ok:
|
||||
_fail += 1
|
||||
printerr("FAIL: " + message)
|
||||
|
||||
func _init() -> void:
|
||||
_run()
|
||||
_ck(_done, "all viewpoint assertions executed")
|
||||
print("forest_viewpoints_test: failures=%d" % _fail)
|
||||
if _fail == 0:
|
||||
print("PASS: forest_viewpoints_test")
|
||||
quit(1 if _fail else 0)
|
||||
|
||||
func _has_error(result: Dictionary, needle: String) -> bool:
|
||||
return result.errors.any(func(e: String) -> bool: return needle in e)
|
||||
|
||||
func _run() -> void:
|
||||
# No fixture file: nothing is confirmed, every kind is missing, nothing fails.
|
||||
var empty := Viewpoints.plan({}, "m", BOUNDS)
|
||||
_ck(empty.errors.is_empty() and empty.viewpoints.is_empty(), "missing fixture is not an error")
|
||||
_ck(empty.missing_kinds == Viewpoints.KINDS, "missing fixture leaves all four kinds unconfirmed")
|
||||
_ck(Viewpoints.load_file("").ok and not Viewpoints.load_file("/nonexistent/vp.json").ok,
|
||||
"empty path means no fixture; a named but absent file is an error")
|
||||
|
||||
var good := {"schema_version": 1, "maps": {"m": {"races": [2301, 2306], "viewpoints": [
|
||||
{"id": "flat-01", "kind": "flat", "status": "confirmed", "confirmed_by": "test-env-owner",
|
||||
"server_cm": [1062400, 12800], "camera_yaw_deg": 90},
|
||||
{"id": "warp-01", "kind": "warp", "status": "unconfirmed", "server_cm": [1100000, 40000]},
|
||||
]}}}
|
||||
var planned := Viewpoints.plan(good, "m", BOUNDS)
|
||||
_ck(planned.errors.is_empty(), "valid fixture accepted: %s" % [planned.errors])
|
||||
_ck(planned.missing_kinds == ["slope", "dense"], "configured kinds are not reported missing")
|
||||
_ck(planned.races == [2301, 2306], "races come from the fixture")
|
||||
_ck(planned.viewpoints[0].local_m.is_equal_approx(Vector2(128, 128)), "server cm converts through BasePosition")
|
||||
_ck(Viewpoints.to_server_cm(BOUNDS, Vector2(128, 128)) == Vector2(1062400, 12800), "local metres convert back to server cm")
|
||||
|
||||
var bad := {"schema_version": 2, "maps": {"m": {"races": [0], "viewpoints": [
|
||||
{"id": "a", "kind": "flat", "status": "confirmed", "server_cm": [1062400, 12800]},
|
||||
{"id": "a", "kind": "cave", "status": "unconfirmed", "confirmed_by": "x", "server_cm": [1062400, 12800]},
|
||||
{"id": "b", "kind": "slope", "status": "maybe", "server_cm": [10, 10]},
|
||||
{"id": "c", "kind": "dense", "status": "unconfirmed", "server_cm": [1.5, 2]},
|
||||
]}, "password": "x"}}
|
||||
var rejected := Viewpoints.plan(bad, "m", BOUNDS)
|
||||
for needle in ["schema_version", "confirmed_by (role) is empty", "duplicates a", "kind must be one of",
|
||||
"unconfirmed but carries confirmed_by", "status must be", "outside m", "server_cm must be",
|
||||
"races must contain positive", "credentials must not be stored"]:
|
||||
_ck(_has_error(rejected, needle), "invalid fixture reports: %s (got %s)" % [needle, rejected.errors])
|
||||
|
||||
# Candidate picking is deterministic over synthetic terrain samples.
|
||||
var samples: Array = []
|
||||
for x in range(0, 101, 4):
|
||||
for z in range(0, 101, 4):
|
||||
var p := Vector2(x, z)
|
||||
var range_m := 0.02 if p.distance_to(Vector2(40, 40)) < 3.0 else 0.3
|
||||
if p.distance_to(Vector2(80, 30)) < 3.0:
|
||||
range_m = 1.2 # steep but walkable (0.4)
|
||||
if p.distance_to(Vector2(20, 80)) < 3.0:
|
||||
range_m = 6.0 # cliff (2.0) must lose to a walkable slope
|
||||
samples.append({"p": p, "range_m": range_m, "span_m": 3.0, "blocked": p == Vector2(44, 40)})
|
||||
var trees := PackedVector2Array()
|
||||
for i in 8:
|
||||
trees.append(Vector2(60, 72) + Vector2(4, 0).rotated(TAU * i / 8.0))
|
||||
trees.append(Vector2(41, 41)) # tree trunk right next to the flattest point
|
||||
var picked := Viewpoints.pick_candidates(samples, trees, Vector2(100, 100), Viewpoints.AUTO_KINDS)
|
||||
var by_kind := {}
|
||||
for c: Dictionary in picked:
|
||||
by_kind[c.kind] = c
|
||||
_ck(by_kind.has("flat") and by_kind.flat.range_m <= 0.3 and by_kind.flat.p.distance_to(Vector2(41, 41)) >= Viewpoints.AUTO_TREE_CLEARANCE_M,
|
||||
"flat candidate keeps clear of tree trunks: %s" % [by_kind.get("flat", {})])
|
||||
_ck(by_kind.has("slope") and (by_kind.slope.p as Vector2).distance_to(Vector2(80, 30)) < 3.0,
|
||||
"slope candidate is the steepest walkable point, not the cliff: %s" % [by_kind.get("slope", {})])
|
||||
_ck(by_kind.has("dense") and by_kind.dense.p.distance_to(Vector2(60, 72)) < Viewpoints.AUTO_DENSITY_RADIUS_M,
|
||||
"dense candidate sits inside the tree cluster: %s" % [by_kind.get("dense", {})])
|
||||
for a: Dictionary in picked:
|
||||
for b: Dictionary in picked:
|
||||
_ck(a == b or (a.p as Vector2).distance_to(b.p) >= Viewpoints.AUTO_SEPARATION_M, "candidates are separated")
|
||||
_ck(a.p.x >= Viewpoints.AUTO_EDGE_MARGIN_M and a.p.y >= Viewpoints.AUTO_EDGE_MARGIN_M, "candidates avoid map edges")
|
||||
_ck(not picked.any(func(c: Dictionary) -> bool: return c.kind == "warp"), "warp points are never guessed offline")
|
||||
var flat_only := Viewpoints.pick_candidates([{"p": Vector2(50, 50), "range_m": 0.0, "span_m": 3.0, "blocked": false}],
|
||||
PackedVector2Array(), Vector2(100, 100), Viewpoints.AUTO_KINDS)
|
||||
_ck(flat_only.size() == 1 and flat_only[0].kind == "flat", "flat terrain yields no fake slope or dense candidate")
|
||||
_done = true
|
||||
Reference in New Issue
Block a user