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; // 受け渡し口に戻された回数 } /// スタッフの仕事。優先は試算の 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行) 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; 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; [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>(); 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; } } /// 人がロボットを待った回数(入口で待つ+通路の中で下がる) 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; } } // ---- 仕事が板で待った秒数(仕事ごと)---- 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)---- /// 動けなくなって、スタッフを呼んだ回数 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(); 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 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 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; } public void ClearRobots() { for (int i = 0; i < _robots.Count; i++) if (_robots[i] != null) _robots[i].TripCompleted -= OnRobotRunDone; _robots.Clear(); _carrying.Clear(); } /// 店の中の時刻を進める(ExperimentRunner が毎コマ呼ぶ) public void Advance(float dt) { NowS += dt; AssignRobots(); 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; } // ---------------- 卓 ---------------- /// /// その人数が座れる空き卓を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++; } // ---------------- 椅子を引いた回数(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皿できて、受け渡し口に出る 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; } /// /// 慣らしのために、もう届いている皿として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); } 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); 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; } /// /// 次にやる仕事を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(); if (batch.Count == 0) continue; List stops = 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); } if (stops.Count == 0) continue; _carrying[r] = batch; r.StartRun(stops, store.PassPoint, NowS); } } /// 段の数までまとめる。近い卓から順に並べる List PickBatch() { List batch = new List(); 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; } /// /// ロボットの1走りが終わった。届けた料理を記録し、 /// 届けられなかった料理は受け渡し口の先頭に戻す。 /// void OnRobotRunDone(ServingRobotController r) { List batch; if (!_carrying.TryGetValue(r, out batch) || batch == null) return; _lastBatch[r] = batch; // trips.csv を書くために残す _carrying[r] = new List(); // なぜ届けられなかったか。ロボットが持っている結果から決める。 // 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); } } /// そのロボットがいま載せている料理 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(); } } }