Newer
Older
PixelPaintWar / test / load-bot.ts
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,
  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[] = [];
// 既にオーナーがいるルームを追跡。

console.log("Load test starting...", {
  url: URL,
  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);
    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): 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 frameDelta = MOVE_TICK_MS / (1000 / 60);
    posX += dirX * BOT_SPEED * frameDelta;
    posY += dirY * BOT_SPEED * frameDelta;

    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();
    },
  };
}