diff --git "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" index 6bcbec5..c190a5a 100644 --- "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" +++ "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" @@ -107,7 +107,7 @@ │ ├── index.ts # 共有エクスポート │ ├── config/ │ │ ├── index.ts # 設定エクスポート - │ │ └── gameConfig.ts # 共通定数 + │ │ ├── gameConfig.ts # 共通定数 │ │ └── networkConfig.ts # 通信設定 │ ├── domains/ │ │ ├── gridMap/ @@ -120,6 +120,12 @@ │ │ └── room.type.ts # ルーム型 │ └── protocol/ │ └── events.ts # 通信イベント定義 + ├── test/ # テスト用スクリプト群 + │ ├── load-bot.constants.ts # 負荷テスト定数 + │ ├── load-bot.ts # 負荷テスト実行 + │ ├── node_modules/ # テスト依存 + │ ├── package.json # テスト依存・スクリプト + │ └── tsconfig.json # テストTS設定 ├── .gitignore # Git除外設定 ├── .npmrc # pnpm設定 ├── docker-compose.prod.yml # 本番Compose定義 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" new file mode 100644 index 0000000..ad79662 --- /dev/null +++ "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" @@ -0,0 +1,85 @@ +======================================================================== +テスト操作手順 (Test Operation Guide) +======================================================================== + + +1. 概要 (Overview) +------------------------------------------------------------------------ +本ドキュメントは,プロジェクト内のテスト関連ファイルの実行方法を整理し,チーム内で共通の運用手順を共有することを目的とする. + + +1-1. 対象範囲 + +■ 対象ファイル +・テスト実行用スクリプト: /workspace/test/load-bot.mjs +・テスト用依存定義: /workspace/test/package.json + +■ 対象外 +・アプリ本体のビルドと起動手順 +・本番運用の監視設定 + + +2. 前提条件 (Prerequisites) +------------------------------------------------------------------------ + +2-1. 環境 +・Node.js / pnpm が利用可能であること +・インターネット接続が利用可能であること + +2-2. 設定値 +■ 接続先URL +・既定値: https://skillsemiwebgame.onrender.com +・変更方法: 環境変数で指定する + + +3. 実行手順 (Execution Steps) +------------------------------------------------------------------------ + +3-1. 初回準備 +1. テスト用ディレクトリへ移動 + - コマンド: $ cd /workspace/test +2. 依存関係のインストール + - コマンド: $ pnpm install + +3-2. 実行 + - コマンド: $ pnpm start + + +4. パラメータ一覧 (Parameters) +------------------------------------------------------------------------ +4-1. 環境変数 + ・TARGET_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座標上限 + + +5. 出力と終了 (Output & Termination) +------------------------------------------------------------------------ + +5-1. 標準出力 + ・開始時に設定値が出力される + ・終了時に簡易統計 (接続数など) が出力される + +5-2. 終了方法 + ・指定時間経過後に自動終了する + ・途中終了する場合はプロセスを終了する + + +6. 注意事項 (Notes) +------------------------------------------------------------------------ + +6-1. 本番サーバへの負荷 + ・負荷テストは低負荷から段階的に実施すること + ・必要に応じて管理者へ事前連絡を行うこと + +6-2. 依存関係 + ・test配下は独立した依存関係を持つため,別途pnpm installが必要になる + +6-3. 計測の限界 + ・本スクリプトは簡易負荷確認用であり,詳細な計測は専用ツールの利用を推奨する diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 7d0ef50..681f9dd 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -23,7 +23,7 @@ // プレイヤー挙動設定 PLAYER_RADIUS: 10, // プレイヤー半径 - PLAYER_SPEED: 5, // 移動速度(ピクセル/秒) + PLAYER_SPEED: 5, // 60fps基準の1フレーム当たりの移動量(px) // チームカラー設定 // teamId インデックス順カラー配列 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..6331e98 --- /dev/null +++ b/test/load-bot.constants.ts @@ -0,0 +1,18 @@ +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_TICK_MS = GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; +export const BOT_SPEED = GAME_CONFIG.PLAYER_SPEED; +export const BOT_RADIUS = GAME_CONFIG.PLAYER_RADIUS; +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.ts b/test/load-bot.ts new file mode 100644 index 0000000..c458ed9 --- /dev/null +++ b/test/load-bot.ts @@ -0,0 +1,178 @@ +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(); + }, + }; +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..bc4924f --- /dev/null +++ b/test/package.json @@ -0,0 +1,17 @@ +{ + "name": "load-test", + "private": true, + "type": "module", + "scripts": { + "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"] +}