using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;
namespace ServiceSim
{
///
/// 確かめ A(依頼 v9 の 7 章 A):人のいない店で、受け渡し口から全卓へ道を引く。
///
/// 見るもの:
/// ・狭い道を使った卓が、サイトの routes の use と同じか(③は 0 卓・⑦は 24 卓)
/// ・引いた道の長さと、サイトの routes の length の比(最小・平均・最大)
/// ・道探しの時間(平均・中央・最大)
/// ・道が引けない卓が無いこと。待機場所へも引けること
///
/// 店も人も建てずに、JSON と地図だけで回せるようにしてある。
/// Unity のメニュー(ServiceSim/Map Planner)と、Unity の外(mono)の両方から
/// **同じこの関数**を呼ぶので、出る数字は必ず同じになる。
///
public static class MapPlanCheck
{
/// 1 卓ぶんの結果
public struct Row
{
public string tableId;
public string siteUse; // サイトの routes の use(normal / narrow)
public string planUse; // 引いた道(normal / narrow)
public float siteLengthM;
public float planLengthM;
public float ratio; // 引いた道 ÷ サイトの length
public int corners;
public float planMs;
public string status;
}
/// 1 店ぶんの結果
public class Result
{
public bool ok;
public string note = "";
public string report = "";
public readonly List rows = new List();
public int narrowMine, narrowSite, failed, mismatch, snapped;
public float ratioMin, ratioMean, ratioMax;
public float planMsMean, planMsMedian, planMsMax;
public int standbyOk, standbyCount;
/// CSV(table,...)。そのまま書き出せる
public string Csv()
{
StringBuilder sb = new StringBuilder();
sb.Append("table,site_use,plan_use,site_length_m,plan_length_m,ratio,corners,plan_ms,status\n");
for (int i = 0; i < rows.Count; i++)
{
Row r = rows[i];
sb.Append(r.tableId).Append(',').Append(r.siteUse).Append(',').Append(r.planUse).Append(',')
.Append(F(r.siteLengthM)).Append(',').Append(F(r.planLengthM)).Append(',')
.Append(F(r.ratio)).Append(',').Append(r.corners).Append(',')
.Append(F(r.planMs)).Append(',').Append(r.status).Append('\n');
}
return sb.ToString();
}
}
///
/// 回す。storeJson は店の形の JSON、mapJsonText は地図の JSON。
/// spec が null なら GridPlanner.Settings.Default で引く。
///
public static Result Run(string label, string storeJson, string mapJsonText, RobotSpec spec)
{
Result res = new Result();
StringBuilder sb = new StringBuilder();
sb.AppendLine("## " + label);
string err;
JsonValue store = JsonValue.Parse(storeJson, out err);
if (store == null || !store.IsObject)
{
res.note = "店の形の JSON を読めません: " + err;
sb.AppendLine(" " + res.note);
res.report = sb.ToString().TrimEnd();
return res;
}
RobotMap map = RobotMap.Read(mapJsonText, out err);
if (map == null)
{
res.note = "地図を読めません: " + err;
sb.AppendLine(" " + res.note);
res.report = sb.ToString().TrimEnd();
return res;
}
sb.AppendLine(map.Report());
GridPlanner.Settings st = spec != null ? spec.PlannerSettings() : GridPlanner.Settings.Default;
sb.AppendLine(" 道の引き方: narrowCellCost " + F(st.narrowCellCost) +
" / centerWeight " + F(st.centerWeight) +
" / preferClearM " + F(st.preferClearM) +
" / maxExpand " + st.maxExpand +
" / snapM " + F(st.snapM) +
" / simplifyMaxM " + F(st.simplifyMaxM));
Vector3 pass = Vector3.zero;
bool hasPass = false;
List standby = new List();
JsonValue places = store["places"];
for (int i = 0; i < places.Count; i++)
{
string kind = places[i]["kind"].S("");
Vector3 p = new Vector3(places[i]["point"][0].F(0f), 0f, places[i]["point"][1].F(0f));
if (kind == "FoodPass") { pass = p; hasPass = true; }
else if (kind == "RobotStandby") standby.Add(p);
}
if (!hasPass)
{
res.note = "places に FoodPass(受け渡し口の前)がありません";
sb.AppendLine(" " + res.note);
res.report = sb.ToString().TrimEnd();
return res;
}
sb.AppendLine(" 受け渡し口の前 " + P(pass) + " → マスの種類 " + map.ClassAt(pass));
GridPlanner planner = map.Planner;
JsonValue fromPass = store["routes"]["from_pass"];
JsonValue tables = store["tables"];
List ratios = new List();
List times = new List();
List mismatchList = new List();
for (int i = 0; i < tables.Count; i++)
{
Row row = new Row();
row.tableId = tables[i]["id"].S("T" + (i + 1).ToString("00"));
Vector3 stop = new Vector3(tables[i]["stop"][0].F(0f), 0f, tables[i]["stop"][1].F(0f));
JsonValue route = fromPass[row.tableId];
row.siteUse = route["use"].S("");
row.siteLengthM = route["length"].F(-1f);
if (row.siteUse == "narrow") res.narrowSite++;
planner.ClearMarks();
GridPlanner.Status s = planner.Plan(pass, stop, st);
row.planMs = planner.LastPlanMs;
times.Add(row.planMs);
if (planner.LastStartWasBlocked || planner.LastGoalWasBlocked) res.snapped++;
if (s != GridPlanner.Status.Ok)
{
res.failed++;
row.status = planner.StatusText;
res.rows.Add(row);
sb.AppendLine(" × " + row.tableId + " 道が引けません: " + row.status);
continue;
}
row.planUse = planner.UsedNarrow ? "narrow" : "normal";
if (planner.UsedNarrow) res.narrowMine++;
row.planLengthM = planner.LastLengthM;
row.corners = planner.Points.Count;
row.status = "ok";
row.ratio = row.siteLengthM > 0.0005f ? row.planLengthM / row.siteLengthM : -1f;
if (row.ratio > 0f) ratios.Add(row.ratio);
if (row.planUse != row.siteUse)
{
res.mismatch++;
mismatchList.Add(row.tableId + "(サイト " + row.siteUse + " / 引いた道 " + row.planUse + ")");
}
res.rows.Add(row);
}
sb.AppendLine(" 狭い道を使った卓: 引いた道 " + res.narrowMine +
" 卓 / サイトの routes の use " + res.narrowSite + " 卓 → " +
(res.mismatch == 0 ? "一致" : "ちがう(" + res.mismatch + " 卓)"));
for (int i = 0; i < mismatchList.Count && i < 12; i++)
sb.AppendLine(" - " + mismatchList[i]);
if (mismatchList.Count > 12)
sb.AppendLine(" - ほか " + (mismatchList.Count - 12) + " 卓");
sb.AppendLine(" 道が引けない卓: " + res.failed + " / " + tables.Count);
if (ratios.Count > 0)
{
ratios.Sort();
float sum = 0f;
for (int i = 0; i < ratios.Count; i++) sum += ratios[i];
res.ratioMin = ratios[0];
res.ratioMean = sum / ratios.Count;
res.ratioMax = ratios[ratios.Count - 1];
sb.AppendLine(" 長さの比(引いた道 ÷ サイトの length): 最小 " + F(res.ratioMin) +
" / 平均 " + F(res.ratioMean) + " / 最大 " + F(res.ratioMax) +
"(真ん中寄りにした分だけ延びる)");
}
if (times.Count > 0)
{
times.Sort();
float sum = 0f;
for (int i = 0; i < times.Count; i++) sum += times[i];
res.planMsMean = sum / times.Count;
res.planMsMedian = times[times.Count / 2];
res.planMsMax = times[times.Count - 1];
sb.AppendLine(" 道探しの時間: 平均 " + F(res.planMsMean) + " ms / 中央 " +
F(res.planMsMedian) + " ms / 最大 " + F(res.planMsMax) + " ms");
}
sb.AppendLine(" 出発点・行き先が地図で通れないマス(0)だったので寄せた卓: " +
res.snapped + " / " + tables.Count);
res.standbyCount = standby.Count;
for (int i = 0; i < standby.Count; i++)
{
planner.ClearMarks();
GridPlanner.Status s = planner.Plan(pass, standby[i], st);
bool good = s == GridPlanner.Status.Ok;
if (good) res.standbyOk++;
sb.AppendLine(" 待機場所" + (i + 1) + " " + P(standby[i]) +
" マスの種類 " + map.ClassAt(standby[i]) + " → " +
(good ? "引けた(" + F(planner.LastLengthM) + " m・" +
F(planner.LastPlanMs) + " ms・" +
(planner.UsedNarrow ? "狭い道を使った" : "ふだんの道") + ")"
: "引けない:" + planner.StatusText));
}
res.ok = res.failed == 0 && res.mismatch == 0 &&
res.standbyOk == res.standbyCount && map.CountsMatch;
sb.AppendLine(" → " + (res.ok ? "確かめ A は通った" : "確かめ A は通っていない"));
res.report = sb.ToString().TrimEnd();
return res;
}
static string F(float v) { return v.ToString("0.###", CultureInfo.InvariantCulture); }
static string P(Vector3 v) { return "(" + F(v.x) + ", " + F(v.z) + ")"; }
}
}