tools/two.mjs

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

145行 · 7,075 bytes · SHA-256 cdd77076c4c19077c1da8850d409fa2b32af130a36a1306ef0198bb9f2f9f02b

分割 1/1
/* 2つの端末で本当に繋がるかを確かめる道具(2026-09-13)。
   ブラウザを2枚開いて、片方が倒した物がもう片方に届くか・スクラップが共有されるか・
   抜けても壊れないかを、数字で確かめる。写真も撮る。

   使い方: node tools/two.mjs [--url https://...] [--local] */
let webkit;
try { ({ webkit } = await import('playwright')); }
catch (e) { ({ webkit } = await import('file:///C:/平成ゾンビ学園/node_modules/playwright/index.mjs')); }
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const argv = process.argv.slice(2);
const opt = (n,d)=>{ const i=argv.indexOf('--'+n); return i>=0?argv[i+1]:d; };
const URL_ = opt('url', 'https://ishi-to-zombie.kaiten-sushi-online.workers.dev/');

const wait = ms => new Promise(r=>setTimeout(r,ms));
const browser = await webkit.launch();
async function openOne(name){
  const ctx = await browser.newContext({ viewport:{width:390,height:797} });
  const page = await ctx.newPage();
  const errs = [];
  page.on('pageerror', e=>errs.push(name+': '+String(e.message).slice(0,140)));
  await page.goto(URL_, { waitUntil:'load' });
  await page.waitForTimeout(900);
  return { name, ctx, page, errs };
}
const st = p => p.page.evaluate(()=>window.ZG.state());
const line = (tag,s)=> tag+'  通信='+s.net+' 部屋='+(s.room||'—')+' 人数='+s.players+
  ' ゾンビ='+s.zombies+' 倒した='+s.kills+' スクラップ='+s.base+' HP='+s.hp;

console.log('相手: '+URL_+'\n');
const A = await openOne('A');
await A.page.evaluate(()=>window.ZG.start());
await wait(2500);
let sa = await st(A);
console.log(line('A ひとりで開始 ', sa));
if (sa.net !== 'online'){ console.log('  ← つながっていない。ここで止める'); await browser.close(); process.exit(1); }

const B = await openOne('B');
await B.page.evaluate(()=>window.ZG.start());
await wait(2800);
sa = await st(A); let sb = await st(B);
console.log(line('B が途中参加  ', sb));
console.log(line('  その時のA   ', sa));
const two = (sa.players===2 && sb.players===2);
console.log(two ? '  → 2人になった(モード選択なしで合流できている)'
                : '  ← 2人になっていない');

/* 同じ部屋か・ゾンビが揃っているか */
console.log('\n同じ部屋: ' + (sa.room===sb.room ? 'はい ('+sa.room+')' : 'いいえ ('+sa.room+' / '+sb.room+')'));
const zz = await Promise.all([A,B].map(p=>p.page.evaluate(()=>window.ZG.zs().map(z=>+z.m.toFixed(2)))));
console.log('Aから見たゾンビの位置: '+JSON.stringify(zz[0]));
console.log('Bから見たゾンビの位置: '+JSON.stringify(zz[1]));

/* ★スクラップの共有: Aが倒した分がBにも入るか(K1「キルの取り合いを発生させない」) */
const b0 = { a:sa.base, b:sb.base, k:sa.kills };
await A.page.evaluate(()=>window.ZG.hitNearest('kill'));
await wait(1400);
sa = await st(A); sb = await st(B);
console.log('\nAが1体倒した(石は同じ道を通してサーバーへ出している)');
console.log(line('  A ', sa));
console.log(line('  B ', sb));
console.log((sa.kills>b0.k && sa.base>b0.a && sb.base>b0.b)
  ? '  → 倒した数もスクラップも**2人に同じだけ**入った(取り合いにならない)'
  : '  ← 共有できていない');

