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