    # v2.6 までは、はじめから「先に空いた方」だったため、受け渡し口の横で待つスタッフが多く運んでいた。
    # ロボットが出るときは、受け渡し口で待っている料理を段の数までまとめて載せ、近い卓から回る。
    def deliver(self, d, fragile=False, counted=True):
        env, p = self.env, self.p
        rec = {"d": d, "ready": env.now, "fragile": fragile, "counted": counted,
               "taken": env.event(), "arrived": env.event()}
        robot_ok = self.robots is not None and not (fragile and p["fragile_by"] == "staff")
        if not robot_ok:
            sreq = self.staff.request(priority=1)
            yield sreq
            yield env.process(self._staff_carry(rec, sreq))
            return
        rreq = self.robots.request(priority=0)
        rec["sreq"], rec["rreq"] = None, rreq
        self.pass_q.append(rec)
        yield rreq | rec["taken"] | env.timeout(p["robot_first_wait_s"])   # まずロボットだけを待つ
        if not (rreq.triggered or rec["taken"].triggered):                # 2分たった：ここからはスタッフも運べる
            sreq = self.staff.request(priority=1)
            rec["sreq"] = sreq
            yield rreq | sreq | rec["taken"]
        sreq = rec["sreq"]
        if rec["taken"].triggered:          # ほかの料理と一緒にロボットに載った
            yield rec["arrived"]
            return
        self.pass_q.remove(rec)
        if rreq.triggered:                  # ロボットが運ぶ
            if sreq is not None:
                self._drop(sreq, self.staff)
            batch = [rec]
            for o in list(self.pass_q):
                if len(batch) >= p["trays"]:
                    break
                self.pass_q.remove(o)
                self._drop(o["rreq"], self.robots)
                if o["sreq"] is not None:
                    self._drop(o["sreq"], self.staff)
                o["taken"].succeed()
                batch.append(o)
            env.process(self._robot_run(batch, rreq))
            yield rec["arrived"]
            return
        rreq.cancel()                       # スタッフが運ぶ
        yield env.process(self._staff_carry(rec, sreq))

    @staticmethod
    def _drop(req, res):
        if req.triggered:
            res.release(req)
        else:
            req.cancel()

    def _robot_run(self, batch, rreq):
        env, p = self.env, self.p
        start = env.now
        batch.sort(key=lambda r: r["d"])
        pos = 0.0
        for i, rec in enumerate(batch):
            leg = rec["d"] - pos + (p["hop_m"] if i else 0)
            yield env.timeout(max(leg, 0) / p["robot_speed"])
            if i == 0:
                yield env.process(self.maybe_stop(rec["d"]))
            if rec["counted"]:
                self.dish_waits.append((env.now - rec["ready"]) / 60)
            self.robot_trips += 1
            self.fragile_robot += rec["fragile"]
            rec["arrived"].succeed()
            yield env.timeout(p["robot_handoff_s"])
            pos = rec["d"]
        yield env.timeout(pos / p["robot_speed"])
        self.robot_busy_s += env.now - start
        self.robots.release(rreq)

    def _staff_carry(self, rec, sreq):
        env, p = self.env, self.p
        self.staff_trips += 1
        go = rec["d"] / p["staff_speed"]
        yield env.timeout(go)
        if rec["counted"]:
            self.dish_waits.append((env.now - rec["ready"]) / 60)
        rec["arrived"].succeed()
        yield env.timeout(p["staff_place_s"] + go)
        self.staff_walk_m += 2 * rec["d"]
        self.staff_busy_s += 2 * go + p["staff_place_s"]
        self.staff.release(sreq)

    # ---- 巡回回収（食事中にロボットが来て、客が空いた皿を載せる）
    def collect_visit(self, d, deadline, box):
        env, p = self.env, self.p
        req = self.robots.request(priority=2)
        timer = env.timeout(max(deadline - env.now, 0))
        res = yield req | timer
        if req not in res:
            req.cancel()
            return
        start = env.now
        t = d / p["robot_speed"]
        back = (d + p["dish_extra_m"]) / p["robot_speed"]
        yield env.timeout(t)
        yield env.process(self.maybe_stop(d))
        yield env.timeout(p["guest_load_s"] + back)
        self.robot_busy_s += env.now - start
        self.robots.release(req)
        self.collects += 1
        box[0] += 1

    # ---- 下げ膳（客が帰ってから卓が使えるようになるまで）
    def bus(self, d, collected, counted=True):
        env, p = self.env, self.p
        start = env.now
        trips = max(p["bus_trips"] - collected, 0)
        if p["mode"] == "bus" and self.robots is not None and trips > 0:
            # v2.4: スタッフの手が空いて片付けに向かうときに、空いているロボットがいれば一緒に卓へ向かわせる
            with self.staff.request(priority=2) as sreq:
                yield sreq
                s0 = env.now
                rreq = self.robots.request(priority=1)
                if rreq.triggered:
                    self.robot_bus += 1
                    arrive, loaded = env.event(), env.event()
                    env.process(self._robot_bus_trip(d, rreq, arrive, loaded))
                    # スタッフは卓まで行き、皿をまとめ、ロボットが着いたら載せ、卓を拭いて戻る
                    yield env.timeout(d / p["staff_speed"] + p["clear_s"])
                    yield arrive
                    yield env.timeout(p["load_s"])
                    loaded.succeed()                 # 載せ終えた。ロボットは洗い場へ
                    yield env.timeout(p["wipe_s"])
                    done_at = env.now
                    yield env.timeout(d / p["staff_speed"])
                    self.staff_walk_m += 2 * d
                    self.staff_busy_s += env.now - s0
                    if counted:
                        self.turns.append((done_at - start) / 60)
                    return
                rreq.cancel()
                # 空いているロボットがいない：そのまま人だけで下げる（皿の量に応じて往復）
                self.staff_bus += 1
                walk = 2 * (d + p["dish_extra_m"]) * trips
                tt = walk / p["staff_speed"] + p["clear_s"] + p["wipe_s"]
                self.staff_walk_m += walk
                self.staff_busy_s += tt
                yield env.timeout(tt)
            if counted:
                self.turns.append((env.now - start) / 60)
            return
        # 人だけで下げる（皿の量に応じて往復）。皿がもうなければ拭きに行くだけ
        self.staff_bus += 1
        walk = 2 * (d + p["dish_extra_m"]) * trips if trips > 0 else 2 * d
        work = (p["clear_s"] if trips > 0 else 0) + p["wipe_s"]
        yield env.process(self.staff_job(2, walk, work))
        if counted:
            self.turns.append((env.now - start) / 60)

    def _robot_bus_trip(self, d, rreq, arrive, loaded):
        env, p = self.env, self.p
        start = env.now
        t = d / p["robot_speed"]
        yield env.timeout(t)
        yield env.process(self.maybe_stop(d, helper_waiting=True))
        arrive.succeed()
        yield loaded                                  # v2.4: スタッフが載せ終えるまで卓で待つ
        back = (d + p["dish_extra_m"]) / p["robot_speed"]
        yield env.timeout(back + 15)  # 洗い場で降ろすのは厨房側
        self.robot_busy_s += env.now - start
        self.robots.release(rreq)

    # ---- 1組の流れ
    def group(self):
        env, p, rng = self.env, self.p, self.rng
        arrive = env.now
        counted = (self.t0 <= arrive < self.t1) if p["sustain"] else True   # 数える時間に来た組だけを記録する
        if counted:
            self.counted += 1
        # 図面のある店は卓ごとの距離から選ぶ。ほかの店は今までどおり（乱数の使い方も同じ）
        d = rng.choice(p["dist_list"]) if p.get("dist_list") else rng.uniform(*p["dist_m"])
        self._acc()
        table = self.tables.request()
        yield table
        blocked = self.blocked.pop(table, 0.0)
        if counted:
            w = (env.now - arrive) / 60
            self.seat_waits.append(w)
            self.seat_wait_by_arrival.append((arrive, w))
        # 案内（v2.4）: スタッフの手が空いたら卓まで一緒に歩く。卓に着いた時点を「席に着いた」とする。
        # 説明（guide_s）と受け渡し口への戻りは、スタッフの手がふさがる時間には入れ、お客さんの注文はそのあとに進む（v2.2 と同じ）
        g0 = env.now
        with self.staff.request(priority=1) as greq:
            yield greq
            gwait = env.now - g0
            yield env.timeout(d / p["staff_speed"])
            seated_at = env.now
            yield env.timeout(p["guide_s"] + d / p["staff_speed"])
            self.staff_walk_m += 2 * d
            self.staff_busy_s += 2 * d / p["staff_speed"] + p["guide_s"]
        if counted:
            self.blocked_waits.append(blocked / 60)
            self.guide_waits.append(gwait / 60)
            self.entry_waits.append((seated_at - arrive) / 60)
        firsts = [env.process(self._cook_then_deliver(c, d, False, counted)) for c in p["first_dishes"]]
        yield simpy.AllOf(env, firsts)
        eat = tri(rng, p["eat_min"]) * 60
        end = env.now + eat
        # 食事中の追加注文・呼び出し・巡回回収
        extras = []
        for _ in range(p["extra_orders"]):
            extras.append(env.process(self._order_later(rng.uniform(0, eat * 0.7), d, p["extra_cook"], False, counted)))
        n_fragile = int(p["fragile"]) + (rng.random() < p["fragile"] - int(p["fragile"]))
        for _ in range(n_fragile):
            extras.append(env.process(self._order_later(rng.uniform(0.3, 0.85) * eat, d, p["fragile_cook"], True, counted)))
        calls = sum(rng.random() < p["call_rate"] / 2 for _ in range(2))
        for _ in range(calls):
            env.process(self._call_later(rng.uniform(0, eat), d))
        collected = [0]
        if p["mode"] == "patrol" and self.robots is not None:
            for k in range(p["bus_trips"]):
                at = eat * (k + 1) / (p["bus_trips"] + 1)
                env.process(self._collect_later(at, d, end, collected))
        yield env.timeout(eat)
        if extras:
            yield simpy.AllOf(env, extras)
        yield env.process(self.staff_job(1, 0, p["pay_s"]))
        self._acc()
        self.dirty += 1                      # 客は帰った。片付けが済むまで卓は使えない
        yield env.process(self.bus(d, collected[0], counted))
        self._acc()
        self.dirty -= 1
        self.tables.release(table)
        self.done += 1
        if counted:
            self.counted_done += 1
            self._check_finished()

    def _cook_then_deliver(self, cook, d, fragile=False, counted=True):
        yield self.env.timeout(tri(self.rng, cook) * 60)
        yield self.env.process(self.deliver(d, fragile, counted))

    def _order_later(self, delay, d, cook, fragile, counted=True):
        yield self.env.timeout(delay)
        yield self.env.process(self._cook_then_deliver(cook, d, fragile, counted))

    def _call_later(self, delay, d):
        yield self.env.timeout(delay)
        yield self.env.process(self.staff_job(1, 2 * d, self.p["call_s"]))

    def _collect_later(self, delay, d, deadline, box):
        yield self.env.timeout(delay)
        yield self.env.process(self.collect_visit(d, deadline, box))