/* Bが倒した分もAへ入るか */
const b1 = { a:sa.base, b:sb.base };
await B.page.evaluate(()=>window.ZG.hitNearest('kill'));
await wait(1400);
sa = await st(A); sb = await st(B);
console.log('こんどはBが1体倒した → A '+b1.a+'→'+sa.base+' / B '+b1.b+'→'+sb.base);

/* 横から当てるとノックバックだけ(芯が2人でも効いているか) */
const zb = (await A.page.evaluate(()=>window.ZG.zs()))[0];
if (zb){
  await A.page.evaluate(()=>window.ZG.hitNearest('knock'));
  await wait(1200);
  const za = (await A.page.evaluate(()=>window.ZG.zs()))[0];
  console.log('横から当てた: ゾンビの位置 '+zb.m.toFixed(2)+'m → '+(za?za.m.toFixed(2)+'m':'—')+
    (za && za.m>zb.m ? '  → 押し戻されただけ(倒れていない)' : '  ← おかしい'));
}

/* 交代 */
const sw = await A.page.evaluate(()=>window.ZG.swap());
await wait(1000);
const pa = sa.px, pb = sb.px;
sa = await st(A); sb = await st(B);
console.log('\n前後交代をAが押した: '+(sw?'受け付けた':'今は押せない'));
console.log('  Aの立ち位置 '+Math.round(pa)+'px → '+Math.round(sa.px)+'px / '+
            'Bの立ち位置 '+Math.round(pb)+'px → '+Math.round(sb.px)+'px');
console.log((sa.px!==pa && sb.px!==pb) ? '  → 2人とも前後が入れ替わった' : '  ← 入れ替わっていない');

/* 写真 */
fs.mkdirSync(path.join(ROOT,'tmp'),{recursive:true});
await A.page.screenshot({ path: path.join(ROOT,'tmp/two_A.png') });
await B.page.screenshot({ path: path.join(ROOT,'tmp/two_B.png') });
console.log('\n写真: tmp/two_A.png / tmp/two_B.png');

/* Bが抜ける → Aは壊れずに続くか */
await B.ctx.close();
await wait(2500);
sa = await st(A);
console.log('\nBが抜けたあと');
console.log(line('  A ', sa));
console.log(sa.players===1 && sa.net==='online' ? '  → ひとりに戻って続いている(壊れていない)'
                                                : '  ← おかしい');
await A.page.screenshot({ path: path.join(ROOT,'tmp/two_A_alone.png') });

/* Aも抜ける → 部屋が使い回されないか(相手探しから外れたか) */
const room = sa.room;
await A.ctx.close();
await wait(1500);
const C = await openOne('C');
await C.page.evaluate(()=>window.ZG.start());
await wait(2500);
const sc = await st(C);
console.log('\n全員抜けたあとに新しい人が来た');
console.log(line('  C ', sc));
console.log(sc.room!==room ? '  → 前の部屋は配られない(畳まれる部屋へ案内しない)'
                           : '  ← 前の部屋がまた配られた');
await C.ctx.close();

/* ★誰も繋がっていない部屋が回り続けていないか(2026-09-13にぷっちーずで実際に起きた穴) */
await wait(4000);
try{
  const r = await fetch(URL_.replace(/\/$/,'')+'/api/room?r='+room);
  const j = await r.json();
  console.log('\n誰もいない部屋のようす: 人数='+j.n+' / 時計='+(j.tick?'まだ回っている':'止まっている'));
  console.log((!j.n && !j.tick) ? '  → 空の部屋は1コマも進めていない(3分後に畳まれる)'
                                : '  ← 空なのに回っている。直すこと');
}catch(e){ console.log('\n部屋のようすを見られなかった: '+e.message); }

/* DoSガードの門がいくつまで来ているか */
try{
  const m = await (await fetch(URL_.replace(/\/$/,'')+'/api/meter')).json();
  console.log('DoSガード(盾2): '+m['今いくつ']+' / 上限 '+m['上限']);
}catch(e){}

const errs = [...A.errs, ...B.errs, ...C.errs];
console.log('\nJSエラー: '+(errs.length?errs.join(' / '):'なし'));
await browser.close();
process.exit(0);