diff --git "a/docs/01_Env/ENV_07_\343\203\206\343\202\271\343\203\210\346\223\215\344\275\234\346\211\213\351\240\206.txt" "b/docs/01_Env/ENV_07_\343\203\206\343\202\271\343\203\210\346\223\215\344\275\234\346\211\213\351\240\206.txt" index 902cefe..ad79662 100644 --- "a/docs/01_Env/ENV_07_\343\203\206\343\202\271\343\203\210\346\223\215\344\275\234\346\211\213\351\240\206.txt" +++ "b/docs/01_Env/ENV_07_\343\203\206\343\202\271\343\203\210\346\223\215\344\275\234\346\211\213\351\240\206.txt" @@ -29,7 +29,7 @@ 2-2. 設定値 ■ 接続先URL ・既定値: https://skillsemiwebgame.onrender.com -・変更方法: CLI引数または環境変数で指定する +・変更方法: 環境変数で指定する 3. 実行手順 (Execution Steps) @@ -42,27 +42,12 @@ - コマンド: $ pnpm install 3-2. 実行 -1. テストの実行 - コマンド: $ pnpm start -2. パラメータ指定の例 - - コマンド: $ pnpm start -- --bots=30 --rooms=3 --duration-ms=60000 4. パラメータ一覧 (Parameters) ------------------------------------------------------------------------ - -4-1. CLI引数 - ・--url: 接続先URL - ・--bots: 同時接続数 - ・--rooms: ルーム数 - ・--duration-ms: 実行時間 (ms) - ・--join-delay-ms: 参加間隔 (ms) - ・--start-game: ゲーム開始の有効化 (1/0) - ・--move-interval-ms: 移動送信間隔 (ms) - ・--max-x: X座標上限 - ・--max-y: Y座標上限 - -4-2. 環境変数 +4-1. 環境変数 ・TARGET_URL: 接続先URL ・BOTS: 同時接続数 ・ROOMS: ルーム数 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6efbec0..c2f0bd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,25 @@ specifier: ^5.9.3 version: 5.9.3 + test: + dependencies: + '@repo/shared': + specifier: workspace:* + version: link:../packages/shared + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.11 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.19.11)(typescript@5.9.3) + typescript: + specifier: ^5.5.4 + version: 5.9.3 + packages: '@babel/code-frame@7.29.0': @@ -171,6 +190,10 @@ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -343,6 +366,9 @@ '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@pixi/app@7.4.3': resolution: {integrity: sha512-opyWMuO0Ir8pf1DYUR++wAA6ZfNU+nIX2z95R2OD172HbcdhB4/HD7leLIIAny/LciEdMqlWEBhXK7N93YWbdg==} peerDependencies: @@ -617,6 +643,18 @@ '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -644,6 +682,9 @@ '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + '@types/node@24.10.11': resolution: {integrity: sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==} @@ -679,6 +720,10 @@ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -687,6 +732,9 @@ any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} @@ -747,6 +795,9 @@ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -759,6 +810,10 @@ supports-color: optional: true + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -910,6 +965,9 @@ magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1123,6 +1181,20 @@ ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -1155,6 +1227,9 @@ ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -1168,6 +1243,9 @@ resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1231,6 +1309,10 @@ yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + snapshots: '@babel/code-frame@7.29.0': @@ -1347,6 +1429,10 @@ '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -1444,6 +1530,11 @@ '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))': dependencies: '@pixi/core': 7.4.3 @@ -1661,6 +1752,14 @@ '@socket.io/component-emitter@3.1.2': {} + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -1694,6 +1793,10 @@ '@types/estree@1.0.8': {} + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.11': dependencies: undici-types: 7.16.0 @@ -1738,10 +1841,16 @@ mime-types: 2.1.35 negotiator: 0.6.3 + acorn-walk@8.3.5: + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} any-promise@1.3.0: {} + arg@4.1.3: {} + base64id@2.0.0: {} baseline-browser-mapping@2.9.19: {} @@ -1792,12 +1901,16 @@ object-assign: 4.1.1 vary: 1.1.2 + create-require@1.1.1: {} + csstype@3.2.3: {} debug@4.4.3: dependencies: ms: 2.1.3 + diff@4.0.4: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1968,6 +2081,8 @@ dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-error@1.3.6: {} + math-intrinsics@1.1.0: {} mime-db@1.52.0: {} @@ -2223,6 +2338,24 @@ ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.11 + acorn: 8.15.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) @@ -2262,6 +2395,8 @@ ufo@1.6.3: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -2275,6 +2410,8 @@ punycode: 1.4.1 qs: 6.14.2 + v8-compile-cache-lib@3.0.1: {} + vary@1.1.2: {} vite@7.3.1(@types/node@24.10.11)(tsx@4.21.0): @@ -2295,3 +2432,5 @@ xmlhttprequest-ssl@2.1.2: {} yallist@3.1.1: {} + + yn@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6acdda0..28c52dc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - apps/* - packages/* + - test onlyBuiltDependencies: - esbuild diff --git a/test/load-bot.constants.ts b/test/load-bot.constants.ts new file mode 100644 index 0000000..6383cfb --- /dev/null +++ b/test/load-bot.constants.ts @@ -0,0 +1,16 @@ +import { config } from "@repo/shared"; + +const { GAME_CONFIG, NETWORK_CONFIG } = config; + +export const URL = NETWORK_CONFIG.PROD_SERVER_URL; +export const BOTS = 20; +export const DURATION_MS = 60_000; +export const JOIN_DELAY_MS = 25; +export const MOVE_INTERVAL_MS = 200; +export const START_DELAY_MS = 800; +export const MAX_X = GAME_CONFIG.MAP_WIDTH; +export const MAX_Y = GAME_CONFIG.MAP_HEIGHT; +export const ROOM_ID = "1"; +export const START_GAME = true; +export const SOCKET_PATH = NETWORK_CONFIG.SOCKET_IO_PATH; +export const SOCKET_TRANSPORTS = [...NETWORK_CONFIG.SOCKET_TRANSPORTS]; diff --git a/test/load-bot.mjs b/test/load-bot.mjs deleted file mode 100644 index bbbf8dd..0000000 --- a/test/load-bot.mjs +++ /dev/null @@ -1,190 +0,0 @@ -import { io } from "socket.io-client"; -import { config } from "@repo/shared"; - -const { GAME_CONFIG, NETWORK_CONFIG } = config; - -const DEFAULT_URL = NETWORK_CONFIG.PROD_SERVER_URL; -const DEFAULT_BOTS = 20; -const DEFAULT_ROOMS = 1; -const DEFAULT_DURATION_MS = 60_000; -const DEFAULT_JOIN_DELAY_MS = 25; -const DEFAULT_MOVE_INTERVAL_MS = 200; -const DEFAULT_START_DELAY_MS = 800; -const DEFAULT_MAX_X = GAME_CONFIG.MAP_WIDTH; -const DEFAULT_MAX_Y = GAME_CONFIG.MAP_HEIGHT; - -const SOCKET_PATH = NETWORK_CONFIG.SOCKET_IO_PATH; -const SOCKET_TRANSPORTS = NETWORK_CONFIG.SOCKET_TRANSPORTS; - -// CLI引数を先に解析して、環境変数やデフォルトを上書きできるようにする。 -const args = parseArgs(process.argv.slice(2)); - -// 実行時オプションを集約(CLI > 環境変数 > デフォルト)。 -const options = { - url: args.url ?? process.env.TARGET_URL ?? DEFAULT_URL, - bots: toInt(args.bots ?? process.env.BOTS, DEFAULT_BOTS), - rooms: toInt(args.rooms ?? process.env.ROOMS, DEFAULT_ROOMS), - roomPrefix: args.roomPrefix ?? process.env.ROOM_PREFIX ?? "load-room", - durationMs: toInt(args.durationMs ?? process.env.DURATION_MS, DEFAULT_DURATION_MS), - joinDelayMs: toInt(args.joinDelayMs ?? process.env.JOIN_DELAY_MS, DEFAULT_JOIN_DELAY_MS), - startGame: toBool(args.startGame ?? process.env.START_GAME ?? "1"), - startDelayMs: toInt(args.startDelayMs ?? process.env.START_DELAY_MS, DEFAULT_START_DELAY_MS), - moveIntervalMs: toInt(args.moveIntervalMs ?? process.env.MOVE_INTERVAL_MS, DEFAULT_MOVE_INTERVAL_MS), - maxX: toInt(args.maxX ?? process.env.MAX_X, DEFAULT_MAX_X), - maxY: toInt(args.maxY ?? process.env.MAX_Y, DEFAULT_MAX_Y), -}; - -// 実行後の簡易サマリ用カウンタ。 -const stats = { - connected: 0, - joined: 0, - startSent: 0, - moveSent: 0, - disconnects: 0, - errors: 0, - gameStarts: 0, -}; - -const bots = []; -// 既にオーナーがいるルームを追跡。 -const perRoomOwnerIndex = new Set(); - -console.log("Load test starting...", options); - -// 接続スパイクを避けるため参加タイミングを分散。 -for (let i = 0; i < options.bots; i += 1) { - const delay = i * options.joinDelayMs; - setTimeout(() => { - const bot = createBot(i, options, perRoomOwnerIndex, 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); -}, options.durationMs); - -function createBot(index, opts, owners, counters) { - // ルームへラウンドロビンで分散。 - const roomIndex = index % opts.rooms; - const roomId = `${opts.roomPrefix}-${roomIndex}`; - const playerName = `bot-${index}`; - // 各ルームの先頭ボットをオーナー扱いにする。 - const isOwner = index < opts.rooms; - - if (isOwner) { - owners.add(roomIndex); - } - - // 固定トランスポートで再接続なしのクライアント接続。 - const socket = io(opts.url, { - transports: SOCKET_TRANSPORTS, - path: SOCKET_PATH, - reconnection: false, - timeout: 10_000, - }); - - let moveTimer = null; - - socket.on("connect", () => { - counters.connected += 1; - // 接続直後にルームへ参加。 - socket.emit("join-room", { roomId, playerName }); - counters.joined += 1; - - if (opts.startGame && isOwner) { - // 他ボットの参加を待つため開始を少し遅らせる。 - setTimeout(() => { - socket.emit("start-game"); - counters.startSent += 1; - }, opts.startDelayMs); - } - }); - - socket.on("game-start", () => { - counters.gameStarts += 1; - // ゲーム開始後に定期的な移動イベントを送信。 - if (!moveTimer && opts.moveIntervalMs > 0) { - moveTimer = setInterval(() => { - const x = Math.random() * opts.maxX; - const y = Math.random() * opts.maxY; - socket.emit("move", { x, y }); - counters.moveSent += 1; - }, opts.moveIntervalMs); - } - }); - - 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(); - }, - }; -} - -function parseArgs(argv) { - const result = {}; - for (let i = 0; i < argv.length; i += 1) { - const current = argv[i]; - if (!current.startsWith("--")) continue; - - const [rawKey, inlineValue] = current.slice(2).split("="); - const key = toCamel(rawKey); - - if (inlineValue !== undefined) { - result[key] = inlineValue; - continue; - } - - const next = argv[i + 1]; - if (next && !next.startsWith("--")) { - result[key] = next; - i += 1; - } else { - result[key] = "true"; - } - } - return result; -} - -// CLIのkebab-caseをcamelCaseに変換。 -function toCamel(value) { - return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase()); -} - -// 整数の安全なパース(失敗時はフォールバック)。 -function toInt(value, fallback) { - const num = Number.parseInt(String(value), 10); - return Number.isFinite(num) ? num : fallback; -} - -// フラグの真値として一般的な文字列を許可。 -function toBool(value) { - if (value === true) return true; - if (value === false) return false; - const normalized = String(value).toLowerCase(); - return normalized === "1" || normalized === "true" || normalized === "yes"; -} diff --git a/test/load-bot.ts b/test/load-bot.ts new file mode 100644 index 0000000..abe3946 --- /dev/null +++ b/test/load-bot.ts @@ -0,0 +1,143 @@ +import { io } from "socket.io-client"; +import { + BOTS, + DURATION_MS, + JOIN_DELAY_MS, + MAX_X, + MAX_Y, + MOVE_INTERVAL_MS, + 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, + moveIntervalMs: MOVE_INTERVAL_MS, + 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; + + 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_INTERVAL_MS > 0) { + moveTimer = setInterval(() => { + const x = Math.random() * MAX_X; + const y = Math.random() * MAX_Y; + socket.emit("move", { x, y }); + counters.moveSent += 1; + }, MOVE_INTERVAL_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(); + }, + }; +} diff --git a/test/package.json b/test/package.json index 3e26a99..bc4924f 100644 --- a/test/package.json +++ b/test/package.json @@ -3,10 +3,15 @@ "private": true, "type": "module", "scripts": { - "start": "node load-bot.mjs" + "start": "node --loader ts-node/esm load-bot.ts" }, "dependencies": { "@repo/shared": "workspace:*", "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" } } diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..03f4007 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"], + "noEmit": true + }, + "include": ["*.ts"] +}