"""試算 v2.8(レジの数を条件に足した版)の結果を、公開中の試算のページに入れる(2026-10-25)。 使い方: python3 build_page_v28.py <公開中のページ> <出力するページ> - const D:条件に registers(レジの数)を足し、結果に pay_wait_mean(食べ終えてから会計まで)を足す。店の情報・平面図は公開中のまま、レジの数だけ足す - 画面:条件に「レジ」を足す。結果に「食べ終えてから会計まで」を足す。前提に会計とレジの決まりを足す。見出しの版を v2.8 にする - 費用の欄・LP 用の色・使われ方の記録は公開中のまま""" import json, re, sys import haizen_sim as h live, res, out = sys.argv[1:4] s = open(live, encoding="utf-8").read() N = json.load(open(res, encoding="utf-8")) assert N["common"] == json.loads(json.dumps(h.COMMON)), "結果が今の haizen_sim.py の前提と違う" assert N["common"]["model"] == "v2.8" getD = lambda t: [l for l in t.split("\n") if l.startswith("const D = ")] ol = getD(s); assert len(ol) == 1 D = json.loads(ol[0][len("const D = "):].rstrip(";")) old_k = D["k"] assert old_k[:7] == ["store", "staff", "robots", "arrivals_per_h", "mode", "trays", "fragile_by"] and old_k[-1] == "ng" cond = old_k[:7] + ["registers"] mets = old_k[7:-1] + ["pay_wait_mean"] D["k"] = cond + mets + ["ng"] conds = list(h.conditions()) assert len(N["rows"]) == len(conds) rows = [] for c, r in zip(conds, N["rows"]): assert all(r[k] == c[k] for k in cond), ("条件の並びが違う", c, r) rows.append([r[k] for k in cond] + [round(r[k], 3) for k in mets] + [1 if r["staff_wait_mean"] >= h.NG_MIN else 0]) D["r"] = rows for k, st in D["stores"].items(): st["registers"] = h.STORES[k]["registers"] st["registers_all"] = h.STORES[k]["registers_all"] D["common"] = N["common"] s = s.replace(ol[0], "const D = " + json.dumps(D, ensure_ascii=False, separators=(",", ":")) + ";") def rep(a, b, n=1): global s assert s.count(a) == n, (a[:80], s.count(a)) s = s.replace(a, b) # 見出しの版 assert len(re.findall(r"試作 v2\.\d+", s)) == 1 s = re.sub(r"試作 v2\.\d+", "試作 v2.8", s) # 条件の欄に「レジ」 rep('''

''', '''

レジ

