tools/sweep.mjs
分割 1 / 1 · 元ファイル 1〜129行付近
/* 大根おろし検査(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);