using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEngine; namespace ServiceSim { /// /// 時間帯ごとの記録(依頼 v10b の 2。2026-10-25)。 /// /// 店の中の時間で 60 秒ごとに、そのコマの様子をそのまま 1 行書く(平均にしない)。 /// 「ピークのどの時間帯に、誰が何をしていて、何が板に残っていたか」を /// 試算と並べて調べるための記録で、店の回し方は 1 つも変えない。 /// /// 1 回の出力フォルダに timeline.csv が 1 つできる(段階Bと Site Run で同じ作り)。 /// /// 卓の数え方は TableSlot の中身での場合分けなので、取りこぼし・二重数えが出ない: /// tables_free groupId < 0 かつ clearing でない(TableSlot.Free) /// tables_dirty_untaken clearing で、片付けの仕事を誰も取っていない /// tables_clearing clearing で、片付けの仕事をスタッフが取っている /// seated_groups groupId ≧ 0 かつ clearing でない(組が座っている卓) /// この 4 つを足すと、いつでも卓の数になる(③42・⑦48)。CheckSums で確かめる。 /// /// スタッフも同じで、StaffAgent.All を「仕事なし(待機)」と仕事の種類 6 つに分ける /// (Guide・Serve・Call・Pay・Clear・Rescue。StaffJobKind のぜんぶ)。 /// 足すとスタッフの人数になる。 /// public class TimelineLogger { /// 60 秒ごとの 1 行 public struct Row { public float tS; public int arrived, waitingGroups, seatedGroups, payWaitingSeat, payInLine; public int tablesFree, tablesDirtyUntaken, tablesClearing; public int boardGuide, boardServe, boardCall, boardPay, boardClear; public int staffIdle, staffGuide, staffServe, staffCall, staffPay, staffClear, staffRescue; public int robotsBusy; /// 卓の 4 つの数え方の合計(卓の数と同じになるはず) public int TableSum { get { return seatedGroups + tablesFree + tablesDirtyUntaken + tablesClearing; } } /// スタッフの人数の合計(スタッフの人数と同じになるはず) public int StaffSum { get { return staffIdle + staffGuide + staffServe + staffCall + staffPay + staffClear + staffRescue; } } } public const string Header = "condition_id,seed,t_s,arrived,waiting_groups,seated_groups," + "pay_waiting_seat,pay_in_line," + "tables_free,tables_dirty_untaken,tables_clearing," + "board_guide,board_serve,board_call,board_pay,board_clear," + "staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue," + "robots_busy"; readonly List _rows = new List(); public int RowCount { get { return _rows.Count; } } public IReadOnlyList Rows { get { return _rows; } } public void Clear() { _rows.Clear(); } // ---------------- 1 行ぶん数える ---------------- /// /// そのコマの様子を 1 行足す。ExperimentRunner が 60 秒ごとに呼ぶ。 /// 数えるだけで、何も書き換えない /// public void Sample(float tS, GroupManager groups, ServiceOps ops) { if (groups == null || ops == null) return; Row r = Build(tS, groups.GroupsArrived, groups.WaitingGroups, ops.Tables, ops.Jobs, groups.Groups, CountRobotsBusy(ops)); CountStaff(StaffAgent.All, ref r); _rows.Add(r); } /// /// 1 行ぶんを組み立てる(スタッフ以外)。 /// MonoBehaviour を触らないので、Unity 抜きでも試せる(確かめ用に public)。 /// スタッフの数は、呼んだ側が CountStaff か AddStaff で足す /// public static Row Build(float tS, int arrived, int waitingGroups, IList tables, IList jobs, IList groups, int robotsBusy) { Row r = new Row(); r.tS = tS; r.arrived = arrived; r.waitingGroups = waitingGroups; r.robotsBusy = robotsBusy; CountTables(tables, jobs, ref r); CountGroups(groups, ref r); CountBoard(jobs, ref r); return r; } /// 組み立てた 1 行を足す(Sample と、確かめの試しが使う) public void AddRow(Row r) { _rows.Add(r); } /// /// 卓を 4 つに分ける。clearing の卓は、片付けの仕事が取られているかで分ける /// (取られていない=ふつうの片付け待ち。これが長いと卓が回らない)。 /// /// MonoBehaviour を触らないので、Unity 抜きでも試せる(確かめ用に public) /// public static void CountTables(IList tables, IList jobs, ref Row r) { for (int i = 0; i < tables.Count; i++) { TableSlot t = tables[i]; if (t == null) continue; if (t.clearing) { if (ClearJobTaken(jobs, t.index)) r.tablesClearing++; else r.tablesDirtyUntaken++; } else if (t.groupId >= 0) r.seatedGroups++; else r.tablesFree++; } } /// その卓の片付けの仕事を、スタッフが取っているか static bool ClearJobTaken(IList jobs, int tableIndex) { for (int i = 0; i < jobs.Count; i++) { StaffJob j = jobs[i]; if (j == null || j.kind != StaffJobKind.Clear) continue; if (j.tableIndex != tableIndex) continue; if (j.taken) return true; } return false; } /// /// 会計を待っている組を 2 つに分ける。 /// pay_waiting_seat 食べ終えたが、レジの並ぶ点が取れず席で待っている /// pay_in_line 並ぶ点に立っている(会計の最中も入る) /// レジの前に人を立たせない版(payAtRegisterByOne が false)では /// 並ぶ点を取らないので、ぜんぶ pay_waiting_seat に入る /// static void CountGroups(IList gs, ref Row r) { if (gs == null) return; for (int i = 0; i < gs.Count; i++) { GroupManager.Group g = gs[i]; if (g == null || g.state != GroupManager.GState.Paying) continue; if (g.payerSpot >= 0) r.payInLine++; else r.payWaitingSeat++; } } /// 板に載っていて、まだ誰も取っていない仕事の数(種類ごと。確かめ用に public) public static void CountBoard(IList jobs, ref Row r) { for (int i = 0; i < jobs.Count; i++) { StaffJob j = jobs[i]; if (j == null || j.taken) continue; switch (j.kind) { case StaffJobKind.Guide: r.boardGuide++; break; case StaffJobKind.Serve: r.boardServe++; break; case StaffJobKind.Call: r.boardCall++; break; case StaffJobKind.Pay: r.boardPay++; break; case StaffJobKind.Clear: r.boardClear++; break; } } } /// いまその仕事をしているスタッフの人数(仕事を持っていない人は待機) static void CountStaff(IReadOnlyList all, ref Row r) { for (int i = 0; i < all.Count; i++) { StaffAgent a = all[i]; if (a == null) continue; AddStaff(a.CurrentJob, ref r); } } /// /// スタッフ1人を、持っている仕事の種類の欄に1つ足す(仕事が無ければ待機)。 /// StaffJobKind のどの値でも必ず1つだけ増えるので、 /// staff_* の合計はスタッフの人数と同じになる(確かめ用に public) /// public static void AddStaff(StaffJob j, ref Row r) { if (j == null) { r.staffIdle++; return; } switch (j.kind) { case StaffJobKind.Guide: r.staffGuide++; break; case StaffJobKind.Serve: r.staffServe++; break; case StaffJobKind.Call: r.staffCall++; break; case StaffJobKind.Pay: r.staffPay++; break; case StaffJobKind.Clear: r.staffClear++; break; case StaffJobKind.Rescue: r.staffRescue++; break; default: r.staffIdle++; break; // 種類が増えたら待機に入れて、合計を崩さない } } /// /// 走っている・卓で待っているロボットの台数。 /// robot_util と同じ見方(待機場所に下がっている Idle と、 /// 往復を終えた Done は「空いている」) /// static int CountRobotsBusy(ServiceOps ops) { int n = 0; IList rs = ops.Robots; for (int i = 0; i < rs.Count; i++) { ServingRobotController rb = rs[i]; if (rb == null) continue; ServingRobotController.Phase p = rb.CurrentPhase; if (p != ServingRobotController.Phase.Idle && p != ServingRobotController.Phase.Done) n++; } return n; } // ---------------- 確かめ(依頼 v10b の 3 A)---------------- /// /// どの行でも、卓の 4 つの合計が卓の数と同じで、 /// スタッフの人数の合計がスタッフの人数と同じになっているか。 /// 合わない行があれば、いちばん早い行を why に書く /// public bool CheckSums(int tables, int staff, out int badTableRows, out int badStaffRows, out string why) { badTableRows = 0; badStaffRows = 0; why = ""; string firstBad = ""; for (int i = 0; i < _rows.Count; i++) { Row r = _rows[i]; bool tOk = tables <= 0 || r.TableSum == tables; bool sOk = staff <= 0 || r.StaffSum == staff; if (!tOk) badTableRows++; if (!sOk) badStaffRows++; if ((!tOk || !sOk) && firstBad.Length == 0) firstBad = "t_s " + F(r.tS) + " で 卓 " + r.TableSum + "(座 " + r.seatedGroups + "・空 " + r.tablesFree + "・片付け待ち " + r.tablesDirtyUntaken + "・片付け中 " + r.tablesClearing + ")/ " + tables + "、スタッフ " + r.StaffSum + " / " + staff; } why = firstBad; return badTableRows == 0 && badStaffRows == 0; } // ---------------- 書き出し ---------------- public void Write(string path, string conditionId, int seed) { StringBuilder sb = new StringBuilder(); sb.Append(Header).Append('\n'); for (int i = 0; i < _rows.Count; i++) { Row r = _rows[i]; sb.Append(conditionId).Append(',').Append(seed).Append(',') .Append(F(r.tS)).Append(',') .Append(r.arrived).Append(',') .Append(r.waitingGroups).Append(',') .Append(r.seatedGroups).Append(',') .Append(r.payWaitingSeat).Append(',') .Append(r.payInLine).Append(',') .Append(r.tablesFree).Append(',') .Append(r.tablesDirtyUntaken).Append(',') .Append(r.tablesClearing).Append(',') .Append(r.boardGuide).Append(',') .Append(r.boardServe).Append(',') .Append(r.boardCall).Append(',') .Append(r.boardPay).Append(',') .Append(r.boardClear).Append(',') .Append(r.staffIdle).Append(',') .Append(r.staffGuide).Append(',') .Append(r.staffServe).Append(',') .Append(r.staffCall).Append(',') .Append(r.staffPay).Append(',') .Append(r.staffClear).Append(',') .Append(r.staffRescue).Append(',') .Append(r.robotsBusy).Append('\n'); } File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false)); } static string F(float v) { return v.ToString("0.###", CultureInfo.InvariantCulture); } } }