"""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)
