"""famires_v3 の確かめ(codex の反例を含む)。使い方: python3 test_famires_v3.py""" import copy import famires_v3 as v import famires_gen2 as gen ok = True def expect(name, cond): global ok print(("OK " if cond else "NG ") + name) ok &= bool(cond) def base(): return v.prepare(gen.p1_efficient_corner()) def mini(aisle, offset=0.0, model="BellaBot"): """幅 aisle の通路1本の奥に、座る側が通路に向いたボックスが1つある店""" v.SETTINGS["model"] = model W, D = 8.0, 6.0 x_wall = 3.0 + offset L = dict(name="test", outer=dict(w=W, d=D), entrance=dict(side="bottom", a0=0.2, a1=1.0), rooms=[dict(label="パントリー", rect=[0.0, 4.0, 2.0, 6.0], kind="back"), dict(label="待合", rect=[7.5, 0.0, 8.0, 1.0], kind="guest"), dict(label="ドリンクバー", rect=[7.0, 5.5, 8.0, 6.0], kind="drink")], walls=[dict(label="壁", rect=[x_wall, 0.0, x_wall + 0.2, 4.6]), # 通路の左の壁 dict(label="壁", rect=[x_wall + 0.2 + aisle, 0.0, 8.0, 3.0])], # 通路の右の壁 dish_up=[dict(label="受け渡し口", rect=[0.0, 4.0, 2.0, 4.4])], robot_standby=dict(label="待機", rect=[6.0, 5.2, 6.8, 6.0], slots=1), tables=[], toilet_doors=[]) # 通路の奥(y<3)の右側の壁に、座る側を通路に向けたボックス(壁の切り欠きに置く) bx = x_wall + 0.2 + aisle L["walls"][1]["rect"] = [bx + 1.2, 0.0, 8.0, 3.0] L["walls"].append(dict(label="壁", rect=[bx, 0.0, bx + 1.2, 0.4])) L["tables"] = [dict(type="booth", axis="v", x=bx, y=0.6, w=1.2, h=1.85, seats=4)] v.prepare(L) L["start"] = [1.0, 3.45] return L for res in (0.025, 0.0125): for off in (0.0, 0.013, 0.031): r, e = v.analyze(mini(0.80, off), res) expect(f"80cmの通路はBellaBotで通れる(マス目{res}・ずらし{off})", r[0]["category"] in ("robot", "narrow")) r, e = v.analyze(mini(0.77, off), res) expect(f"77cmの通路はBellaBotで通れない(マス目{res}・ずらし{off})", r[0]["category"] == "human") r, e = v.analyze(mini(0.75, off, model="Servi"), res) expect(f"75cmの通路はServiで通れる(マス目{res}・ずらし{off})", r[0]["category"] in ("robot", "narrow")) v.SETTINGS["model"] = "BellaBot" # 背中側(ベンチの背)は座る側に数えない L = mini(1.2) L["tables"][0].update(axis="h", w=1.85, h=1.2) expect("横向きのボックスの左右(ベンチの背)は座る側の候補にしない", set(v.table_sides(L["tables"][0])) == {"bottom", "top"}) L = mini(1.2) expect("縦向きのボックスの上下(ベンチの背)は座る側の候補にしない", set(v.table_sides(L["tables"][0])) == {"left", "right"}) # codex の反例:卓を店の外へ L = base() L["tables"][0]["x"] = -2.0 expect("店の外にはみ出した卓を見つける", any(k == "店の外" for k, _ in v.check_rules(L))) # codex の反例:トイレの入口の前に壁 L = base() L["walls"].append(dict(label="壁", rect=list(L["toilet_doors"][0]))) expect("トイレの入口の前の壁を見つける", any(k == "トイレ" for k, _ in v.check_rules(L))) # ちょうど1mも「1m以内」。壁でへだてていれば置いてよい L = v.prepare(gen.p2_efficient_narrow()) tr = [r["rect"] for r in L["rooms"] if r["label"] == "トイレ"][0] t = [t for t in L["tables"] if t["type"] == "t2" and t["side"] == "top"][0] t["x"] = tr[2] + 1.0 expect("トイレの壁でへだてた1mの席は違反にしない", not any("トイレから1.00m" in m for _, m in v.check_rules(L))) L["walls"] = [w for w in L["walls"] if w["label"] != "トイレの壁"] expect("壁がなければ、トイレからちょうど1mの席を見つける", any("トイレから1.00m" in m for _, m in v.check_rules(L))) # 卓と壁の重なり(仕切りの接合は許し、それ以外は見つける) L = base() L["walls"].append(dict(label="壁", rect=[L["tables"][5]["x"] + 0.3, L["tables"][5]["y"] + 0.3, L["tables"][5]["x"] + 0.6, L["tables"][5]["y"] + 0.6])) expect("卓に食い込んだ壁を見つける", any(k == "重なり" and "壁" in m for k, m in v.check_rules(L))) L = v.prepare(gen.p3_relaxed_center()) expect("ゆったり型の仕切り(背もたれの上)は違反にしない", not v.check_rules(L)) # 待合をふさいだ6人席(⑤で実際に起きた) L = v.prepare(gen.p3_relaxed_center()) for r in L["rooms"]: if r["label"] == "待合": r["rect"] = [15.6, 2.2, 18.2, 2.8] res, err = v.analyze(L, 0.025) expect("座る側の前をふさがれた卓を見つける", any(k == "届かない卓" for k, _ in err)) # 出発点がふさがれていたら止める L = base() L["walls"].append(dict(label="壁", rect=[L["start"][0] - 0.2, L["start"][1] - 0.2, L["start"][0] + 0.2, L["start"][1] + 0.2])) try: v.analyze(L, 0.025) expect("出発点がふさがれていたら止める", False) except ValueError: expect("出発点がふさがれていたら止める", True) # 待合・トイレの壁がない L = base() L["rooms"] = [r for r in L["rooms"] if r["label"] != "待合"] expect("待合がないのを見つける", any("待合" in m for _, m in v.check_rules(L))) # ⑦ は③と同じ卓・同じ席数 a, b = v.prepare(gen.p1_efficient_corner()), v.prepare(gen.p5_crammed()) expect("⑦は③と同じ卓の数・種類・席数", [(t["type"], t["seats"]) for t in a["tables"]] == [(t["type"], t["seats"]) for t in b["tables"]]) # ---- codex の2回目の検証(review_result_v3.md)を受けて足した確かめ(2026-10-22) import famires_p0 # L字席:flip・mirror の4通りすべてで、止まってよい辺は「内側の角の開いた2辺」+「ソファの端に沿って reach+余白まで」だけ ext = v.SETTINGS["lsofa_end_reach"] + v.SETTINGS["side_end_margin"] for flip in (False, True): for mirror in (False, True): t = dict(type="booth6L", x=10.0, y=10.0, w=2.6, h=3.6, seats=6, flip=flip, mirror=mirror) x, y, w, h = 10.0, 10.0, 2.6, 3.6 qx = (x, x + 0.45 * w) if mirror else (x + 0.55 * w, x + w) # 開いた角の x の範囲(mirror なら左側) qy = (y, y + 0.55 * h) if flip else (y + 0.45 * h, y + h) # 開いた角の y の範囲(flip なら下側) S = v.table_sides(t) good = len(S) == 2 for k, ((ax, ay), (bx, by), (nx, ny)) in S.items(): if nx: # 縦の辺:開いた角の側の縦の外形線の上で、y の範囲は角の y の範囲+ソファ側へ ext まで good &= abs(ax - (x if mirror else x + w)) < 1e-9 and nx == (-1 if mirror else 1) lo, hi = sorted((ay, by)) good &= (qy[0] - ext - 1e-9 <= lo) and (hi <= qy[1] + ext + 1e-9) good &= (lo <= qy[0] + 1e-9 and hi >= qy[1] - 1e-9) # 開いた辺は全部含む outside = (qy[0] - lo) + (hi - qy[1]) good &= outside <= ext + 1e-9 # ソファの横は ext まで else: # 横の辺 good &= abs(ay - (y if flip else y + h)) < 1e-9 and ny == (-1 if flip else 1) lo, hi = sorted((ax, bx)) good &= (lo <= qx[0] + 1e-9 and hi >= qx[1] - 1e-9) good &= (qx[0] - lo) + (hi - qx[1]) <= ext + 1e-9 expect(f"L字席(flip={flip}・mirror={mirror})の止まってよい辺は、開いた角とソファの端{v.SETTINGS['lsofa_end_reach']}mまで", good) # ⓪T02(codex の反例):止まる位置がソファの横(角から0.6mより先)にならない L0 = v.prepare(famires_p0.p0_memory()) r0, e0 = v.analyze(L0, 0.025) t02 = L0["tables"][1] a02 = r0[1] corner_y = t02["y"] + 0.45 * t02["h"] expect("⓪T02 の止まる位置は、開いた角か、そこからソファの端に沿って0.6m以内", a02.get("stop") is not None and (a02["side"] == "top" or a02["stop"][1] >= corner_y - v.SETTINGS["lsofa_end_reach"] - 1e-6)) expect("⓪の待機場所は、1台ずつ受け渡し口の前まで出られる", not any(k == "待機場所" for k, _ in e0)) # 2人席:ベンチの側・ベンチの横は使わない for side in ("bottom", "top", "left", "right"): t = dict(type="t2", x=5.0, y=5.0, w=1.4 if side in ("left", "right") else 0.8, h=0.8 if side in ("left", "right") else 1.4, seats=2, side=side) S = v.table_sides(t) b = v.FURNITURE_TYPES["t2"]["bench_depth"] x0, y0, x1, y1 = v.fp(t) bench = {"bottom": (x0, y0, x1, y0 + b), "top": (x0, y1 - b, x1, y1), "left": (x0, y0, x0 + b, y1), "right": (x1 - b, y0, x1, y1)}[side] good = side not in S and len(S) == 3 for k, ((ax, ay), (bx, by), (nx, ny)) in S.items(): if nx: good &= min(ay, by) >= bench[3] - 1e-9 or max(ay, by) <= bench[1] + 1e-9 or side in ("left", "right") else: good &= min(ax, bx) >= bench[2] - 1e-9 or max(ax, bx) <= bench[0] + 1e-9 or side in ("bottom", "top") expect(f"2人席(ベンチが{side})は、ベンチの側とベンチの横を止まる辺にしない", good) # 待機場所の寸法・台数 L = v.prepare(famires_p0.p0_memory()) L["robot_standby"]["rect"] = [12.2, 5.2, 14.4, 5.9] # 直す前の⓪(2.2×0.7m に3台) expect("直す前の⓪の待機場所(2.2×0.7mに3台)を見つける", any(k == "待機場所" for k, _ in v.check_rules(L))) for bad in (100, 0, -1, "3", 2.5, True): L = base() L["robot_standby"]["slots"] = bad expect(f"待機場所の台数 {bad!r} を見つける", any(k == "待機場所" for k, _ in v.check_rules(L))) # 待機場所の前を壁でふさぐと、両端の台は横から出られるが、真ん中の台は出られない L = base() sb = L["robot_standby"]["rect"] L["walls"].append(dict(label="壁", rect=[sb[0], sb[1] - 0.3, sb[2], sb[1]])) res, err = v.analyze(L, 0.025) msgs = [m for k, m in err if k == "待機場所"] expect("待機場所の前をふさぐと、真ん中の台だけ「出られない」を見つける", len(msgs) == 1 and "2台目" in msgs[0]) # 前と両横をふさぐと、どの台も出られない L["walls"] += [dict(label="壁", rect=[sb[0] - 0.3, sb[1] - 0.3, sb[0], sb[3]]), dict(label="壁", rect=[sb[2], sb[1] - 0.3, sb[2] + 0.3, sb[3]])] res, err = v.analyze(L, 0.025) expect("待機場所の前と両横をふさぐと、3台とも「出られない」を見つける", sum(k == "待機場所" for k, _ in err) == 3) # ---- codex の2回目の検証の「検査をすり抜ける配置」7つ(すべて違反として見つかること) L = base() L["rooms"] = [r for r in L["rooms"] if r["label"] not in ("レジ", "レジ内")] expect("反例1:レジとレジ内を消すと見つける", any("レジがない" in m for _, m in v.check_rules(L)) and any("レジ内)がない" in m for _, m in v.check_rules(L))) L = base() L["walls"] = [w for w in L["walls"] if w["label"] != "トイレの壁"] + [dict(label="トイレの壁", rect=[0.0, 13.8, 0.3, 14.1])] expect("反例2:トイレの壁をほぼ全部外すと「切れ目」を見つける", any("切れ目" in m for _, m in v.check_rules(L))) L = base() L["walls"].append(dict(label="トイレの壁", rect=list(L["toilet_doors"][0]))) expect("反例3:トイレの入口の前を「トイレの壁」という名前の壁でふさいでも見つける", any("入口の前をふさいでいる" in m for _, m in v.check_rules(L))) L = base() t = L["tables"][4] cx, cy = t["x"] + t["w"] / 2, t["y"] + t["h"] / 2 L["walls"].append(dict(label="仕切り", rect=[cx - 0.025, cy - 0.15, cx + 0.025, cy + 0.15], joint=True)) expect("反例4:家具の真ん中に置いた接合扱いの仕切りを見つける", any(k == "重なり" and t["id"] in m for k, m in v.check_rules(L))) L = base() L["robot_standby"]["slots"] = 100 expect("反例5:待機場所100台を見つける", any(k == "待機場所" for k, _ in v.check_rules(L))) L = v.prepare(famires_p0.p0_memory()) L["rooms"] = [r for r in L["rooms"] if r["kind"] != "outside"] expect("反例6:⓪の建物の外を消すと、輪郭と合わないのを見つける", any(k == "建物の形" for k, _ in v.check_rules(L))) L = base() L["outline"] = [[0, 0], [2, 0], [2, 2], [0, 2]] expect("反例7:輪郭を2m四方にすると見つける", any(k == "建物の形" for k, _ in v.check_rules(L))) L = v.prepare(famires_p0.p0_memory()) L["outline"] = [[0, 0], [24, 0], [24, 19.2], [0, 19.2]] # 欠けのない輪郭なのに建物の外がある expect("⓪の輪郭から欠けを消すと、建物の外と合わないのを見つける", any(k == "建物の形" for k, _ in v.check_rules(L))) for nm, fn in v.BUILDS: expect(f"今の店 {nm} は決まりを守っている", not v.check_rules(v.prepare(fn()))) # 正しい接合(ベンチの背の上の仕切り)は認め、背でない辺や深すぎる食い込みは認めない L = v.prepare(gen.p3_relaxed_center()) jw = [w for w in L["walls"] if w.get("joint") and w["rect"][3] - w["rect"][1] < 0.2][0] bt = [t for t in L["tables"] if t["type"] == "booth" and v.inter(v.fp(t), jw["rect"])][0] expect("ベンチの背に立つ仕切りは認める", v.joint_ok(bt, jw)) deep = dict(jw, rect=[jw["rect"][0], jw["rect"][1] - 0.2, jw["rect"][2], jw["rect"][3]]) expect("ベンチの背でも0.08mより深い食い込みは認めない", not v.joint_ok(bt, deep) or not v.inter(v.fp(bt), deep["rect"])) # トイレの目隠し:中心の1本だけ壁に当たっても、4本全部が当たらなければ違反 def toilet_mini(screen): # トイレ(左上)の入口は下向き。席は入口の右手前(トイレから約0.7m) return dict(outer=dict(w=6.0, d=6.0), tables=[dict(id="T01", type="t4", x=2.6, y=2.4, w=1.2, h=1.2, seats=4)], rooms=[dict(label="トイレ", rect=[0.0, 4.0, 2.0, 6.0], kind="guest")], walls=[dict(label="トイレの壁", rect=[0.0, 4.0, 0.5, 4.3]), dict(label="トイレの壁", rect=[1.5, 4.0, 2.0, 4.3]), dict(label="トイレの壁", rect=[1.7, 4.3, 2.0, 6.0]), dict(label="目隠し", rect=screen)], toilet_doors=[[0.5, 2.8, 1.5, 4.0]], robot_standby=dict(rect=[5.0, 0.0, 5.8, 0.8], slots=1)) ms = [m for k, m in v.check_toilets(toilet_mini([2.0, 3.4, 2.1, 3.9])) if "T01" in m] expect("トイレの目隠し:中心の線だけ当たる短い壁では違反にする(4本の線)", len(ms) == 1) ms = [m for k, m in v.check_toilets(toilet_mini([2.0, 2.2, 2.1, 4.0])) if "T01" in m] expect("トイレの目隠し:4本とも当たる壁なら違反にしない", not ms) # 線と矩形の交わり:薄い壁を見逃さない expect("線分が厚さ2mmの壁を横切るのを見つける", v.seg_hits((0, 0), (1, 0), [(0.004, -1, 0.006, 1)])) expect("線分の端が壁に触れるだけなら、通ったとみなさない", not v.seg_hits((0, 0), (1, 0), [(1.0, -1, 1.2, 1)])) expect("線分が壁から離れていれば、通ったとみなさない", not v.seg_hits((0, 0), (1, 0), [(0.4, 0.1, 0.6, 1)])) # 止まる位置の候補:201番目だけ見通せる場合も見つける(200点で打ち切らない) import numpy as np class FakeGrid: def stop_cells(self, side, width, reach): return np.ones((1, 201), bool), None def sight(self, cy, cx, side, skip): return cx == 200 bs = v.best_stop(FakeGrid(), np.arange(201, dtype=float).reshape(1, 201), {"x": None}, 1.0, 0.3) expect("201番目だけ見通せる止まる位置も見つける", bs is not None and bs[2] == 200) # 違反があるときは、出力先に何も書かない(前の版がそのまま残り、新旧が混ざらない) import os, sys, tempfile, json as _json from pathlib import Path d = Path(tempfile.mkdtemp()) saved = (v.BUILDS, sys.argv) v.BUILDS = [("p2_efficient_narrow", gen.p2_efficient_narrow)] sys.argv = ["famires_v3.py", "--out", str(d)] v.main() before = {str(f.relative_to(d)): f.read_bytes() for f in d.rglob('*') if f.is_file()} gid = _json.load(open(d / "summary.json", encoding="utf-8"))[0]["generation"] same_gen = _json.load(open(d / "p2_efficient_narrow.json", encoding="utf-8"))["meta"]["generation"] == gid == \ _json.load(open(d / "p2_efficient_narrow_analysis.json", encoding="utf-8"))["meta"]["generation"] expect("成功したときは、layout・analysis・summary に同じ版の番号が入る", same_gen) def broken(): L = gen.p2_efficient_narrow() L["rooms"] = [r for r in L["rooms"] if r["label"] != "レジ"] return L v.BUILDS = [("p2_efficient_narrow", broken)] try: v.main() code = 0 except SystemExit as ex: code = ex.code after = {str(f.relative_to(d)): f.read_bytes() for f in d.rglob('*') if f.is_file()} expect("違反があると止まり、出力先のファイルは前の版のまま(作業用のフォルダも残らない)", code == 1 and before == after) # 出力先へ入れ替える途中(2つ目のファイル)で失敗しても、前の版に戻る(新旧が混ざらない) v.BUILDS = [("p2_efficient_narrow", gen.p2_efficient_narrow)] orig_replace, calls = v.os.replace, [0] def flaky_replace(a, b): calls[0] += 1 if calls[0] == 2: raise OSError("2つ目で失敗(テスト)") return orig_replace(a, b) v.SETTINGS["side_end_margin"] += 0.01 # 出力が前と変わるように、設定を少し変える v.os.replace = flaky_replace try: v.main() raised = False except OSError: raised = True finally: v.os.replace = orig_replace v.SETTINGS["side_end_margin"] -= 0.01 after = {str(f.relative_to(d)): f.read_bytes() for f in d.rglob('*') if f.is_file()} expect("入れ替えの途中で失敗すると、出力先は前の版に戻る(作業用のフォルダも残らない)", raised and before == after) v.BUILDS, sys.argv = saved # ---- codex の3回目の検証(review_result_v4.md)の反例 L = base() bw =[w for w in L["walls"] if w["label"] == "トイレの壁" and abs(w["rect"][0]) < 1e-9 and w["rect"][2] > 2.0][0] L["walls"].remove(bw) L["walls"] += [dict(label="トイレの壁", rect=[bw["rect"][0], bw["rect"][1], 1.005, bw["rect"][3]]), dict(label="トイレの壁", rect=[1.015, bw["rect"][1], bw["rect"][2], bw["rect"][3]])] expect("反例A:トイレの壁の1cmの切れ目を見つける", any("切れ目" in m for _, m in v.check_rules(L))) L = base() L["walls"].append(dict(label="閉塞片", rect=[2.7, 14.905, 2.8, 14.915])) expect("反例B:トイレの入口の内側の1cmのふさぎを見つける", any("入口に 閉塞片 が重なっている" in m for _, m in v.check_rules(L))) L = base() dz = L["toilet_doors"][0] L["toilet_doors"][0] = [dz[0], dz[1], dz[0] + 0.1, dz[3]] expect("反例C:トイレの入口の前の奥行き0.1mを見つける", any("奥行き" in m for _, m in v.check_rules(L))) L = base() L["outline"] = [[0, 0], [24, 0], [24, 16], [13.02, 16], [13.02, 15], [13.01, 15], [13.01, 16], [0, 16]] expect("反例D:輪郭の幅1cmの切れ込み(建物の外なし)を見つける", any(k == "建物の形" for k, _ in v.check_rules(L))) L = base() L["outline"] = [[0, 0], [24, 0], [24, 16], [0, 15]] expect("輪郭に斜めの辺があれば止める", any("縦と横の辺" in m for _, m in v.check_rules(L))) L = v.prepare(famires_p0.p0_memory()) L["walls"].append(dict(label="壁", rect=list(L["doors"][0]["rect"]))) expect("出入り口Aを壁でふさぐと見つける", any(k == "出入り口" for k, _ in v.check_rules(L))) # ---- Unity 用の細かい形(unity_export.py。2026-10-22) import unity_export as ux for nm, fn, width in (("p1", gen.p1_efficient_corner, 1.2), ("p5", gen.p5_crammed, 0.8)): L = v.prepare(fn()) ctx = {} res, err = v.analyze(L, 0.025, ctx) U = ux.build(L, res, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], v.standby_slots(L["robot_standby"]), v.FURNITURE_TYPES, {}) expect(f"{nm}:細い通路は3本で、幅はどれも{width}m", len(U["aisles"]) == 3 and all(abs(a["width"] - width) < 1e-6 for a in U["aisles"])) expect(f"{nm}:卓の数・止まる位置・距離が analysis と同じ", len(U["tables"]) == len(res) and all(t["stop"] == r.get("stop") and t["dist"] == r.get("dist") for t, r in zip(U["tables"], res))) expect(f"{nm}:細い通路を通る卓は、止まる位置がその通路の区画の中", all( any(a["id"] == t["aisle"] and a["rect"][0] - 0.05 <= t["stop"][0] <= a["rect"][2] + 0.05 and a["rect"][1] - 0.05 <= t["stop"][1] <= a["rect"][3] + 0.05 for a in U["aisles"]) for t in U["tables"] if t["aisle"])) expect(f"{nm}:細い通路の入口は、受け渡し口の前から行ける端", all(a["entrance"] is not None for a in U["aisles"])) expect(f"{nm}:全部の卓に部品(天板・座る位置)があり、座る位置の数が席数と同じ", all(t["parts"] and len(t["parts"]["seat_points"]) == t["seats"] for t in U["tables"])) kinds = {p["kind"] for p in U["places"]} expect(f"{nm}:場所の点がそろっている", {"FoodPass", "DishReturn", "Entrance", "Waiting", "Register", "RegisterBack", "Toilet", "DrinkBar", "DrinkBarStand", "RobotStandby", "StaffStandby"} <= kinds) g = ctx["grid"] pts = [p["point"] for p in U["places"] if p["kind"] in ("StaffStandby", "RegisterQueue", "DrinkBarQueue", "DrinkBarStand")] expect(f"{nm}:スタッフが待つ点・並ぶ点・立つ点は、障害物から0.2m以上離れ、細い通路に入らない", all(g.clear[g.cell(x, y)] >= 0.2 - 1e-9 and not ux._in_aisle(U["aisles"], x, y) for x, y in pts)) expect(f"{nm}:席に着く点は障害物から0.2m以上離れ、どの卓にも1つ以上ある", all(t["spots"] for t in U["tables"]) and all(g.clear[g.cell(*s["point"])] >= 0.2 - 1e-9 for t in U["tables"] for s in t["spots"])) expect(f"{nm}:レジ内は通れる部屋(スタッフだけ)、ほかの部屋は通れない箱", all((r["walkable"] and r.get("staff_only")) if r["label"] == "レジ内" else not r["walkable"] for r in U["rooms"])) # ロボットの道順(2026-10-22) R = ux.build_routes(L, res, g, v.SETTINGS["normal_width"], v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"]) fp = R["from_pass"] expect(f"{nm}:受け渡し口から全卓・卓から卓の全組に道順がある", len(fp) == len(res) and all(fp.values()) and len(R["between"]) == len(res) * (len(res) - 1) // 2 and all(R["between"].values())) expect(f"{nm}:ふだんの道か狭い道かは、試算の卓の分け方と同じ(robot→normal、narrow→narrow)", all((a["category"] == "robot") == (fp[t["id"]]["use"] == "normal") for t, a in zip(L["tables"], res))) expect(f"{nm}:道順は受け渡し口の前から始まり、止まる位置で終わる", all(fp[t["id"]]["points"][0] == [round(L["start"][0], 3), round(L["start"][1], 3)] and fp[t["id"]]["points"][-1] == [round(a["stop"][0], 3), round(a["stop"][1], 3)] for t, a in zip(L["tables"], res))) f_n, f_p = g.free(v.SETTINGS["normal_width"]), g.free(v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"]) allr = list(fp.values()) + list(R["between"].values()) expect(f"{nm}:道順のどの区間も、その幅で通れるマス目だけを通る", all(ux._visible(f_n if r["use"] == "normal" else f_p, g, a, b) for r in allr for a, b in zip(r["points"], r["points"][1:]))) L = v.prepare(gen.p1_efficient_corner()) L["tables"][5]["x"] += 0.05 # 列の中の1卓をずらす → 列が切れる cols = ux.booth_columns(L["tables"]) expect("ボックス席の列は、すき間なく並んだものだけをまとめる", len(cols) > len(ux.booth_columns(v.prepare(gen.p1_efficient_corner())["tables"]))) print("\nすべて OK" if ok else "\nNG あり") raise SystemExit(0 if ok else 1)