import { io } from "socket.io-client";
import {
BOTS,
DURATION_MS,
JOIN_DELAY_MS,
MAX_X,
MAX_Y,
MOVE_TICK_MS,
BOT_SPEED,
BOT_RADIUS,
ROOM_ID,
SOCKET_PATH,
SOCKET_TRANSPORTS,
START_DELAY_MS,
START_GAME,
DEV_URL,
URL,
} from "./load-bot.constants.js";
type Stats = {
connected: number;
joined: number;
startSent: number;
moveSent: number;
disconnects: number;
errors: number;
gameStarts: number;
};
type Bot = {
stop: () => void;
};
// 実行後の簡易サマリ用カウンタ。
const stats: Stats = {
connected: 0,
joined: 0,
startSent: 0,
moveSent: 0,
disconnects: 0,
errors: 0,
gameStarts: 0,
};
const bots: Bot[] = [];
// 既にオーナーがいるルームを追跡。
const args = new Set(process.argv.slice(2));
const useDev = args.has("--dev");
const targetUrl = useDev ? DEV_URL : URL;
console.log("Load test starting...", {
env: useDev ? "dev" : "prod",
url: targetUrl,
bots: BOTS,
roomId: ROOM_ID,
durationMs: DURATION_MS,
joinDelayMs: JOIN_DELAY_MS,
startGame: START_GAME,
startDelayMs: START_DELAY_MS,
moveTickMs: MOVE_TICK_MS,
botSpeed: BOT_SPEED,
botRadius: BOT_RADIUS,
maxX: MAX_X,
maxY: MAX_Y,
});
// 接続スパイクを避けるため参加タイミングを分散。
for (let i = 0; i < BOTS; i += 1) {
const delay = i * JOIN_DELAY_MS;
setTimeout(() => {
const bot = createBot(i, stats, targetUrl);
bots.push(bot);
}, delay);
}
// 指定時間後に全ボットを停止。
setTimeout(() => {
console.log("Stopping bots...");
for (const bot of bots) {
bot.stop();
}
setTimeout(() => {
console.log("Final stats:", stats);
process.exit(0);
}, 500);
}, DURATION_MS);
function createBot(index: number, counters: Stats, url: string): Bot {
const roomId = ROOM_ID;
const playerName = `bot-${index}`;
const isOwner = index === 0;
// 固定トランスポートで再接続なしのクライアント接続。
const socket = io(url, {
transports: SOCKET_TRANSPORTS,
path: SOCKET_PATH,
reconnection: false,
timeout: 10_000,
});
let moveTimer: NodeJS.Timeout | null = null;
let posX = BOT_RADIUS + Math.random() * (MAX_X - BOT_RADIUS * 2);
let posY = BOT_RADIUS + Math.random() * (MAX_Y - BOT_RADIUS * 2);
let dirX = 1;
let dirY = 0;
const updateDirection = () => {
const angle = Math.random() * Math.PI * 2;
dirX = Math.cos(angle);
dirY = Math.sin(angle);
};
const tickMove = () => {
const dtSec = MOVE_TICK_MS / 1000;
posX += dirX * BOT_SPEED * dtSec;
posY += dirY * BOT_SPEED * dtSec;
if (posX < BOT_RADIUS) {
posX = BOT_RADIUS;
updateDirection();
} else if (posX > MAX_X - BOT_RADIUS) {
posX = MAX_X - BOT_RADIUS;
updateDirection();
}
if (posY < BOT_RADIUS) {
posY = BOT_RADIUS;
updateDirection();
} else if (posY > MAX_Y - BOT_RADIUS) {
posY = MAX_Y - BOT_RADIUS;
updateDirection();
}
socket.emit("move", { x: posX, y: posY });
counters.moveSent += 1;
};
socket.on("connect", () => {
counters.connected += 1;
// 接続直後にルームへ参加。
socket.emit("join-room", { roomId, playerName });
counters.joined += 1;
if (START_GAME && isOwner) {
// 他ボットの参加を待つため開始を少し遅らせる。
setTimeout(() => {
socket.emit("start-game");
counters.startSent += 1;
}, START_DELAY_MS);
}
});
socket.on("game-start", () => {
counters.gameStarts += 1;
// ゲーム開始後に定期的な移動イベントを送信。
if (!moveTimer && MOVE_TICK_MS > 0) {
updateDirection();
moveTimer = setInterval(tickMove, MOVE_TICK_MS);
}
});
socket.on("disconnect", () => {
counters.disconnects += 1;
if (moveTimer) {
clearInterval(moveTimer);
moveTimer = null;
}
});
socket.on("connect_error", () => {
counters.errors += 1;
});
return {
stop() {
if (moveTimer) {
clearInterval(moveTimer);
moveTimer = null;
}
socket.disconnect();
},
};
}