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