using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>料理1皿</summary>
    public class DishOrder
    {
        public int id;
        public int groupId;
        public int tableIndex;      // StoreBuilder.Tables の番号
        public int tableId;         // 卓の番号（報告用）
        public float readyS = -1f;  // 受け渡し口に出た時刻（店の中, 秒）
        public float arrivedS = -1f;// 卓に届いた時刻

        /// <summary>はじめて誰かが持ち上げた時刻（受け渡し口で待った時間を出すため）</summary>
        public float firstTakenS = -1f;

        /// <summary>最後に持ち上げられた時刻（運んでいた時間を出すため）</summary>
        public float lastTakenS = -1f;
        public bool fragile;        // デザート・ドリンク
        public bool taken;          // 誰かが持っている
        public bool byRobot;        // ロボットが運んだ
        public int requeued;        // 受け渡し口に戻された回数
    }

    /// <summary>スタッフの仕事。優先は試算の staff_job と同じ（1=案内・配膳・呼び出し・会計、2=片付け）</summary>
    /// <summary>
    /// スタッフの仕事の種類。
    /// Rescue（v5 3章）は、動けなくなったロボットのところへ歩いて行って rescue_s 動かす仕事。
    /// サイトの決まりで「ほかの仕事より先」なので、優先度は 0（ほかは 1 と 2）。
    /// </summary>
    public enum StaffJobKind { Guide, Serve, Call, Pay, Clear, Rescue }

    public class StaffJob
    {
        public StaffJobKind kind;
        public int priority;
        public int groupId;
        public int tableIndex;
        public int tableId;
        public DishOrder dish;      // Serve のときだけ
        public ServingRobotController robot;  // Rescue のときだけ（動けなくなった台）
        public bool taken;
        public float postedS;

        /// <summary>スタッフが取った時刻（店の中, 秒）。まだなら -1。入店待ちの内訳に使う</summary>
        public float takenS = -1f;

        /// <summary>
        /// この仕事を何回あきらめたか（v5 3章）。**仕事ごと**に数える。
        /// 人ごとに数えていたときは、仕事が人から人へ回ると誰も上限（staffGiveUpMaxTries）に
        /// 届かず、「3回で移して済ませる」逃がし弁が働かないまま回り続けた。
        /// </summary>
        public int tries;
        /// <summary>仕事が終わったときに呼ばれる（出した側が入れる）</summary>
        public System.Action onDone;
    }

    /// <summary>
    /// あきらめた・移した・卓を空けた、の1回ぶん（give_ups.csv に1行）。
    /// 「移した」「卓を空けた」が多いときは、人が通れない店になっているしるし。
    /// </summary>
    public struct GiveUpRow
    {
        public float tS;         // 店の中の時刻（秒）
        public string who;       // 客 / スタッフ / 組
        public string job;       // 何をしていたか
        public int tableId;      // 関わる卓（無ければ 0）
        public float toX, toZ;   // 向かっていた先
        public float x, z;       // そのときの位置
        public string aisle;     // そのときいた通路
        public float waitedS;    // 着けないまま待った秒数
        public string action;    // あきらめた / 移した / 卓を空けた
    }

    /// <summary>
    /// 人がロボットに近づきすぎた1回ぶん（contacts.csv に1行）。
    /// ロボットの外周から PeopleSpec.robotContactGapM 以内に入ったとき。
    /// </summary>
    public struct ContactRow
    {
        public float tS;         // 店の中の時刻（秒）
        public float x, z;       // そのときの位置
        public string aisle;     // そのときいた通路
        public string place;     // そのときいた場所（レジ・待合・ドリンクバーなど。無ければ空）
        public string who;       // 誰が（種類:名前）
        public bool careless;    // よけ方を守らない人だったか
        public int robotId;      // 相手の台
        public float gapM;       // ロボットの外周からの空き（m。負なら重なった）
    }

    /// <summary>届けられずに受け渡し口へ戻した1回ぶん（dishes.csv に1行）</summary>
    public struct RequeueRow
    {
        public float tS;
        public int dishId;
        public int tableId;
        public int robotId;
        public bool fragile;
        public float readyS;      // 料理ができた時刻
        public int requeuedSoFar; // この皿が戻された回数（この回を含む）
        public string why;        // 動けなくなった / 打ち切り / 取りやめ / 着かなかった
    }

    /// <summary>卓1つの状態</summary>
    public class TableSlot
    {
        public int index;
        public int tableId;
        public Vector3 deliveryPoint;
        public Vector3 aisleEntrance;
        public Bounds aisleZone;
        public Vector3 seatPoint;

        /// <summary>何人かけの卓か（2人席なら2、4人席なら4）</summary>
        public int seatCount = 4;

        public int groupId = -1;    // -1 なら空き
        public bool clearing;       // 片付け中（まだ使えない）
        public bool Free { get { return groupId < 0 && !clearing; } }
    }

    /// <summary>
    /// 店の回し方の中心。卓の空き、受け渡し口の料理の待ち行列、スタッフの仕事板を持ち、
    /// 空いたロボットに料理をまとめて渡す（試算の deliver / _robot_run と同じ決まり）。
    ///
    /// この script は店の運営だけを扱う。人の歩き方はそれぞれの script、
    /// ロボットの止まる決まりは ServingRobotController に任せる。
    /// 数字は ServiceSpec の .asset に置く。
    /// </summary>
    public class ServiceOps : MonoBehaviour
    {
        [Header("設定")]
        public ServiceSpec spec;
        public RobotSpec robotSpec;

        [Tooltip("卓と通路を読む先")]
        public StoreBuilder store;

        [Header("いまの様子（読むだけ）")]
        [SerializeField] int _freeTables;
        [SerializeField] int _dishesWaiting;
        [SerializeField] int _jobsWaiting;

        readonly List<TableSlot> _tables = new List<TableSlot>();
        readonly List<DishOrder> _pass = new List<DishOrder>();
        readonly List<StaffJob> _jobs = new List<StaffJob>();
        readonly List<ServingRobotController> _robots = new List<ServingRobotController>();
        readonly Dictionary<ServingRobotController, List<DishOrder>> _carrying =
            new Dictionary<ServingRobotController, List<DishOrder>>();
        readonly Dictionary<ServingRobotController, List<DishOrder>> _lastBatch =
            new Dictionary<ServingRobotController, List<DishOrder>>();

        int _nextDishId = 1;

        // ---- 記録（TripLogger と ExperimentRunner が読む）----
        public readonly List<float> dishWaitMin = new List<float>();   // 料理ができてから届くまで（分）
        public readonly List<float> seatWaitMin = new List<float>();   // 入店から案内が始まるまで（分）
        public readonly List<float> turnMin = new List<float>();       // 退店から卓が空くまで（分）

        // ---- 入店待ちの内訳（v5 3章・サイトの試算（haizen_sim.py）と同じ分け方）----
        //   seat_wait   … 店に来てから案内が始まるまで（seatWaitMin）
        //   blocked_wait… そのうち、卓は空いたのに片付けが終わらず待った分
        //   full_wait   … seat_wait − blocked_wait（ほんとうに満席で待った分）
        //   guide_wait  … 案内の仕事が板に出てから、スタッフが取るまで
        //   staff_wait  … blocked_wait ＋ guide_wait（店側が追いつかなかった分）
        //   entry_wait  … 店に来てから卓に着くまで（案内の歩きと guide_s を含む）
        /// <summary>entry_wait_mean：店に来てから案内されて卓に着くまで（分）</summary>
        public readonly List<float> entryWaitMin = new List<float>();

        /// <summary>full_wait_mean：席待ち − blocked_wait（分）</summary>
        public readonly List<float> fullWaitMin = new List<float>();

        /// <summary>blocked_wait_mean：卓は空いたが片付けが終わらず待った分（分）</summary>
        public readonly List<float> blockedWaitMin = new List<float>();

        /// <summary>guide_wait_mean：案内の仕事が板に出てから取られるまで（分）</summary>
        public readonly List<float> guideWaitMin = new List<float>();

        /// <summary>staff_wait_mean：blocked_wait ＋ guide_wait（分）</summary>
        public readonly List<float> staffWaitMin = new List<float>();
        public int robotDishes { get; private set; }
        public int staffDishes { get; private set; }
        public int requeuedDishes { get; private set; }

        // ---- あきらめた記録（B1）----
        readonly List<GiveUpRow> _giveUps = new List<GiveUpRow>();
        public IReadOnlyList<GiveUpRow> GiveUps { get { return _giveUps; } }

        // ---- 人がロボットをよけた記録 ----
        readonly List<ContactRow> _contacts = new List<ContactRow>();

        /// <summary>人がロボットに近づきすぎた1回ずつ（contacts.csv）</summary>
        public IReadOnlyList<ContactRow> Contacts { get { return _contacts; } }

        /// <summary>人がロボットを待った回数（入口で待つ＋通路の中で下がる）</summary>
        public int robotYieldWaits { get; private set; }

        /// <summary>人がロボットを待っていた秒数の合計</summary>
        public float robotYieldWaitSeconds { get; private set; }

        /// <summary>人が回り道した回数</summary>
        public int robotYieldDetours { get; private set; }

        // ---- レジの前の混み具合（調べるための記録）----
        /// <summary>レジの前にいた人数のいちばん多いとき</summary>
        public int registerCrowdMax { get; private set; }

        float _registerCrowdSum;
        int _registerCrowdSamples;

        // ---- 会計のときスタッフが店員側（レジの内側）にいたか ----
        /// <summary>会計をした回数</summary>
        public int registerPayCount { get; private set; }

        /// <summary>そのうち、店員側にいなかった回数（0 になるはず）</summary>
        public int registerPayNotAtBackCount { get; private set; }

        /// <summary>立つ点から 0.1 m より離れて会計した回数（0 になるはず）</summary>
        public int registerPayOffSpotCount { get; private set; }

        /// <summary>立つ点と実際に立った位置の距離のいちばん大きいもの（m）</summary>
        public float registerPayMaxOffsetM { get; private set; }

        /// <summary>立つ点からの距離の合計（平均を出すため, m）</summary>
        float _registerPayOffsetSum;

        /// <summary>
        /// いま店員側（レジの内側）で会計しているスタッフがいるか。
        /// 店員側は 0.8 × 1.2 m で1人ぶんなので、会計の仕事は同時に1つだけ取れる。
        /// </summary>
        public bool RegisterBackBusy { get; private set; }

        /// <summary>店員側に、会計でないスタッフがいた秒数（0 になるはず）</summary>
        public float registerBackIntruderSeconds { get; private set; }

        /// <summary>店員側に、会計でないスタッフがいた回数（0 になるはず）</summary>
        public int registerBackIntruderCount { get; private set; }

        /// <summary>会計でないスタッフが店員側にいたことを残す（StaffAgent が毎コマ呼ぶ）</summary>
        public void AddRegisterBackIntruder(float dt, bool firstFrame)
        {
            registerBackIntruderSeconds += dt;
            if (firstFrame) registerBackIntruderCount++;
        }

        /// <summary>立つ点と実際に立った位置の距離の平均（m）</summary>
        public float RegisterPayMeanOffsetM
        {
            get { return registerPayCount > 0 ? _registerPayOffsetSum / registerPayCount : 0f; }
        }

        /// <summary>
        /// 会計1回ぶんを数える。
        /// atBack が false なら店員側にいなかった。offsetM は立つ点からの距離。
        /// どちらも 0 になるはず（ならなければ、スタッフが店員側に入れていない）。
        /// </summary>
        public void AddRegisterPay(bool atBack, float offsetM)
        {
            registerPayCount++;
            if (!atBack) registerPayNotAtBackCount++;

            _registerPayOffsetSum += offsetM;
            if (offsetM > registerPayMaxOffsetM) registerPayMaxOffsetM = offsetM;
            float limit = spec != null ? spec.registerPayOffSpotM : 0.1f;
            if (offsetM > limit) registerPayOffSpotCount++;
        }

        /// <summary>レジの前にいた人数の平均</summary>
        public float RegisterCrowdMean
        {
            get { return _registerCrowdSamples > 0 ? _registerCrowdSum / _registerCrowdSamples : 0f; }
        }

        // ---- スタッフの時間の使い方（仕事ごとの秒数）----
        readonly Dictionary<string, float> _staffTime = new Dictionary<string, float>();

        /// <summary>スタッフが何にどれだけ時間を使ったか（仕事の名前 → 秒）</summary>
        public IDictionary<string, float> StaffTime { get { return _staffTime; } }

        // ---- 料理が届くまでの内訳（運んだのがロボットか人かで分ける）----
        /// <summary>届いた料理1皿ぶんの内訳（秒）</summary>
        public struct DishParts
        {
            public bool byRobot;
            public float passWaitS;   // 受け渡し口で待った（できてから、はじめて持ち上げられるまで）
            public float carryS;      // 運んでいた（最後に持ち上げられてから、卓に届くまで）
            public float requeueS;    // 途中で戻されて失った（残り）
            public int requeued;      // 戻された回数
        }

        readonly List<DishParts> _dishParts = new List<DishParts>();

        /// <summary>届いた料理の、届くまでの内訳（summary.json に出す）</summary>
        public IReadOnlyList<DishParts> DishPartsList { get { return _dishParts; } }

        // ---- 仕事が板で待った秒数（仕事ごと）----
        readonly Dictionary<string, float> _jobWaitSum = new Dictionary<string, float>();
        readonly Dictionary<string, int> _jobWaitCount = new Dictionary<string, int>();

        /// <summary>仕事が板に出てから取られるまでの秒数の合計（仕事の名前 → 秒）</summary>
        public IDictionary<string, float> JobWaitSum { get { return _jobWaitSum; } }

        /// <summary>仕事が取られた回数（仕事の名前 → 回）</summary>
        public IDictionary<string, int> JobWaitCount { get { return _jobWaitCount; } }

        void RecordJobWait(StaffJob j)
        {
            if (j == null) return;
            string k = StaffAgent.JobKindName(j.kind);
            float v; int n;
            _jobWaitSum.TryGetValue(k, out v);
            _jobWaitCount.TryGetValue(k, out n);
            _jobWaitSum[k] = v + Mathf.Max(0f, NowS - j.postedS);
            _jobWaitCount[k] = n + 1;
        }

        /// <summary>スタッフの時間を足す（StaffAgent が毎コマ呼ぶ）</summary>
        public void AddStaffTime(string what, float dt)
        {
            if (string.IsNullOrEmpty(what)) return;
            float v;
            _staffTime.TryGetValue(what, out v);
            _staffTime[what] = v + dt;
        }

        // ---- レジの前に並ぶ点（会計に立つ人が1人ずつ）----
        readonly List<Vector3> _regQueues = new List<Vector3>();
        readonly List<bool> _regQueueUsed = new List<bool>();

        /// <summary>レジの並ぶ点を1つ取る。無ければ -1（会計に立つ人も席で待つ）</summary>
        public int TakeRegisterQueueSpot(out Vector3 spot)
        {
            for (int i = 0; i < _regQueueUsed.Count; i++)
            {
                if (_regQueueUsed[i]) continue;
                _regQueueUsed[i] = true;
                spot = _regQueues[i];
                return i;
            }
            spot = RegisterPointOrPass();
            return -1;
        }

        /// <summary>取ったレジの並ぶ点を空ける</summary>
        public void ReleaseRegisterQueueSpot(int index)
        {
            if (index < 0 || index >= _regQueueUsed.Count) return;
            _regQueueUsed[index] = false;
        }

        Vector3 RegisterPointOrPass()
        {
            return store != null ? store.RegisterPoint : Vector3.zero;
        }
        public int gaveUpCount { get; private set; }      // あきらめた
        public int warpedCount { get; private set; }      // 移した
        public int tableFreedCount { get; private set; }  // 卓を空けた

        // ---- 立ち往生でスタッフを呼んだ回数（v5 3章・meta.metrics の rescues）----
        /// <summary>動けなくなって、スタッフを呼んだ回数</summary>
        public int rescueCalls { get; private set; }

        /// <summary>そのうち、スタッフが着いて動かした回数</summary>
        public int rescueDone { get; private set; }

        /// <summary>スタッフが着くまでロボットが待った秒数の合計</summary>
        public float rescueWaitSeconds { get; private set; }

        // ---- ロボットのふさがっている割合（robot_util）----
        float _robotBusyS, _robotAliveS;

        /// <summary>robot_util：ロボットが空いていなかった時間の割合</summary>
        public float RobotUtil { get { return _robotAliveS > 0.01f ? _robotBusyS / _robotAliveS : 0f; } }

        // ---- スタッフが歩いた距離（walk_km_per_staff）----
        float _staffWalkM;
        int _staffCount;

        /// <summary>スタッフ1人が歩いた距離（km）</summary>
        public float WalkKmPerStaff
        {
            get { return _staffCount > 0 ? _staffWalkM / 1000f / _staffCount : 0f; }
        }

        /// <summary>スタッフの人数を伝える（walk_km_per_staff を出すため）</summary>
        public void SetStaffCount(int n) { _staffCount = n; }

        /// <summary>スタッフが1コマで歩いた距離を足す（StaffAgent が呼ぶ）</summary>
        public void AddStaffWalk(float m)
        {
            if (m > 0f && m < 1f) _staffWalkM += m;   // 1 m を超える跳びは「移した」ぶんなので数えない
        }

        // ---- 戻した皿の記録（B8）----
        readonly List<RequeueRow> _requeues = new List<RequeueRow>();
        public IReadOnlyList<RequeueRow> Requeues { get { return _requeues; } }

        // ---- 椅子を引いた回数（B6）----
        readonly Dictionary<string, int> _chairPullsByReason = new Dictionary<string, int>();
        readonly Dictionary<int, int> _chairPullsByTable = new Dictionary<int, int>();
        public int chairPulls { get; private set; }
        public IDictionary<string, int> ChairPullsByReason { get { return _chairPullsByReason; } }
        public IDictionary<int, int> ChairPullsByTable { get { return _chairPullsByTable; } }

        // ---- ドリンクバーの場所取り（B4）----
        readonly List<Vector3> _dbStands = new List<Vector3>();
        readonly List<Vector3> _dbQueues = new List<Vector3>();
        readonly List<bool> _dbStandUsed = new List<bool>();
        readonly List<bool> _dbQueueUsed = new List<bool>();

        /// <summary>並ぶ点に回された回数</summary>
        public int drinkBarQueued { get; private set; }

        /// <summary>立つ点も並ぶ点も空いていなくて、あきらめた回数</summary>
        public int drinkBarGaveUp { get; private set; }

        /// <summary>並んで待った秒数（分で記録）</summary>
        public readonly List<float> drinkBarWaitMin = new List<float>();

        public IList<ServingRobotController> Robots { get { return _robots; } }
        public IList<TableSlot> Tables { get { return _tables; } }
        public int FreeTableCount { get { return _freeTables; } }
        public int DishesWaiting { get { return _dishesWaiting; } }

        /// <summary>店の中の時刻（秒）。ExperimentRunner が進める</summary>
        public float NowS { get; private set; }

        // ---------------- 用意 ----------------

        public void Setup()
        {
            _tables.Clear();
            _pass.Clear();
            _jobs.Clear();
            _carrying.Clear();
            dishWaitMin.Clear();
            seatWaitMin.Clear();
            turnMin.Clear();
            entryWaitMin.Clear();
            fullWaitMin.Clear();
            blockedWaitMin.Clear();
            guideWaitMin.Clear();
            staffWaitMin.Clear();
            rescueCalls = 0;
            rescueDone = 0;
            rescueWaitSeconds = 0f;
            _robotBusyS = 0f;
            _robotAliveS = 0f;
            _staffWalkM = 0f;
            robotDishes = 0;
            staffDishes = 0;
            requeuedDishes = 0;
            _giveUps.Clear();
            _requeues.Clear();
            _contacts.Clear();
            _staffTime.Clear();
            _dishParts.Clear();
            _jobWaitSum.Clear();
            _jobWaitCount.Clear();
            registerCrowdMax = 0;
            registerPayCount = 0;
            registerPayNotAtBackCount = 0;
            registerPayOffSpotCount = 0;
            registerPayMaxOffsetM = 0f;
            _registerPayOffsetSum = 0f;
            RegisterBackBusy = false;
            registerBackIntruderSeconds = 0f;
            registerBackIntruderCount = 0;
            _registerCrowdSum = 0f;
            _registerCrowdSamples = 0;
            _regQueues.Clear();
            _regQueueUsed.Clear();
            robotYieldWaits = 0;
            robotYieldWaitSeconds = 0f;
            robotYieldDetours = 0;
            _chairPullsByReason.Clear();
            _chairPullsByTable.Clear();
            chairPulls = 0;
            gaveUpCount = 0;
            warpedCount = 0;
            tableFreedCount = 0;
            _nextDishId = 1;
            NowS = 0f;

            drinkBarQueued = 0;
            drinkBarGaveUp = 0;
            drinkBarWaitMin.Clear();
            _dbStands.Clear();
            _dbQueues.Clear();
            _dbStandUsed.Clear();
            _dbQueueUsed.Clear();

            if (store == null) return;
            IList<StoreBuilder.TableInfo> t = store.Tables;
            for (int i = 0; i < t.Count; i++)
            {
                TableSlot s = new TableSlot();
                s.index = i;
                s.tableId = t[i].tableId;
                s.deliveryPoint = t[i].deliveryPoint;
                s.aisleEntrance = t[i].aisleEntrance;
                s.aisleZone = t[i].aisleZone;
                s.seatPoint = t[i].center;
                s.seatCount = t[i].seatCount;
                _tables.Add(s);
            }
            // ドリンクバーの立つ点・並ぶ点を読む（B4）
            SetupDrinkBar(store.PointsOf(PlaceKind.DrinkBarStand),
                          store.PointsOf(PlaceKind.DrinkBarQueue));

            // レジの前に並ぶ点を読む
            IList<Vector3> rq = store.RegisterQueuePoints;
            for (int i = 0; i < rq.Count; i++) { _regQueues.Add(rq[i]); _regQueueUsed.Add(false); }

            Recount();
        }

        // ---------------- ドリンクバーの場所取り（B4）----------------

        void SetupDrinkBar(IList<Vector3> stands, IList<Vector3> queues)
        {
            for (int i = 0; i < stands.Count; i++) { _dbStands.Add(stands[i]); _dbStandUsed.Add(false); }
            for (int i = 0; i < queues.Count; i++) { _dbQueues.Add(queues[i]); _dbQueueUsed.Add(false); }
        }

        /// <summary>
        /// ドリンクバーの場所を1つ取る（B4）。
        ///   ・空いている立つ点があれば、そこへ
        ///   ・埋まっていれば、端の並ぶ点へ
        ///   ・それも埋まっていれば取れない（客はあきらめて席に戻る）
        /// spot は向かう点、queued は並ぶ点だったか。
        /// 返した番号を ReleaseDrinkBarSpot に渡して空ける。
        /// </summary>
        public int TakeDrinkBarSpot(out Vector3 spot, out bool queued)
        {
            for (int i = 0; i < _dbStandUsed.Count; i++)
            {
                if (_dbStandUsed[i]) continue;
                _dbStandUsed[i] = true;
                spot = _dbStands[i];
                queued = false;
                return i;
            }

            for (int i = 0; i < _dbQueueUsed.Count; i++)
            {
                if (_dbQueueUsed[i]) continue;
                _dbQueueUsed[i] = true;
                spot = _dbQueues[i];
                queued = true;
                drinkBarQueued++;
                return 1000 + i;      // 1000 以上は並ぶ点
            }

            spot = _dbStands.Count > 0 ? _dbStands[0] : Vector3.zero;
            queued = false;
            drinkBarGaveUp++;
            return -1;
        }

        /// <summary>取った場所を空ける。waitedS は並んで待った秒数</summary>
        public void ReleaseDrinkBarSpot(int token, float waitedS)
        {
            if (token < 0) return;
            if (token >= 1000)
            {
                int i = token - 1000;
                if (i < _dbQueueUsed.Count) _dbQueueUsed[i] = false;
                drinkBarWaitMin.Add(Mathf.Max(0f, waitedS / 60f));
                return;
            }
            if (token < _dbStandUsed.Count) _dbStandUsed[token] = false;
        }

        /// <summary>ドリンクバーに立つ点があるか（無ければ今までどおり1点へ行く）</summary>
        public bool HasDrinkBarSpots { get { return _dbStands.Count > 0; } }

        public void RegisterRobot(ServingRobotController r)
        {
            if (r == null || _robots.Contains(r)) return;
            _robots.Add(r);
            _carrying[r] = new List<DishOrder>();
            r.ops = this;               // 立ち往生したときスタッフを呼ぶ先（v5 3章）
            // 待機場所を先に教える（1往復めの戻りから、戻りの線を使えるように。v5）
            if (store != null)
            {
                IList<Vector3> spots = store.RobotStandbyPoints;
                if (spots.Count > 0) r.SetStandbyPoint(spots[(_robots.Count - 1) % spots.Count]);
            }
            r.TripCompleted += OnRobotRunDone;
        }

        public void ClearRobots()
        {
            for (int i = 0; i < _robots.Count; i++)
                if (_robots[i] != null) _robots[i].TripCompleted -= OnRobotRunDone;
            _robots.Clear();
            _carrying.Clear();
        }

        /// <summary>店の中の時刻を進める（ExperimentRunner が毎コマ呼ぶ）</summary>
        public void Advance(float dt)
        {
            NowS += dt;
            AssignRobots();
            ParkIdleRobots();
            SampleRegisterCrowd();
            SampleRobotUtil(dt);
            Recount();
        }

        /// <summary>
        /// ロボットがふさがっていた時間を数える（robot_util）。
        /// 待機場所へ下がっているあいだ・受け渡し口で空のまま待っているあいだは Idle / Done なので
        /// 「空いている」。動けなくなって人を待っているあいだ（StuckWait）は「ふさがっている」。
        /// </summary>
        void SampleRobotUtil(float dt)
        {
            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                _robotAliveS += dt;
                ServingRobotController.Phase p = r.CurrentPhase;
                if (p != ServingRobotController.Phase.Idle &&
                    p != ServingRobotController.Phase.Done) _robotBusyS += dt;
            }
        }

        /// <summary>
        /// レジの前にいた人数を数える（調べるための記録）。
        /// 待合に立つ客も、通路を歩く人も、スタッフも数える。
        /// </summary>
        void SampleRegisterCrowd()
        {
            if (store == null || spec == null) return;
            if (!store.HasPoint(PlaceKind.Register)) return;

            Vector3 reg = store.RegisterPoint;
            float r2 = spec.registerCrowdRadiusM * spec.registerCrowdRadiusM;

            int n = 0;
            IReadOnlyList<PersonBody> all = PersonBody.All;
            for (int i = 0; i < all.Count; i++)
            {
                PersonBody b = all[i];
                if (b == null || b.kind == PersonKind.Seated) continue;
                Vector3 d = b.transform.position - reg;
                d.y = 0f;
                if (d.sqrMagnitude <= r2) n++;
            }

            if (n > registerCrowdMax) registerCrowdMax = n;
            _registerCrowdSum += n;
            _registerCrowdSamples++;
        }

        /// <summary>
        /// 仕事のないロボットを待機場所へ下げる（B2）。
        /// 受け渡し口の前の点にはスタッフも料理を取りに来るので、空の台を置かない。
        /// 料理を載せるときは AssignRobots が受け渡し口から走り出させる。
        ///
        /// v5：料理が待っていても下げる。
        /// 前は「料理が待っているなら受け渡し口で待つ」にしていたが、そのせいで
        /// 出発する台と戻ってきた台が受け渡し口の前の1点に重なり、ふさぎ合っていた
        /// （20261023 0913 の回で、受け渡し口の前の止まりが「もう1台のロボット」23 回）。
        /// 待機場所は1台に1つなので、重ならない。
        /// 料理の割り当て（AssignRobots）はこの前に走るので、載せる台はそのまま走り出す。
        /// </summary>
        void ParkIdleRobots()
        {
            if (store == null) return;
            IList<Vector3> spots = store.RobotStandbyPoints;
            if (spots.Count == 0) return;

            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
                    r.CurrentPhase != ServingRobotController.Phase.Done) continue;

                Vector3 spot = spots[i % spots.Count];
                r.SetStandbyPoint(spot);   // 戻りの線の終わりに使う（v5）
                r.GoStandby(spot);
            }
        }

        void Recount()
        {
            int free = 0;
            for (int i = 0; i < _tables.Count; i++) if (_tables[i].Free) free++;
            _freeTables = free;

            int d = 0;
            for (int i = 0; i < _pass.Count; i++) if (!_pass[i].taken) d++;
            _dishesWaiting = d;

            int j = 0;
            for (int i = 0; i < _jobs.Count; i++) if (!_jobs[i].taken) j++;
            _jobsWaiting = j;
        }

        // ---------------- 卓 ----------------

        /// <summary>
        /// その人数が座れる空き卓を1つ取る。無ければ null。
        /// 「入るうちでいちばん小さい卓」を選ぶので、
        ///   ・1〜2人の組は空いている2人席を先に使い、無ければ4人席
        ///   ・3〜4人の組は4人席だけ
        /// になる（2人席に3人以上は座らせない）。
        /// </summary>
        public TableSlot TakeFreeTable(int groupId, int size)
        {
            TableSlot slot = FindFreeTable(size);
            if (slot == null) return null;

            slot.groupId = groupId;
            Recount();
            return slot;
        }

        /// <summary>その人数が入るうちで、いちばん席数の少ない空き卓。無ければ null</summary>
        TableSlot FindFreeTable(int size)
        {
            int want = Mathf.Max(1, size);
            TableSlot best = null;
            int bestSeats = int.MaxValue;

            for (int i = 0; i < _tables.Count; i++)
            {
                if (!_tables[i].Free) continue;
                int seats = Mathf.Max(1, _tables[i].seatCount);
                if (seats < want) continue;
                if (seats >= bestSeats) continue;
                best = _tables[i];
                bestSeats = seats;
                if (bestSeats == want) break;      // ちょうどの大きさが見つかった
            }
            return best;
        }

        /// <summary>その人数が座れる空き卓があるか</summary>
        public bool HasFreeTableFor(int size)
        {
            return FindFreeTable(size) != null;
        }

        /// <summary>慣らしのために、卓を使っている状態で始める</summary>
        public TableSlot TakeTableAt(int index, int groupId)
        {
            if (index < 0 || index >= _tables.Count) return null;
            if (!_tables[index].Free) return null;
            _tables[index].groupId = groupId;
            Recount();
            return _tables[index];
        }

        /// <summary>片付けが終わって卓が空く</summary>
        public void ReleaseTable(TableSlot slot, float leftS)
        {
            if (slot == null) return;
            slot.groupId = -1;
            slot.clearing = false;
            if (leftS >= 0f) turnMin.Add((NowS - leftS) / 60f);
            Recount();
        }

        public void RecordSeatWait(float arriveS)
        {
            seatWaitMin.Add(Mathf.Max(0f, (NowS - arriveS) / 60f));
        }

        /// <summary>
        /// 入店待ちの内訳を1組ぶん残す（v5 3章）。卓に着いた時に GroupManager が呼ぶ。
        /// 分け方はサイトの試算（haizen_sim.py）と同じ。
        ///   entryWaitS   … 店に来てから卓に着くまで（案内に歩いた時間と guide_s を含む）
        ///   seatWaitS    … 店に来てから案内が始まるまで
        ///   blockedWaitS … そのうち、卓は空いたのに片付けが終わらず待った分
        ///   guideWaitS   … 案内の仕事が板に出てから、スタッフが取るまで
        /// full_wait は seat_wait − blocked_wait、staff_wait は blocked_wait ＋ guide_wait。
        /// </summary>
        public void RecordEntryWait(float entryWaitS, float seatWaitS,
                                    float blockedWaitS, float guideWaitS)
        {
            float blocked = Mathf.Clamp(blockedWaitS, 0f, Mathf.Max(0f, seatWaitS));
            float full = Mathf.Max(0f, seatWaitS - blocked);
            float guide = Mathf.Max(0f, guideWaitS);

            entryWaitMin.Add(Mathf.Max(0f, entryWaitS / 60f));
            fullWaitMin.Add(full / 60f);
            blockedWaitMin.Add(blocked / 60f);
            guideWaitMin.Add(guide / 60f);
            staffWaitMin.Add((blocked + guide) / 60f);
        }

        /// <summary>
        /// 片付け待ちの卓の数（v5 3章・サイトの _acc と同じ blocked_wait を数えるのに使う）。
        /// 客が帰ってから、拭き終わって使えるようになるまでの卓
        /// </summary>
        public int ClearingTableCount
        {
            get
            {
                int n = 0;
                for (int i = 0; i < _tables.Count; i++) if (_tables[i].clearing) n++;
                return n;
            }
        }

        // ---------------- 立ち往生でスタッフを呼ぶ（v5 3章）----------------

        /// <summary>
        /// 動けなくなったロボットのところへスタッフを呼ぶ。
        /// サイトの決まりで「ほかの仕事より先」なので、優先度 0 で板の先頭に出す。
        /// 受け渡し口へ移す（warp）のは、もうしない。
        /// </summary>
        public StaffJob PostRescue(ServingRobotController robot)
        {
            if (robot == null) return null;
            StaffJob j = NewJob(StaffJobKind.Rescue, 0, null, null, 0);
            j.robot = robot;
            _jobs.Insert(0, j);
            rescueCalls++;
            Recount();
            return j;
        }

        /// <summary>スタッフが着いて rescue_s 動かし終わった（ロボットが呼ぶ）</summary>
        public void RescueFinished(float waitedS)
        {
            rescueDone++;
            rescueWaitSeconds += Mathf.Max(0f, waitedS);
        }

        /// <summary>
        /// そのロボットに出ている Rescue の仕事を板から外す（往復を打ち切ったときなど）。
        /// まだ誰も取っていないものは板から消す。
        /// もう誰かが向かっている途中のものは消さず、相手の台だけ外す
        /// （歩き出したスタッフはそのまま着いて終える。消すとその人の仕事が宙に浮く）。
        /// </summary>
        public void CancelRescue(ServingRobotController robot)
        {
            for (int i = _jobs.Count - 1; i >= 0; i--)
            {
                StaffJob j = _jobs[i];
                if (j.kind != StaffJobKind.Rescue || j.robot != robot) continue;
                if (j.taken) j.robot = null;
                else _jobs.RemoveAt(i);
            }
            Recount();
        }

        // ---------------- あきらめた記録（B1）----------------

        /// <summary>あきらめた・移した・卓を空けたことを1行残す</summary>
        public void AddGiveUp(string who, string job, int tableId, Vector3 to, Vector3 at,
                              float waitedS, string action)
        {
            GiveUpRow r = new GiveUpRow();
            r.tS = NowS;
            r.who = who;
            r.job = job;
            r.tableId = tableId;
            r.toX = to.x;
            r.toZ = to.z;
            r.x = at.x;
            r.z = at.z;
            r.aisle = AisleNameAt(at);
            r.waitedS = waitedS;
            r.action = action;
            _giveUps.Add(r);

            if (action == "移した") warpedCount++;
            else if (action == "卓を空けた") tableFreedCount++;
            else gaveUpCount++;
        }

        // ---------------- 人がロボットをよけた記録 ----------------

        /// <summary>人がロボットの外周の近くまで来た1回を残す（接触に近い）</summary>
        public void AddRobotContact(Vector3 at, string who, bool careless, int robotId, float gapM)
        {
            ContactRow r = new ContactRow();
            r.tS = NowS;
            r.x = at.x;
            r.z = at.z;
            r.aisle = AisleNameAt(at);
            r.place = store != null ? store.PlaceNameAt(at) : "";
            r.who = who;
            r.careless = careless;
            r.robotId = robotId;
            r.gapM = gapM;
            _contacts.Add(r);
        }

        /// <summary>人がロボットを待ち始めた（入口で待つ・通路の中で下がる）</summary>
        public void AddRobotYieldWait() { robotYieldWaits++; }

        /// <summary>人がロボットを待っていた秒数を足す</summary>
        public void AddRobotYieldWaitSeconds(float dt) { robotYieldWaitSeconds += dt; }

        /// <summary>人が回り道した</summary>
        public void AddRobotYieldDetour() { robotYieldDetours++; }

        // ---------------- 椅子を引いた回数（B6）----------------

        /// <summary>椅子を1脚引いた。きっかけごと・卓ごとに数える</summary>
        public void AddChairPull(string reason, int tableId)
        {
            chairPulls++;

            string key = string.IsNullOrEmpty(reason) ? "その他" : reason;
            int n;
            _chairPullsByReason[key] = _chairPullsByReason.TryGetValue(key, out n) ? n + 1 : 1;
            _chairPullsByTable[tableId] = _chairPullsByTable.TryGetValue(tableId, out n) ? n + 1 : 1;
        }

        /// <summary>その点がどの通路の上かを名前で返す。細い通路を先に見る</summary>
        public string AisleNameAt(Vector3 p)
        {
            if (store == null) return "";
            string wide = "";
            IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
            for (int i = 0; i < aisles.Count; i++)
            {
                Bounds b = aisles[i].zone;
                if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
                if (aisles[i].narrow) return aisles[i].name;
                if (wide.Length == 0) wide = aisles[i].name;
            }
            return wide;
        }

        // ---------------- 料理 ----------------

        /// <summary>料理が1皿できて、受け渡し口に出る</summary>
        public DishOrder PutDish(int groupId, TableSlot slot, bool fragile)
        {
            DishOrder d = new DishOrder();
            d.id = _nextDishId++;
            d.groupId = groupId;
            d.tableIndex = slot.index;
            d.tableId = slot.tableId;
            d.readyS = NowS;
            d.fragile = fragile;
            _pass.Add(d);

            // スタッフの仕事にも出しておく。ロボットとスタッフの先に空いた方が運ぶ
            // （ロボットが運べない料理は、ロボットの取り分けで外す）
            PostJob(StaffJobKind.Serve, groupId, slot, d, 1);
            Recount();
            return d;
        }

        /// <summary>
        /// 慣らしのために、もう届いている皿として1枚作る（B3）。
        /// 受け渡し口にも仕事板にも出さず、待ち時間の記録にも入れない
        /// （慣らしの皿を 0 分として数えると dish_wait が小さく出てしまう）。
        /// </summary>
        public DishOrder SeedArrivedDish(int groupId, TableSlot slot)
        {
            DishOrder d = new DishOrder();
            d.id = _nextDishId++;
            d.groupId = groupId;
            d.tableIndex = slot != null ? slot.index : -1;
            d.tableId = slot != null ? slot.tableId : 0;
            d.readyS = NowS;
            d.arrivedS = NowS;
            d.fragile = false;
            return d;
        }

        /// <summary>届いた（ロボットでもスタッフでも）</summary>
        /// <summary>料理を持ち上げた。受け渡し口で待った時間・運んでいた時間を出すために時刻を残す</summary>
        void MarkTaken(DishOrder d)
        {
            if (d == null) return;
            d.taken = true;
            if (d.firstTakenS < 0f) d.firstTakenS = NowS;
            d.lastTakenS = NowS;
        }

        public void DishArrived(DishOrder d, bool byRobot)
        {
            if (d == null || d.arrivedS >= 0f) return;
            d.arrivedS = NowS;
            d.byRobot = byRobot;
            dishWaitMin.Add(Mathf.Max(0f, (d.arrivedS - d.readyS) / 60f));
            if (byRobot) robotDishes++; else staffDishes++;
            AddDishParts(d);
            _pass.Remove(d);
            RemoveServeJob(d);
            Recount();
        }

        /// <summary>
        /// 届けられなかった料理を、受け渡し口の待ち行列の先頭に戻す。
        /// 次に空いたロボットかスタッフが運ぶ。
        /// </summary>
        /// <summary>
        /// 届いた1皿の、届くまでの内訳を足す。
        ///   受け渡し口で待った ＋ 運んでいた ＋ 途中で戻されて失った ＝ できてから届くまで
        /// </summary>
        void AddDishParts(DishOrder d)
        {
            if (d == null || d.arrivedS < 0f || d.readyS < 0f) return;

            float total = Mathf.Max(0f, d.arrivedS - d.readyS);
            float passWait = d.firstTakenS >= 0f ? Mathf.Max(0f, d.firstTakenS - d.readyS) : total;
            float carry = d.lastTakenS >= 0f ? Mathf.Max(0f, d.arrivedS - d.lastTakenS) : 0f;

            DishParts p = new DishParts();
            p.byRobot = d.byRobot;
            p.passWaitS = passWait;
            p.carryS = carry;
            p.requeueS = Mathf.Max(0f, total - passWait - carry);
            p.requeued = d.requeued;
            _dishParts.Add(p);
        }

        public void RequeueDish(DishOrder d)
        {
            RequeueDish(d, 0, "");
        }

        /// <summary>戻した理由も残す（B8）。robotId 0 はロボット以外</summary>
        public void RequeueDish(DishOrder d, int robotId, string why)
        {
            if (d == null || d.arrivedS >= 0f) return;
            d.taken = false;
            d.requeued++;
            requeuedDishes++;

            RequeueRow r = new RequeueRow();
            r.tS = NowS;
            r.dishId = d.id;
            r.tableId = d.tableId;
            r.robotId = robotId;
            r.fragile = d.fragile;
            r.readyS = d.readyS;
            r.requeuedSoFar = d.requeued;
            r.why = string.IsNullOrEmpty(why) ? "着かなかった" : why;
            _requeues.Add(r);

            _pass.Remove(d);
            _pass.Insert(0, d);

            TableSlot slot = SlotOf(d.tableIndex);
            RemoveServeJob(d);
            if (slot != null) PostJobFront(StaffJobKind.Serve, d.groupId, slot, d, 1);
            Recount();
        }

        TableSlot SlotOf(int index)
        {
            return index >= 0 && index < _tables.Count ? _tables[index] : null;
        }

        // ---------------- スタッフの仕事板 ----------------

        public StaffJob PostJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = NewJob(kind, groupId, slot, dish, priority);
            _jobs.Add(j);
            Recount();
            return j;
        }

        StaffJob PostJobFront(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = NewJob(kind, groupId, slot, dish, priority);
            _jobs.Insert(0, j);
            Recount();
            return j;
        }

        StaffJob NewJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = new StaffJob();
            j.kind = kind;
            j.priority = priority;
            j.groupId = groupId;
            j.tableIndex = slot != null ? slot.index : -1;
            j.tableId = slot != null ? slot.tableId : 0;
            j.dish = dish;
            j.postedS = NowS;
            return j;
        }

        /// <summary>
        /// 次にやる仕事を1つ取る。優先度が小さいものが先、同じなら出た順。
        /// 配膳の仕事は、料理がまだ誰も持っていないものだけ。
        /// 立ち往生（Rescue）は優先度 0 なので、ほかの仕事より先に取られる。
        /// </summary>
        public StaffJob TakeNextJob()
        {
            StaffJob best = null;
            for (int i = 0; i < _jobs.Count; i++)
            {
                StaffJob j = _jobs[i];
                if (j.taken) continue;
                if (j.kind == StaffJobKind.Serve && (j.dish == null || j.dish.taken || j.dish.arrivedS >= 0f)) continue;
                // 店員側（レジの内側）は1人ぶんなので、会計はひとつずつ
                if (j.kind == StaffJobKind.Pay && RegisterBackBusy) continue;
                if (best == null || j.priority < best.priority) best = j;
                if (best != null && best.priority <= 0) break;   // 優先0（立ち往生）が見つかれば、それが先
            }
            if (best == null) return null;
            best.taken = true;
            best.takenS = NowS;
            if (best.kind == StaffJobKind.Pay) RegisterBackBusy = true;
            RecordJobWait(best);
            if (best.kind == StaffJobKind.Serve && best.dish != null) MarkTaken(best.dish);
            Recount();
            return best;
        }

        /// <summary>仕事が終わった</summary>
        public void FinishJob(StaffJob job)
        {
            if (job == null) return;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            _jobs.Remove(job);
            Recount();
            if (job.onDone != null) job.onDone();
        }

        /// <summary>
        /// 仕事を板から外すだけ（終わったときの呼び出しはしない）。
        /// 卓を強引に空けたときなど、出した側がもう待っていないときに使う。
        /// </summary>
        public void FinishJobSilently(StaffJob job)
        {
            if (job == null) return;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            job.onDone = null;
            _jobs.Remove(job);
            if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
            Recount();
        }

        /// <summary>仕事を手放す（届けられなかったときなど）</summary>
        public void AbandonJob(StaffJob job)
        {
            if (job == null) return;
            job.taken = false;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
            Recount();
        }

        void RemoveServeJob(DishOrder d)
        {
            for (int i = _jobs.Count - 1; i >= 0; i--)
                if (_jobs[i].kind == StaffJobKind.Serve && _jobs[i].dish == d) _jobs.RemoveAt(i);
        }

        // ---------------- ロボットに渡す ----------------

        /// <summary>
        /// 空いているロボットに、受け渡し口の料理を段の数までまとめて渡す。
        /// 近い卓から回る（試算の _robot_run と同じ）。
        /// </summary>
        void AssignRobots()
        {
            if (spec == null || store == null) return;

            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
                    r.CurrentPhase != ServingRobotController.Phase.Done) continue;

                List<DishOrder> batch = PickBatch();
                if (batch.Count == 0) continue;

                List<ServingRobotController.RunStop> stops = new List<ServingRobotController.RunStop>();
                for (int k = 0; k < batch.Count; k++)
                {
                    TableSlot s = SlotOf(batch[k].tableIndex);
                    if (s == null) continue;
                    ServingRobotController.RunStop stop = new ServingRobotController.RunStop();
                    stop.tableId = s.tableId;
                    stop.point = s.deliveryPoint;
                    stop.aisleEntrance = s.aisleEntrance;
                    stop.aisleZone = s.aisleZone;
                    stops.Add(stop);
                }
                if (stops.Count == 0) continue;

                _carrying[r] = batch;
                r.StartRun(stops, store.PassPoint, NowS);
            }
        }

        /// <summary>段の数までまとめる。近い卓から順に並べる</summary>
        List<DishOrder> PickBatch()
        {
            List<DishOrder> batch = new List<DishOrder>();
            Vector3 pass = store.PassPoint;
            int trays = Mathf.Max(1, spec.trays);

            for (int i = 0; i < _pass.Count && batch.Count < trays; i++)
            {
                DishOrder d = _pass[i];
                if (d.taken || d.arrivedS >= 0f) continue;
                if (!spec.fragileByRobot && d.fragile) continue;   // 人だけが運ぶ料理
                batch.Add(d);
            }

            batch.Sort(delegate (DishOrder a, DishOrder b)
            {
                TableSlot sa = SlotOf(a.tableIndex), sb = SlotOf(b.tableIndex);
                float da = sa != null ? Flat(sa.deliveryPoint - pass) : 0f;
                float db = sb != null ? Flat(sb.deliveryPoint - pass) : 0f;
                return da.CompareTo(db);
            });

            for (int i = 0; i < batch.Count; i++)
            {
                MarkTaken(batch[i]);
                RemoveServeJob(batch[i]);
            }
            return batch;
        }

        static float Flat(Vector3 d) { d.y = 0f; return d.magnitude; }

        /// <summary>
        /// ロボットの1走りが終わった。届けた料理を記録し、
        /// 届けられなかった料理は受け渡し口の先頭に戻す。
        /// </summary>
        void OnRobotRunDone(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (!_carrying.TryGetValue(r, out batch) || batch == null) return;

            _lastBatch[r] = batch;      // trips.csv を書くために残す
            _carrying[r] = new List<DishOrder>();

            // なぜ届けられなかったか。ロボットが持っている結果から決める。
            // v5 3章：動けなくなってもスタッフが動かせば走りは続くので、
            // 「動けなくなった」は打ち切り・取りやめより後ろに置く（往復が終わった理由が先）
            string why = r.TimedOut ? "打ち切り"
                       : r.Aborted ? "取りやめ"
                       : r.Stuck ? "動けなくなった"
                       : "着かなかった";

            int done = Mathf.Clamp(r.StopsCompleted, 0, batch.Count);
            for (int i = 0; i < batch.Count; i++)
            {
                if (i < done) DishArrived(batch[i], true);
                else RequeueDish(batch[i], r.robotId, why);
            }
        }

        /// <summary>そのロボットがいま載せている料理</summary>
        public IList<DishOrder> CarriedBy(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (r != null && _carrying.TryGetValue(r, out batch)) return batch;
            return new List<DishOrder>();
        }

        /// <summary>直前の1走りで載せていた料理（trips.csv を書くのに使う）</summary>
        public IList<DishOrder> LastBatchOf(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (r != null && _lastBatch.TryGetValue(r, out batch) && batch != null) return batch;
            return new List<DishOrder>();
        }
    }
}
