tools/sweep.mjs

既存の検品コード。今回の作業では実行していない

129行 · 6,297 bytes · SHA-256 0fa11506fd3cf44f0850249f81305055057df4299271f60153898b8ed8a9e21f

分割 1/1
/* 大根おろし検査(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);