tools/two.mjs
既存の検品コード。今回の作業では実行していない
/* 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);