''') # 選んでいる条件・データの引き方 rep('''const state = { store: "family", staff: 3, robots: 2, mode: "serve", crowd: 1, trays: 3, fb: "any", cbase: 3, crob: 2 };''', '''const state = { store: "family", staff: 3, robots: 2, mode: "serve", crowd: 1, trays: 3, fb: "any", cbase: 3, crob: 2, reg: D.stores.family.registers };''') rep('''const IDX = new Map(ROWS.map(r => [[r.store, r.staff, r.robots, r.arrivals_per_h, r.mode, r.trays, r.fragile_by].join("|"), r])); function find(store, staff, robots, arr, mode, trays = state.trays, fb = state.fb) { const k = robots === 0 ? [store, staff, 0, arr, "serve", 1, "any"] : [store, staff, robots, arr, mode, trays, fb];''', '''const IDX = new Map(ROWS.map(r => [[r.store, r.staff, r.robots, r.arrivals_per_h, r.mode, r.trays, r.fragile_by, r.registers].join("|"), r])); // レジの数(v2.8)。指定がなければ左の条件のまま function find(store, staff, robots, arr, mode, trays = state.trays, fb = state.fb, reg = state.reg) { const k = robots === 0 ? [store, staff, 0, arr, "serve", 1, "any", reg] : [store, staff, robots, arr, mode, trays, fb, reg];''') # 店を選び直したら、レジはその店の最初の数に戻す rep('''state.cbase = S().staff[1];''', '''state.cbase = S().staff[1]; state.reg = S().registers;''', 2) rep(''' seg("seg-arr", "arr", s.arrivals.map((v, i) => ({ v: i, l: `${CROWD[i]}` })), () => state.crowd, v => state.crowd = v);''', ''' seg("seg-arr", "arr", s.arrivals.map((v, i) => ({ v: i, l: `${CROWD[i]}` })), () => state.crowd, v => state.crowd = v); if (!s.registers_all.includes(state.reg)) state.reg = s.registers; seg("seg-reg", "reg", s.registers_all.map(v => ({ v, l: `${v}つ` })), () => state.reg, v => state.reg = v);''') # 結果 rep('''const METRICS = [ { key: "entry_wait_mean", label: "入店待ち(平均)", plain: plainMin }, { key: "turn_mean", label: "客が帰ってから卓が使えるまで", plain: plainMin }, { key: "dish_wait_mean", label: "料理ができてから届くまで", plain: plainSec }, { key: "walk_km_per_staff", label: "スタッフ1人が歩く距離", plain: v => `${v.toFixed(2)}km` }, { key: "staff_util", label: "スタッフの手がふさがっている割合", plain: plainPct }, ];''', '''const METRICS = [ { key: "entry_wait_mean", label: "入店待ち(平均)", plain: plainMin }, { key: "turn_mean", label: "客が帰ってから卓が使えるまで", plain: plainMin }, { key: "dish_wait_mean", label: "料理ができてから届くまで", plain: plainSec }, { key: "walk_km_per_staff", label: "スタッフ1人が歩く距離", plain: v => `${v.toFixed(2)}km` }, { key: "staff_util", label: "スタッフの手がふさがっている割合", plain: plainPct }, { key: "pay_wait_mean", label: "食べ終えてから会計が始まるまで", plain: plainMin }, ];''') # 「レジが詰まる」の印(v2.8):食べ終えてから会計が始まるまでが NG_MIN 分以上。「店が回らない」とは別に出す(2026-10-25 エイイチと決めた) rep('''const cellOf = (m, r) => m.key === "entry_wait_mean" ? seatCell(r) : m.plain(r[m.key]);''', '''const regJam = r => r.pay_wait_mean >= NG_MIN; const regTag = r => `レジが詰まる`; const cellOf = (m, r) => m.key === "entry_wait_mean" ? seatCell(r) : m.key === "pay_wait_mean" ? m.plain(r[m.key]) + (regJam(r) ? `${regTag(r)}` : "") : m.plain(r[m.key]);''') rep(''' `${s.label}では、空いている ${s.arrivals[0]}組/時・普通 ${s.arrivals[1]}組/時・混雑 ${s.arrivals[2]}組/時。混む時間は${s.peak_min}分だけ続く前提です(ランチ・ディナーのピーク)。`;''', ''' `${s.label}では、空いている ${s.arrivals[0]}組/時・普通 ${s.arrivals[1]}組/時・混雑 ${s.arrivals[2]}組/時。混む時間は${s.peak_min}分だけ続く前提です(ランチ・ディナーのピーク)。`; document.getElementById("reg-note").textContent = `会計はレジ1つにつき1組ずつ(1組${D.common.pay_s}秒)。レジ${state.reg}つなら1時間に${Math.floor(3600 * state.reg / D.common.pay_s)}組までです。${s.label}は${s.registers}つを最初に見せています。`;''') rep(''' document.getElementById("cond-sub").textContent = `${CROWD[state.crowd]}(${arr}組/時)` + ''', ''' document.getElementById("cond-sub").textContent = `${CROWD[state.crowd]}(${arr}組/時)・レジ${state.reg}つ` + ''') rep(''' { k: "スタッフの手がふさがっている割合", v: pct(r.staff_util), h: "9割を超えると呼び出しに出られない" },''', ''' { k: "スタッフの手がふさがっている割合", v: pct(r.staff_util), h: "9割を超えると呼び出しに出られない" }, { k: "食べ終えてから会計が始まるまで", v: `${fmtMin(r.pay_wait_mean)}${regJam(r) ? regTag(r) : ""}`, h: `レジの順番とスタッフの手が空くのを待つ。そのあいだ卓はふさがったまま(レジ${state.reg}つ)` + (regJam(r) ? `。平均${NG_MIN}分以上待つので「レジが詰まる」` : "") },''') # 店ごとの比べ:レジは各店の最初の数 rep(''' `各店のスタッフは真ん中の人数・混み具合は普通・運び方は左の条件のままで、ロボット2台を入れたときの入店待ち(上)と、スタッフ1人が歩く距離(下)の変化です。`;''', ''' `各店のスタッフは真ん中の人数・混み具合は普通・レジは各店の最初の数(大型店は2つ、ほかは1つ)・運び方は左の条件のままで、ロボット2台を入れたときの入店待ち(上)と、スタッフ1人が歩く距離(下)の変化です。`;''') rep(''' const z = find(k, st, 0, arr, "serve"); const rs = MODES.map(o => find(k, st, 2, arr, o.v));''', ''' const z = find(k, st, 0, arr, "serve", undefined, undefined, s.registers); const rs = MODES.map(o => find(k, st, 2, arr, o.v, undefined, undefined, s.registers));''') # 前提 rep(''' ["案内・会計・呼び出し対応", `${c.guide_s}秒・${c.pay_s}秒・${c.call_s}秒(歩く時間は別)`],''', ''' ["案内・会計・呼び出し対応", `${c.guide_s}秒・${c.pay_s}秒・${c.call_s}秒(歩く時間は別)`], ["「レジが詰まる」の決め方", `食べ終えてから会計が始まるまでが、平均${NG_MIN}分以上の条件。「店が回らない」とは別の印で、「店が回らない」の決め方は変えていない(v2.8)`], ["会計とレジ", `会計はレジ1つにつき1組ずつ。食べ終えた組は卓に座ったままレジの順番を待ち、順番が来たらスタッフの手が空くのを待って会計する。レジ1つなら1時間に${Math.floor(3600 / c.pay_s)}組まで。レジの数は1つ・2つを選べる(v2.8 から。それまでは、会計をスタッフのだれでも同時にできた)`],''') open(out, "w", encoding="utf-8").write(s) print(len(s), "bytes", len(rows), "rows")