using System.Collections.Generic; using UnityEngine; namespace ServiceSim { /// 料理1皿 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;// 卓に届いた時刻 /// はじめて誰かが持ち上げた時刻(受け渡し口で待った時間を出すため) public float firstTakenS = -1f; /// 最後に持ち上げられた時刻(運んでいた時間を出すため) public float lastTakenS = -1f; public bool fragile; // デザート・ドリンク public bool taken; // 誰かが持っている public bool byRobot; // ロボットが運んだ public int requeued; // 受け渡し口に戻された回数 /// /// 同じ通路に2台を集めないために、後に回した皿か(v5・A)。 /// 数を1皿につき1回だけ数えるための印(毎コマ数えないため) /// public bool deferredForAisle; /// /// スタッフも運べるようになった時刻(店の中, 秒)。まだなら -1(サイトの試算 v2.7)。 /// /// 料理ができてから robot_first_wait_s のあいだは、ロボットだけが運ぶ。 /// その時間を過ぎたら、ここに時刻が入り、スタッフの仕事板にも出る /// (そこからはロボットとスタッフの先に空いた方が運ぶ)。 /// public float staffEligibleS = -1f; /// /// はじめからスタッフも運べる皿か(サイトの試算 v2.7)。 /// 割れ物を人が運ぶ設定(fragile_by=staff)の倒れやすい料理と、ロボット0台のとき。 /// この皿は「ロボットを待っていた皿」として数えない /// (待つ決まりがはじめから当てはまらないため) /// public bool staffFromStart; /// /// ロボットだけを待つ時間を過ぎたか(サイトの試算 v2.7)。 /// staffFromStart の皿では立たない /// public bool robotWaitExpired; } /// スタッフの仕事。優先は試算の staff_job と同じ(1=案内・配膳・呼び出し・会計、2=片付け) /// /// スタッフの仕事の種類。 /// Rescue(v5 3章)は、動けなくなったロボットのところへ歩いて行って rescue_s 動かす仕事。 /// サイトの決まりで「ほかの仕事より先」なので、優先度は 0(ほかは 1 と 2)。 /// 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; /// スタッフが取った時刻(店の中, 秒)。まだなら -1。入店待ちの内訳に使う public float takenS = -1f; /// /// この仕事を何回あきらめたか(v5 3章)。**仕事ごと**に数える。 /// 人ごとに数えていたときは、仕事が人から人へ回ると誰も上限(staffGiveUpMaxTries)に /// 届かず、「3回で移して済ませる」逃がし弁が働かないまま回り続けた。 /// public int tries; /// 仕事が終わったときに呼ばれる(出した側が入れる) public System.Action onDone; } /// /// あきらめた・移した・卓を空けた、の1回ぶん(give_ups.csv に1行)。 /// 「移した」「卓を空けた」が多いときは、人が通れない店になっているしるし。 /// 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; // あきらめた / 移した / 卓を空けた } /// /// 人がロボットに近づきすぎた1回ぶん(contacts.csv に1行)。 /// ロボットの外周から PeopleSpec.robotContactGapM 以内に入ったとき。 /// 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。負なら重なった) } /// /// 届いた料理1皿ぶん(dishes.csv に1行)。サイトの試算 v2.7 の割り当ての決まりを確かめるため。 /// 「誰が運んだか」「受け渡し口で待った秒数」「2分を過ぎて人が拾ったか」を残す。 /// public struct DishRow { public int dishId; public int tableId; public int groupId; public bool fragile; /// 料理ができた時刻(店の中, 秒) public float readyS; /// はじめて誰かが持ち上げた時刻 public float firstTakenS; /// 卓に届いた時刻 public float arrivedS; /// 誰が運んだか(ロボット / スタッフ) public bool byRobot; /// 受け渡し口で待った秒数(できてから、はじめて持ち上げられるまで) public float passWaitS; /// 運んでいた秒数(最後に持ち上げられてから、卓に届くまで) public float carryS; /// 途中で戻されて失った秒数 public float requeueS; /// 受け渡し口に戻された回数 public int requeued; /// はじめからスタッフも運べる皿だったか(fragile_by=staff・ロボット0台) public bool staffFromStart; /// ロボットだけを待つ時間(robot_first_wait_s)を過ぎたか public bool robotWaitExpired; /// /// 2分(robot_first_wait_s)を過ぎて人が拾った皿か。 /// robotWaitExpired かつスタッフが運んだもの(staffFromStart の皿は数えない) /// public bool lateStaffPickup; /// 同じ通路に2台を集めない決まりで後に回された皿か(v5・A) public bool deferredForAisle; } /// /// 誰が誰に譲った1回ぶん(yields.csv に1行。2026-10-23)。 /// 譲る順番は RobotSpec(ロボット>スタッフ>客)。 /// 譲り終わったとき(相手が通り過ぎた・待ちすぎてやめた)に1行足す。 /// public struct YieldRow { /// 店の中の時刻(秒)。譲り**始めた**時刻 public float tS; /// 譲った側(ロボット1号機 / スタッフ:名前 / 客:名前) public string who; /// 譲った側の順番(0=ロボット・1=スタッフ・2=客) public int rank; /// 相手(ロボット2号機 など) public string toWhom; /// 譲り始めた位置 public float x, z; /// そのときいた通路(無ければ空) public string aisle; /// そのときいた場所(レジ・待合など。無ければ空) public string place; /// 受け渡し口と客席をつなぐ角の中だったか public bool inCorner; /// 譲って待った秒数 public float waitedS; /// 相手の道から横へどけたか(false なら来た道へ戻って待った) public bool steppedAside; /// 相手が通り過ぎて終わったか(false なら待ちすぎてやめた) public bool passed; } /// 届けられずに受け渡し口へ戻した1回ぶん(requeues.csv に1行) 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; // 動けなくなった / 打ち切り / 取りやめ / 着かなかった } /// 卓1つの状態 public class TableSlot { public int index; public int tableId; public Vector3 deliveryPoint; public Vector3 aisleEntrance; public Bounds aisleZone; /// この卓へ行くときに通る細い通路の番号(店の形の aisles)。-1 なら通らない public int aisleIndex = -1; public Vector3 seatPoint; /// 何人かけの卓か(2人席なら2、4人席なら4) public int seatCount = 4; public int groupId = -1; // -1 なら空き public bool clearing; // 片付け中(まだ使えない) public bool Free { get { return groupId < 0 && !clearing; } } } /// /// 店の回し方の中心。卓の空き、受け渡し口の料理の待ち行列、スタッフの仕事板を持ち、 /// 空いたロボットに料理をまとめて渡す(試算の deliver / _robot_run と同じ決まり)。 /// /// この script は店の運営だけを扱う。人の歩き方はそれぞれの script、 /// ロボットの止まる決まりは ServingRobotController に任せる。 /// 数字は ServiceSpec の .asset に置く。 /// public class ServiceOps : MonoBehaviour { [Header("設定")] public ServiceSpec spec; public RobotSpec robotSpec; [Tooltip("卓と通路を読む先")] public StoreBuilder store; [Header("いまの様子(読むだけ)")] [SerializeField] int _freeTables; [SerializeField] int _dishesWaiting; /// 受け渡し口でロボットを待っている皿(サイトの試算 v2.7) [SerializeField] int _dishesWaitingForRobot; [SerializeField] int _jobsWaiting; readonly List _tables = new List(); readonly List _pass = new List(); readonly List _jobs = new List(); readonly List _robots = new List(); readonly Dictionary> _carrying = new Dictionary>(); readonly Dictionary> _lastBatch = new Dictionary>(); /// /// 止まる所の番号 → その所で降ろす皿(依頼 v8 の 1)。 /// /// StartRun に渡す stops を作るループは SlotOf が null の皿を飛ばすので、 /// 「止まる所の番号=batch の番号」とは限らない。だからこの対応表で引く。 /// readonly Dictionary> _stopDishes = new Dictionary>(); int _nextDishId = 1; // ---- 記録(TripLogger と ExperimentRunner が読む)---- public readonly List dishWaitMin = new List(); // 料理ができてから届くまで(分) public readonly List seatWaitMin = new List(); // 入店から案内が始まるまで(分) public readonly List turnMin = new List(); // 退店から卓が空くまで(分) // ---- 入店待ちの内訳(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 を含む) /// entry_wait_mean:店に来てから案内されて卓に着くまで(分) public readonly List entryWaitMin = new List(); /// full_wait_mean:席待ち − blocked_wait(分) public readonly List fullWaitMin = new List(); /// blocked_wait_mean:卓は空いたが片付けが終わらず待った分(分) public readonly List blockedWaitMin = new List(); /// guide_wait_mean:案内の仕事が板に出てから取られるまで(分) public readonly List guideWaitMin = new List(); /// staff_wait_mean:blocked_wait + guide_wait(分) public readonly List staffWaitMin = new List(); public int robotDishes { get; private set; } public int staffDishes { get; private set; } public int requeuedDishes { get; private set; } // ---- あきらめた記録(B1)---- readonly List _giveUps = new List(); public IReadOnlyList GiveUps { get { return _giveUps; } } // ---- 人がロボットをよけた記録 ---- readonly List _contacts = new List(); /// 人がロボットに近づきすぎた1回ずつ(contacts.csv) public IReadOnlyList Contacts { get { return _contacts; } } // ---- 譲り合いの記録(yields.csv・2026-10-23)---- readonly List _yields = new List(); /// 誰が誰に譲った1回ずつ(yields.csv) public IReadOnlyList Yields { get { return _yields; } } /// /// 譲り1回ぶんを残す。譲り**終わった**ときに呼ぶ(待った秒数が決まってから)。 /// startS は譲り始めた時刻、at は譲り始めた位置。 /// public void AddYield(float startS, Vector3 at, string who, int rank, string toWhom, float waitedS, bool steppedAside, bool passed) { YieldRow r = new YieldRow(); r.tS = startS; r.who = who; r.rank = rank; r.toWhom = toWhom; r.x = at.x; r.z = at.z; r.aisle = AisleNameAt(at); r.place = store != null ? store.PlaceNameAt(at) : ""; r.inCorner = robotSpec != null && robotSpec.InCorner(at); r.waitedS = Mathf.Max(0f, waitedS); r.steppedAside = steppedAside; r.passed = passed; _yields.Add(r); } /// 人がロボットを待った回数(入口で待つ+通路の中で下がる) public int robotYieldWaits { get; private set; } /// 人がロボットを待っていた秒数の合計 public float robotYieldWaitSeconds { get; private set; } /// 人が回り道した回数 public int robotYieldDetours { get; private set; } // ---- レジの前の混み具合(調べるための記録)---- /// レジの前にいた人数のいちばん多いとき public int registerCrowdMax { get; private set; } float _registerCrowdSum; int _registerCrowdSamples; // ---- 会計のときスタッフが店員側(レジの内側)にいたか ---- /// 会計をした回数 public int registerPayCount { get; private set; } /// そのうち、店員側にいなかった回数(0 になるはず) public int registerPayNotAtBackCount { get; private set; } /// 立つ点から 0.1 m より離れて会計した回数(0 になるはず) public int registerPayOffSpotCount { get; private set; } /// 立つ点と実際に立った位置の距離のいちばん大きいもの(m) public float registerPayMaxOffsetM { get; private set; } /// 立つ点からの距離の合計(平均を出すため, m) float _registerPayOffsetSum; /// /// いま店員側(レジの内側)で会計しているスタッフがいるか。 /// 店員側は 0.8 × 1.2 m で1人ぶんなので、会計の仕事は同時に1つだけ取れる。 /// public bool RegisterBackBusy { get; private set; } /// 店員側に、会計でないスタッフがいた秒数(0 になるはず) public float registerBackIntruderSeconds { get; private set; } /// 店員側に、会計でないスタッフがいた回数(0 になるはず) public int registerBackIntruderCount { get; private set; } /// 会計でないスタッフが店員側にいたことを残す(StaffAgent が毎コマ呼ぶ) public void AddRegisterBackIntruder(float dt, bool firstFrame) { registerBackIntruderSeconds += dt; if (firstFrame) registerBackIntruderCount++; } /// 立つ点と実際に立った位置の距離の平均(m) public float RegisterPayMeanOffsetM { get { return registerPayCount > 0 ? _registerPayOffsetSum / registerPayCount : 0f; } } /// /// 会計1回ぶんを数える。 /// atBack が false なら店員側にいなかった。offsetM は立つ点からの距離。 /// どちらも 0 になるはず(ならなければ、スタッフが店員側に入れていない)。 /// 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++; } /// レジの前にいた人数の平均 public float RegisterCrowdMean { get { return _registerCrowdSamples > 0 ? _registerCrowdSum / _registerCrowdSamples : 0f; } } // ---- スタッフの時間の使い方(仕事ごとの秒数)---- readonly Dictionary _staffTime = new Dictionary(); /// スタッフが何にどれだけ時間を使ったか(仕事の名前 → 秒) public IDictionary StaffTime { get { return _staffTime; } } // ---- 料理が届くまでの内訳(運んだのがロボットか人かで分ける)---- /// 届いた料理1皿ぶんの内訳(秒) public struct DishParts { public bool byRobot; public float passWaitS; // 受け渡し口で待った(できてから、はじめて持ち上げられるまで) public float carryS; // 運んでいた(最後に持ち上げられてから、卓に届くまで) public float requeueS; // 途中で戻されて失った(残り) public int requeued; // 戻された回数 } readonly List _dishParts = new List(); /// 届いた料理の、届くまでの内訳(summary.json に出す) public IReadOnlyList DishPartsList { get { return _dishParts; } } // ---- 皿ごとの記録(dishes.csv・サイトの試算 v2.7)---- readonly List _dishRows = new List(); /// 届いた料理1皿ずつの記録(dishes.csv) public IReadOnlyList DishRows { get { return _dishRows; } } /// /// 2分(robot_first_wait_s)を過ぎて人が拾った皿の数(summary.json に出す)。 /// ロボットだけを待つ時間を過ぎてから、スタッフが運んだ皿 /// public int lateStaffDishes { get; private set; } /// /// いま受け渡し口でロボットを待っている皿の数(キッチンのモニターに出す)。 /// ロボットだけを待つ時間(robot_first_wait_s)の中にいて、まだ誰も持っていない皿。 /// はじめからスタッフも運べる皿(fragile_by=staff・ロボット0台)は数えない /// public int DishesWaitingForRobot { get { return _dishesWaitingForRobot; } } // ---- 仕事が板で待った秒数(仕事ごと)---- readonly Dictionary _jobWaitSum = new Dictionary(); readonly Dictionary _jobWaitCount = new Dictionary(); /// 仕事が板に出てから取られるまでの秒数の合計(仕事の名前 → 秒) public IDictionary JobWaitSum { get { return _jobWaitSum; } } /// 仕事が取られた回数(仕事の名前 → 回) public IDictionary 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; } /// スタッフの時間を足す(StaffAgent が毎コマ呼ぶ) 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 _regQueues = new List(); readonly List _regQueueUsed = new List(); /// レジの並ぶ点を1つ取る。無ければ -1(会計に立つ人も席で待つ) 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; } /// 取ったレジの並ぶ点を空ける 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)---- /// /// 同じ通路に2台を集めないために、料理を後に回した回数(v5・A)。 /// 多いときは、2台が同じ通路の卓ばかりを受け持とうとしているしるし /// public int aisleDeferrals { get; private set; } /// 動けなくなって、スタッフを呼んだ回数 public int rescueCalls { get; private set; } /// そのうち、スタッフが着いて動かした回数 public int rescueDone { get; private set; } /// スタッフが着くまでロボットが待った秒数の合計 public float rescueWaitSeconds { get; private set; } // ---- ロボットのふさがっている割合(robot_util)---- float _robotBusyS, _robotAliveS; /// robot_util:ロボットが空いていなかった時間の割合 public float RobotUtil { get { return _robotAliveS > 0.01f ? _robotBusyS / _robotAliveS : 0f; } } // ---- スタッフが歩いた距離(walk_km_per_staff)---- float _staffWalkM; int _staffCount; /// スタッフ1人が歩いた距離(km) public float WalkKmPerStaff { get { return _staffCount > 0 ? _staffWalkM / 1000f / _staffCount : 0f; } } /// スタッフの人数を伝える(walk_km_per_staff を出すため) public void SetStaffCount(int n) { _staffCount = n; } /// スタッフが1コマで歩いた距離を足す(StaffAgent が呼ぶ) public void AddStaffWalk(float m) { if (m > 0f && m < 1f) _staffWalkM += m; // 1 m を超える跳びは「移した」ぶんなので数えない } // ---- 戻した皿の記録(B8)---- readonly List _requeues = new List(); public IReadOnlyList Requeues { get { return _requeues; } } // ---- 椅子を引いた回数(B6)---- readonly Dictionary _chairPullsByReason = new Dictionary(); readonly Dictionary _chairPullsByTable = new Dictionary(); public int chairPulls { get; private set; } public IDictionary ChairPullsByReason { get { return _chairPullsByReason; } } public IDictionary ChairPullsByTable { get { return _chairPullsByTable; } } // ---- ドリンクバーの場所取り(B4)---- readonly List _dbStands = new List(); readonly List _dbQueues = new List(); readonly List _dbStandUsed = new List(); readonly List _dbQueueUsed = new List(); /// 並ぶ点に回された回数 public int drinkBarQueued { get; private set; } /// 立つ点も並ぶ点も空いていなくて、あきらめた回数 public int drinkBarGaveUp { get; private set; } /// 並んで待った秒数(分で記録) public readonly List drinkBarWaitMin = new List(); public IList Robots { get { return _robots; } } public IList Tables { get { return _tables; } } public int FreeTableCount { get { return _freeTables; } } public int DishesWaiting { get { return _dishesWaiting; } } /// 店の中の時刻(秒)。ExperimentRunner が進める public float NowS { get; private set; } // ---------------- 用意 ---------------- public void Setup() { _tables.Clear(); _pass.Clear(); _jobs.Clear(); _carrying.Clear(); _stopDishes.Clear(); // 止まる所と皿の対応表(依頼 v8 の 1) _lastBatch.Clear(); dishWaitMin.Clear(); seatWaitMin.Clear(); turnMin.Clear(); entryWaitMin.Clear(); fullWaitMin.Clear(); blockedWaitMin.Clear(); guideWaitMin.Clear(); staffWaitMin.Clear(); aisleDeferrals = 0; rescueCalls = 0; rescueDone = 0; rescueWaitSeconds = 0f; _robotBusyS = 0f; _robotAliveS = 0f; _staffWalkM = 0f; robotDishes = 0; staffDishes = 0; requeuedDishes = 0; _giveUps.Clear(); _requeues.Clear(); _contacts.Clear(); _yields.Clear(); _staffTime.Clear(); _dishParts.Clear(); _dishRows.Clear(); lateStaffDishes = 0; _dishesWaitingForRobot = 0; _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; robotStuckDetours = 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 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.aisleIndex = t[i].aisleIndex; s.seatPoint = t[i].center; s.seatCount = t[i].seatCount; _tables.Add(s); } // ドリンクバーの立つ点・並ぶ点を読む(B4) SetupDrinkBar(store.PointsOf(PlaceKind.DrinkBarStand), store.PointsOf(PlaceKind.DrinkBarQueue)); // レジの前に並ぶ点を読む IList rq = store.RegisterQueuePoints; for (int i = 0; i < rq.Count; i++) { _regQueues.Add(rq[i]); _regQueueUsed.Add(false); } Recount(); } // ---------------- ドリンクバーの場所取り(B4)---------------- void SetupDrinkBar(IList stands, IList 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); } } /// /// ドリンクバーの場所を1つ取る(B4)。 /// ・空いている立つ点があれば、そこへ /// ・埋まっていれば、端の並ぶ点へ /// ・それも埋まっていれば取れない(客はあきらめて席に戻る) /// spot は向かう点、queued は並ぶ点だったか。 /// 返した番号を ReleaseDrinkBarSpot に渡して空ける。 /// 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; } /// 取った場所を空ける。waitedS は並んで待った秒数 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; } /// ドリンクバーに立つ点があるか(無ければ今までどおり1点へ行く) 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(); r.ops = this; // 立ち往生したときスタッフを呼ぶ先(v5 3章) // 待機場所を先に教える(1往復めの戻りから、戻りの線を使えるように。v5) if (store != null) { IList spots = store.RobotStandbyPoints; if (spots.Count > 0) r.SetStandbyPoint(spots[(_robots.Count - 1) % spots.Count]); } r.TripCompleted += OnRobotRunDone; r.StopArrived += OnRobotStopArrived; // 卓に着いた時点で届いたにする(依頼 v8 の 1) } public void ClearRobots() { for (int i = 0; i < _robots.Count; i++) { if (_robots[i] == null) continue; _robots[i].TripCompleted -= OnRobotRunDone; _robots[i].StopArrived -= OnRobotStopArrived; } _robots.Clear(); _carrying.Clear(); _stopDishes.Clear(); _lastBatch.Clear(); } /// 店の中の時刻を進める(ExperimentRunner が毎コマ呼ぶ) public void Advance(float dt) { NowS += dt; AssignRobots(); ReleaseDishesToStaff(); ParkIdleRobots(); SampleRegisterCrowd(); SampleRobotUtil(dt); Recount(); } /// /// ロボットがふさがっていた時間を数える(robot_util)。 /// 待機場所へ下がっているあいだ・受け渡し口で空のまま待っているあいだは Idle / Done なので /// 「空いている」。動けなくなって人を待っているあいだ(StuckWait)は「ふさがっている」。 /// 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; } } /// /// レジの前にいた人数を数える(調べるための記録)。 /// 待合に立つ客も、通路を歩く人も、スタッフも数える。 /// 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 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++; } /// /// 仕事のないロボットを待機場所へ下げる(B2)。 /// 受け渡し口の前の点にはスタッフも料理を取りに来るので、空の台を置かない。 /// 料理を載せるときは AssignRobots が受け渡し口から走り出させる。 /// /// v5:料理が待っていても下げる。 /// 前は「料理が待っているなら受け渡し口で待つ」にしていたが、そのせいで /// 出発する台と戻ってきた台が受け渡し口の前の1点に重なり、ふさぎ合っていた /// (20261023 0913 の回で、受け渡し口の前の止まりが「もう1台のロボット」23 回)。 /// 待機場所は1台に1つなので、重ならない。 /// 料理の割り当て(AssignRobots)はこの前に走るので、載せる台はそのまま走り出す。 /// void ParkIdleRobots() { if (store == null) return; IList 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; } // ---------------- 卓 ---------------- /// /// ほかの台が向かっている、または中にいる細い通路の番号(v5・A)。 /// ・まだ届けていない卓の通路(往復の途中のもの) /// ・いま体がその通路の区画の中にある台の通路(戻り道で通っているときも押さえる) /// HashSet AislesTakenByOthers(ServingRobotController self) { HashSet busy = new HashSet(); for (int i = 0; i < _robots.Count; i++) { ServingRobotController o = _robots[i]; if (o == null || o == self) continue; if (o.CurrentPhase == ServingRobotController.Phase.Idle || o.CurrentPhase == ServingRobotController.Phase.Done) continue; IReadOnlyList ids = o.TableIds; for (int k = o.StopsCompleted; k < ids.Count; k++) { TableSlot s = SlotOfTableId(ids[k]); if (s != null && s.aisleIndex >= 0) busy.Add(s.aisleIndex); } if (store == null) continue; IList aisles = store.Aisles; Vector3 p = o.transform.position; for (int a = 0; a < aisles.Count; a++) { Bounds b = aisles[a].zone; if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue; busy.Add(a); } } return busy; } TableSlot SlotOfTableId(int tableId) { for (int i = 0; i < _tables.Count; i++) if (_tables[i].tableId == tableId) return _tables[i]; return null; } /// /// その人数が座れる空き卓を1つ取る。無ければ null。 /// 「入るうちでいちばん小さい卓」を選ぶので、 /// ・1〜2人の組は空いている2人席を先に使い、無ければ4人席 /// ・3〜4人の組は4人席だけ /// になる(2人席に3人以上は座らせない)。 /// public TableSlot TakeFreeTable(int groupId, int size) { TableSlot slot = FindFreeTable(size); if (slot == null) return null; slot.groupId = groupId; Recount(); return slot; } /// その人数が入るうちで、いちばん席数の少ない空き卓。無ければ null 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; } /// その人数が座れる空き卓があるか public bool HasFreeTableFor(int size) { return FindFreeTable(size) != null; } /// 慣らしのために、卓を使っている状態で始める 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]; } /// 片付けが終わって卓が空く 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)); } /// /// 入店待ちの内訳を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。 /// 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); } /// /// 片付け待ちの卓の数(v5 3章・サイトの _acc と同じ blocked_wait を数えるのに使う)。 /// 客が帰ってから、拭き終わって使えるようになるまでの卓 /// public int ClearingTableCount { get { int n = 0; for (int i = 0; i < _tables.Count; i++) if (_tables[i].clearing) n++; return n; } } // ---------------- 立ち往生でスタッフを呼ぶ(v5 3章)---------------- /// /// 動けなくなったロボットのところへスタッフを呼ぶ。 /// サイトの決まりで「ほかの仕事より先」なので、優先度 0 で板の先頭に出す。 /// 受け渡し口へ移す(warp)のは、もうしない。 /// 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; } /// スタッフが着いて rescue_s 動かし終わった(ロボットが呼ぶ) public void RescueFinished(float waitedS) { rescueDone++; rescueWaitSeconds += Mathf.Max(0f, waitedS); } /// /// そのロボットに出ている Rescue の仕事を板から外す(往復を打ち切ったときなど)。 /// まだ誰も取っていないものは板から消す。 /// もう誰かが向かっている途中のものは消さず、相手の台だけ外す /// (歩き出したスタッフはそのまま着いて終える。消すとその人の仕事が宙に浮く)。 /// 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)---------------- /// あきらめた・移した・卓を空けたことを1行残す 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++; } // ---------------- 人がロボットをよけた記録 ---------------- /// 人がロボットの外周の近くまで来た1回を残す(接触に近い) 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); } /// 人がロボットを待ち始めた(入口で待つ・通路の中で下がる) public void AddRobotYieldWait() { robotYieldWaits++; } /// 人がロボットを待っていた秒数を足す public void AddRobotYieldWaitSeconds(float dt) { robotYieldWaitSeconds += dt; } /// 人が回り道した public void AddRobotYieldDetour() { robotYieldDetours++; } /// /// 店員優先の版の逃げ道:相手のロボットが別の理由で止まったまま /// staffFirstRobotStuckSec(60 秒)たって、店員が回り道した回数(v5) /// public int robotStuckDetours { get; private set; } public void AddRobotStuckDetour() { robotStuckDetours++; } // ---------------- 椅子を引いた回数(B6)---------------- /// 椅子を1脚引いた。きっかけごと・卓ごとに数える 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; } /// その点がどの通路の上かを名前で返す。細い通路を先に見る public string AisleNameAt(Vector3 p) { if (store == null) return ""; string wide = ""; IList 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; } // ---------------- 料理 ---------------- /// /// 料理が1皿できて、受け渡し口に出る。 /// /// 割り当ての決まりはサイトの試算 v2.7 にそろえる: /// ・まずロボットを待つ。そのあいだスタッフは料理を持たず、案内・片付け・会計を続ける /// (=スタッフの仕事板に Serve を出さない。ロボットは _pass を直に見るので受け持てる) /// ・robot_first_wait_s(common.robot_first_wait_s)を過ぎてもロボットが受け持たなければ、 /// そこからはロボットとスタッフのうち先に空いた方が運ぶ(ReleaseDishesToStaff が出す) /// ・割れ物を人が運ぶ設定(fragile_by=staff)の倒れやすい料理と、ロボット0台のときは、 /// 今までどおり、はじめからスタッフが運ぶ /// 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; d.staffFromStart = StaffCarriesFromStart(d); _pass.Add(d); if (d.staffFromStart) { // ロボットが受け持てない皿。待たせる意味がないので、はじめから仕事板に出す d.staffEligibleS = NowS; PostJob(StaffJobKind.Serve, groupId, slot, d, 1); } Recount(); return d; } /// /// はじめからスタッフも運べる皿か(サイトの試算 v2.7)。 /// ・割れ物を人が運ぶ設定(fragile_by=staff)の、倒れやすい料理 /// ・ロボットが1台もいないとき /// ・robot_first_wait_s が 0 以下のとき(v2.7 より前と同じ回し方に戻せる逃がし口) /// bool StaffCarriesFromStart(DishOrder d) { if (spec == null) return true; if (spec.robotFirstWaitSec <= 0f) return true; if (!spec.fragileByRobot && d.fragile) return true; return RobotCount == 0; } /// 動いているロボットの数(null の枠は数えない) public int RobotCount { get { int n = 0; for (int i = 0; i < _robots.Count; i++) if (_robots[i] != null) n++; return n; } } /// /// ロボットだけを待つ時間を過ぎた皿を、スタッフの仕事板にも出す(サイトの試算 v2.7)。 /// /// 待った時間は **料理ができた時(readyS)から**数える。 /// 同じ通路に2台を集めない決まり(oneRobotPerAisle)で後に回した皿も、 /// 受け渡し口に戻された皿も、この数え方は変えない /// (後に回された分だけ待ちが延びるのを、そのまま出すため)。 /// void ReleaseDishesToStaff() { if (spec == null) return; float wait = spec.robotFirstWaitSec; int waitingForRobot = 0; for (int i = 0; i < _pass.Count; i++) { DishOrder d = _pass[i]; if (d.arrivedS >= 0f) continue; // もうスタッフも運べる皿(はじめから運べる皿も含む) if (d.staffEligibleS >= 0f) continue; if (NowS - d.readyS < wait) { // まだロボットだけを待つ時間の中。誰も持っていなければ「ロボットを待っている皿」 if (!d.taken) waitingForRobot++; continue; } d.staffEligibleS = NowS; d.robotWaitExpired = true; // 誰かが持っている皿(ロボットが載せた・運んでいる途中)は、板に出さない。 // 戻されたときに RequeueDish が出し直す if (d.taken) continue; TableSlot slot = SlotOf(d.tableIndex); if (slot != null) PostJob(StaffJobKind.Serve, d.groupId, slot, d, 1); } _dishesWaitingForRobot = waitingForRobot; } /// /// 慣らしのために、もう届いている皿として1枚作る(B3)。 /// 受け渡し口にも仕事板にも出さず、待ち時間の記録にも入れない /// (慣らしの皿を 0 分として数えると dish_wait が小さく出てしまう)。 /// 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; } /// 届いた(ロボットでもスタッフでも) /// 料理を持ち上げた。受け渡し口で待った時間・運んでいた時間を出すために時刻を残す 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(); } /// /// 届けられなかった料理を、受け渡し口の待ち行列の先頭に戻す。 /// 次に空いたロボットかスタッフが運ぶ。 /// /// /// 届いた1皿の、届くまでの内訳を足す。 /// 受け渡し口で待った + 運んでいた + 途中で戻されて失った = できてから届くまで /// 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); // ---- 皿ごとの記録(dishes.csv・サイトの試算 v2.7)---- // 「2分を過ぎて人が拾った皿」は、ロボットだけを待つ時間を過ぎてから // スタッフが運んだもの。はじめからスタッフが運ぶ皿 // (fragile_by=staff・ロボット0台)は、待つ決まりが当てはまらないので数えない bool late = d.robotWaitExpired && !d.staffFromStart && !d.byRobot; DishRow row = new DishRow(); row.dishId = d.id; row.tableId = d.tableId; row.groupId = d.groupId; row.fragile = d.fragile; row.readyS = d.readyS; row.firstTakenS = d.firstTakenS; row.arrivedS = d.arrivedS; row.byRobot = d.byRobot; row.passWaitS = passWait; row.carryS = carry; row.requeueS = p.requeueS; row.requeued = d.requeued; row.staffFromStart = d.staffFromStart; row.robotWaitExpired = d.robotWaitExpired; row.lateStaffPickup = late; row.deferredForAisle = d.deferredForAisle; _dishRows.Add(row); if (late) lateStaffDishes++; } public void RequeueDish(DishOrder d) { RequeueDish(d, 0, ""); } /// 戻した理由も残す(B8)。robotId 0 はロボット以外 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); // サイトの試算 v2.7:まだロボットだけを待つ時間の中なら、板に出さずロボットを待つ。 // 待った時間は料理ができた時から数えるので、戻されても数え直さない // (過ぎていれば ReleaseDishesToStaff が staffEligibleS を入れている) if (slot != null && d.staffEligibleS >= 0f) 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; } /// /// 次にやる仕事を1つ取る。優先度が小さいものが先、同じなら出た順。 /// 配膳の仕事は、料理がまだ誰も持っていないものだけ。 /// 立ち往生(Rescue)は優先度 0 なので、ほかの仕事より先に取られる。 /// 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; } /// 仕事が終わった 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(); } /// /// 仕事を板から外すだけ(終わったときの呼び出しはしない)。 /// 卓を強引に空けたときなど、出した側がもう待っていないときに使う。 /// 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(); } /// 仕事を手放す(届けられなかったときなど) 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); } // ---------------- ロボットに渡す ---------------- /// /// 空いているロボットに、受け渡し口の料理を段の数までまとめて渡す。 /// 近い卓から回る(試算の _robot_run と同じ)。 /// 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 batch = PickBatch(r); if (batch.Count == 0) continue; // 止まる所と皿を、同じ並びで組む(依頼 v8 の 1)。 // SlotOf が null の皿は止まる所を作れないので、どちらからも外す List stops = new List(); List stopDishes = new List(); 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); stopDishes.Add(batch[k]); } if (stops.Count == 0) continue; _carrying[r] = batch; _stopDishes[r] = stopDishes; r.StartRun(stops, store.PassPoint, NowS); } } /// 段の数までまとめる。近い卓から順に並べる List PickBatch(ServingRobotController self) { List batch = new List(); Vector3 pass = store.PassPoint; int trays = Mathf.Max(1, spec.trays); // 同じ通路に2台を集めない(v5・A)。 // ほかの台が向かっている・中にいる通路の卓は、この回では受け持たない。 // 1往復で3卓まとめるときも入れない(この for で毎回見る) bool oneEach = robotSpec != null && robotSpec.oneRobotPerAisle; HashSet busy = oneEach ? AislesTakenByOthers(self) : null; 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; // 人だけが運ぶ料理 if (busy != null) { TableSlot slot = SlotOf(d.tableIndex); if (slot != null && slot.aisleIndex >= 0 && busy.Contains(slot.aisleIndex)) { // 数えるのは1皿につき1回だけ(毎コマ数えると意味のない大きさになる) if (!d.deferredForAisle) { d.deferredForAisle = true; aisleDeferrals++; } 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; } /// /// ロボットが止まる所(卓)に着いた(依頼 v8 の 1)。 /// /// サイトの試算と同じで、**卓に着いた時点でその皿は届いた**。 /// 受け渡しの 25 秒(RobotSpec.tableDwellSec)はこのあとなので、 /// 客は受け渡し口へ戻るのを待たずに食べ始められる /// (前は往復の最後にまとめて届いたにしていて、料理の待ちが /// 1皿あたり 1.0〜1.3 分長く出ていた)。 /// void OnRobotStopArrived(ServingRobotController r, int stopIndex) { List stopDishes; if (!_stopDishes.TryGetValue(r, out stopDishes) || stopDishes == null) return; if (stopIndex < 0 || stopIndex >= stopDishes.Count) return; DishArrived(stopDishes[stopIndex], true); } /// /// ロボットの1走りぶんの片づけが終わった(依頼 v8 の 2)。 /// /// ExperimentRunner は trips.csv を書くときに LastBatchOf を読むので、 /// ServingRobotController.TripCompleted を直に受けると、呼ばれる順番によって /// **1つ前の往復の皿**を読んでしまっていた。 /// ここで片づけを終えてから出すこのイベントを受ければ、順番に頼らずに済む。 /// public event System.Action RunRecorded; /// /// ロボットの1走りが終わった。 /// 卓に着いた皿はもう届いているので何もしない。 /// まだ届いていない皿(arrivedS < 0)だけを受け渡し口の先頭へ戻す。 /// void OnRobotRunDone(ServingRobotController r) { List batch; if (!_carrying.TryGetValue(r, out batch) || batch == null) return; _lastBatch[r] = batch; // trips.csv を書くために残す _carrying[r] = new List(); _stopDishes[r] = new List(); // なぜ届けられなかったか。ロボットが持っている結果から決める。 // v5 3章:動けなくなってもスタッフが動かせば走りは続くので、 // 「動けなくなった」は打ち切り・取りやめより後ろに置く(往復が終わった理由が先) string why = r.TimedOut ? "打ち切り" : r.Aborted ? "取りやめ" : r.Stuck ? "動けなくなった" : "着かなかった"; for (int i = 0; i < batch.Count; i++) { // 卓に着いた時点で DishArrived してあるので、arrivedS で見分ける // (StopsCompleted の数で数えない。止まる所と皿の並びは対応表で持っている) if (batch[i].arrivedS >= 0f) continue; RequeueDish(batch[i], r.robotId, why); } // 片づけが終わってから知らせる(ここで LastBatchOf を読んでよい) if (RunRecorded != null) RunRecorded(r); } /// そのロボットがいま載せている料理 public IList CarriedBy(ServingRobotController r) { List batch; if (r != null && _carrying.TryGetValue(r, out batch)) return batch; return new List(); } /// 直前の1走りで載せていた料理(trips.csv を書くのに使う) public IList LastBatchOf(ServingRobotController r) { List batch; if (r != null && _lastBatch.TryGetValue(r, out batch) && batch != null) return batch; return new List(); } } }