/* 大根おろし検査(2026-09-13)。 犯人を探しに行かず、**距離 x 角度 x 力**を1刻みで全部なめて表にする。 石とゾンビの芯は「上から落とす軌道を作れるか」なので、ここで測るのは2つだけ: ① 頭に落として倒せる組み合わせが、どの距離にもちゃんとあるか(=詰みが無いか) ② 横なげ(低い角度)では倒せないか(=芯が効いているか) 使い方: node tools/sweep.mjs [--prod] [--ang 4] [--pull 8] --prod 本番URLを撃つ(既定は手元のファイル) --ang 角度の刻み(度。既定4) --pull 力の刻み数(既定8 = 12.5%刻み) */ let webkit; try { ({ webkit } = await import('playwright')); } catch (e) { ({ webkit } = await import('file:///C:/平成ゾンビ学園/node_modules/playwright/index.mjs')); } import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /* 静的ファイルは public/ が本体(Workersの[assets]と同じ)。 無ければプロジェクト直下も見る(作りかけの見本ページ用) */ const pick = (u) => { const a = path.join(ROOT, 'public', u); return fs.existsSync(a) ? a : path.join(ROOT, u); }; const MIME = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json' }; const argv = process.argv.slice(2); const opt = (n, d) => { const i = argv.indexOf('--' + n); return i >= 0 ? argv[i + 1] : d; }; const angStep = +opt('ang', 4); const pullSteps = +opt('pull', 8); let url = 'https://ishi-to-zombie.pages.dev/', srv = null; if (!argv.includes('--prod')) { const port = 8800 + Math.floor(Math.random() * 90); srv = http.createServer((q, r) => { let u = decodeURIComponent(q.url.split('?')[0]); if (u === '/') u = '/index.html'; fs.readFile(pick(u), (e, d) => { if (e) { r.writeHead(404); r.end(); return; } r.writeHead(200, { 'Content-Type': MIME[path.extname(u).toLowerCase()] || 'application/octet-stream' }); r.end(d); }); }); await new Promise(ok => srv.listen(port, ok)); url = 'http://localhost:' + port + '/index.html'; } const browser = await webkit.launch(); const page = await browser.newPage({ viewport: { width: 390, height: 797 } }); const errs = []; page.on('pageerror', e => errs.push(String(e.message).slice(0, 200))); await page.goto(url, { waitUntil: 'load' }); await page.waitForTimeout(1200); const st = await page.evaluate(() => window.ZG && window.ZG.state()); if (!st) { console.log('ZGが無い。index.htmlの検品の口が壊れている'); process.exit(1); } console.log('版: ' + (await page.evaluate(() => window.ZG.ver))); console.log('画面 ' + st.W + 'x' + st.H + ' / 1m=' + st.ppm.toFixed(1) + 'px / 頭の半径 ' + st.hr.toFixed(1) + 'px / 石の半径 ' + st.stoneR.toFixed(1) + 'px'); console.log('投げる所 x=' + st.hx.toFixed(0) + ' y=' + st.ry.toFixed(0) + ' / 立ち位置 x=' + st.px.toFixed(0)); console.log('画面に入る距離: ' + ((st.W - st.px) / st.ppm).toFixed(2) + 'm まで\n'); const dists = [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5]; const res = await page.evaluate(({ dists, angStep, pullSteps }) => { const out = []; for (const d of dists) { const row = { d, kill: 0, knock: 0, miss: 0, n: 0, bank: 0, killAng: [], killPull: [] }; for (let a = 14; a <= 84; a += angStep) { for (let k = 1; k <= pullSteps; k++) { const pf = k / pullSteps; const o = window.ZG.trial(d, a, pf); row.n++; row[o.r]++; if (o.r === 'kill') { row.killAng.push(a); row.killPull.push(+pf.toFixed(3)); if (o.b > 0) row.bank++; } } } out.push(row); } return out; }, { dists, angStep, pullSteps }); console.log('距離 試行 倒した うち跳ね 押した 外れ 倒せる角度 倒せる力'); let dead = 0; for (const r of res) { const a = r.killAng.length ? Math.min(...r.killAng) + '〜' + Math.max(...r.killAng) + '度' : '—'; const p = r.killPull.length ? Math.round(Math.min(...r.killPull) * 100) + '〜' + Math.round(Math.max(...r.killPull) * 100) + '%' : '—'; if (!r.kill) dead++; console.log( String(r.d.toFixed(1) + 'm').padEnd(6) + String(r.n).padEnd(6) + (r.kill + '(' + (r.kill / r.n * 100).toFixed(1) + '%)').padEnd(14) + (r.bank + '').padEnd(10) + (r.knock + '').padEnd(8) + (r.miss + '').padEnd(7) + a.padEnd(16) + p); } /* 芯の確認: 低い角度(横なげ)で**直接**倒せてしまっていないか。 跳ね返らせてから落とした物は芯どおりの手なので、別に数える。 帯を14〜28度にしてあるのは実測の結果(2026-09-13): 30度・5m・62%は、頭に届く時点ですでに水平から33度下へ落ちているので 「横なげ」ではなく**低い山なり**。倒れて正しい。判定は落ちる角度で見ており、 投げた角度では見ていない——だから投げた角度で線を引くならここまで。 */ const flat = await page.evaluate(() => { let direct = 0, bank = 0, knock = 0, miss = 0; for (const d of [2, 3, 4, 5, 6, 7]) { for (let a = 14; a <= 28; a += 2) { for (let k = 1; k <= 8; k++) { const o = window.ZG.trial(d, a, k / 8); if (o.r === 'kill') { if (o.b > 0) bank++; else direct++; } else if (o.r === 'knock') knock++; else miss++; } } } return { direct, bank, knock, miss }; }); console.log('\n横なげ(14〜28度)だけ: 直接倒した ' + flat.direct + ' / 跳ね返らせて倒した ' + flat.bank + ' / 押した ' + flat.knock + ' / 外れ ' + flat.miss); console.log(flat.direct === 0 ? ' → 芯は効いている(横なげが直接当たって倒れることは一度も無い)' : ' ← 横なげが直接当たって倒れている。判定が緩い'); console.log(dead ? '\n詰み: ' + dead + '件の距離で一度も倒せない ← 直すこと' : '\n詰み: 無し(どの距離にも倒せる投げ方がある)'); if (errs.length) console.log('\nJSエラー: ' + errs.slice(0, 4).join(' / ')); await browser.close(); if (srv) srv.close(); process.exit(0);