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();
}
}
}