dorkfuncli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.env.example +6 -0
  2. package/dist/commands/agent.d.ts +2 -0
  3. package/dist/commands/agent.js +163 -0
  4. package/dist/commands/agent.js.map +1 -0
  5. package/dist/commands/config.d.ts +3 -0
  6. package/dist/commands/config.js +124 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/config/configFile.d.ts +6 -0
  9. package/dist/config/configFile.js +43 -0
  10. package/dist/config/configFile.js.map +1 -0
  11. package/dist/config/defaults.d.ts +10 -0
  12. package/dist/config/defaults.js +19 -0
  13. package/dist/config/defaults.js.map +1 -0
  14. package/dist/config/index.d.ts +4 -0
  15. package/dist/config/index.js +5 -0
  16. package/dist/config/index.js.map +1 -0
  17. package/dist/config/resolve.d.ts +3 -0
  18. package/dist/config/resolve.js +24 -0
  19. package/dist/config/resolve.js.map +1 -0
  20. package/dist/config/runtime.d.ts +3 -0
  21. package/dist/config/runtime.js +13 -0
  22. package/dist/config/runtime.js.map +1 -0
  23. package/dist/config.d.ts +6 -0
  24. package/dist/config.js +6 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +230 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/transport/httpClient.d.ts +13 -0
  30. package/dist/transport/httpClient.js +91 -0
  31. package/dist/transport/httpClient.js.map +1 -0
  32. package/dist/transport/wsClient.d.ts +30 -0
  33. package/dist/transport/wsClient.js +196 -0
  34. package/dist/transport/wsClient.js.map +1 -0
  35. package/dist/tui/App.d.ts +6 -0
  36. package/dist/tui/App.js +40 -0
  37. package/dist/tui/App.js.map +1 -0
  38. package/dist/tui/components/ChatPanel.d.ts +12 -0
  39. package/dist/tui/components/ChatPanel.js +18 -0
  40. package/dist/tui/components/ChatPanel.js.map +1 -0
  41. package/dist/tui/components/ColoredBoard.d.ts +7 -0
  42. package/dist/tui/components/ColoredBoard.js +73 -0
  43. package/dist/tui/components/ColoredBoard.js.map +1 -0
  44. package/dist/tui/components/PlayerInfo.d.ts +10 -0
  45. package/dist/tui/components/PlayerInfo.js +10 -0
  46. package/dist/tui/components/PlayerInfo.js.map +1 -0
  47. package/dist/tui/components/StatusBar.d.ts +7 -0
  48. package/dist/tui/components/StatusBar.js +13 -0
  49. package/dist/tui/components/StatusBar.js.map +1 -0
  50. package/dist/tui/components/TicTacToeBoard.d.ts +6 -0
  51. package/dist/tui/components/TicTacToeBoard.js +19 -0
  52. package/dist/tui/components/TicTacToeBoard.js.map +1 -0
  53. package/dist/tui/hooks/useEnsNames.d.ts +5 -0
  54. package/dist/tui/hooks/useEnsNames.js +33 -0
  55. package/dist/tui/hooks/useEnsNames.js.map +1 -0
  56. package/dist/tui/screens/GameBoard.d.ts +10 -0
  57. package/dist/tui/screens/GameBoard.js +245 -0
  58. package/dist/tui/screens/GameBoard.js.map +1 -0
  59. package/dist/tui/screens/GameOver.d.ts +10 -0
  60. package/dist/tui/screens/GameOver.js +21 -0
  61. package/dist/tui/screens/GameOver.js.map +1 -0
  62. package/dist/tui/screens/Leaderboard.d.ts +5 -0
  63. package/dist/tui/screens/Leaderboard.js +102 -0
  64. package/dist/tui/screens/Leaderboard.js.map +1 -0
  65. package/dist/tui/screens/Lobby.d.ts +8 -0
  66. package/dist/tui/screens/Lobby.js +113 -0
  67. package/dist/tui/screens/Lobby.js.map +1 -0
  68. package/dist/tui/screens/Matchmaking.d.ts +9 -0
  69. package/dist/tui/screens/Matchmaking.js +66 -0
  70. package/dist/tui/screens/Matchmaking.js.map +1 -0
  71. package/dist/tui/screens/WatchGame.d.ts +7 -0
  72. package/dist/tui/screens/WatchGame.js +99 -0
  73. package/dist/tui/screens/WatchGame.js.map +1 -0
  74. package/dist/tui/screens/WatchList.d.ts +6 -0
  75. package/dist/tui/screens/WatchList.js +49 -0
  76. package/dist/tui/screens/WatchList.js.map +1 -0
  77. package/dist/tui/theme.d.ts +30 -0
  78. package/dist/tui/theme.js +31 -0
  79. package/dist/tui/theme.js.map +1 -0
  80. package/dist/wallet/signer.d.ts +13 -0
  81. package/dist/wallet/signer.js +41 -0
  82. package/dist/wallet/signer.js.map +1 -0
  83. package/package.json +43 -0
  84. package/play-agents.cjs +444 -0
  85. package/src/commands/agent.ts +175 -0
  86. package/src/commands/config.ts +162 -0
  87. package/src/config/configFile.ts +55 -0
  88. package/src/config/defaults.ts +28 -0
  89. package/src/config/index.ts +15 -0
  90. package/src/config/resolve.ts +33 -0
  91. package/src/config/runtime.ts +18 -0
  92. package/src/index.ts +237 -0
  93. package/src/transport/httpClient.ts +104 -0
  94. package/src/transport/wsClient.ts +214 -0
  95. package/src/tui/App.tsx +130 -0
  96. package/src/tui/components/ChatPanel.tsx +53 -0
  97. package/src/tui/components/ColoredBoard.tsx +98 -0
  98. package/src/tui/components/PlayerInfo.tsx +31 -0
  99. package/src/tui/components/StatusBar.tsx +35 -0
  100. package/src/tui/hooks/useEnsNames.ts +35 -0
  101. package/src/tui/screens/GameBoard.tsx +434 -0
  102. package/src/tui/screens/GameOver.tsx +83 -0
  103. package/src/tui/screens/Leaderboard.tsx +196 -0
  104. package/src/tui/screens/Lobby.tsx +197 -0
  105. package/src/tui/screens/Matchmaking.tsx +107 -0
  106. package/src/tui/screens/WatchGame.tsx +182 -0
  107. package/src/tui/screens/WatchList.tsx +99 -0
  108. package/src/tui/theme.ts +31 -0
  109. package/src/wallet/signer.ts +54 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,41 @@
1
+ import { Wallet, JsonRpcProvider, Contract } from "ethers";
2
+ import { getConfig } from "../config/runtime.js";
3
+ const ESCROW_ABI = [
4
+ "function depositStake(bytes32 matchId) external payable",
5
+ ];
6
+ let wallet = null;
7
+ export function getWallet() {
8
+ if (!wallet) {
9
+ const { privateKey } = getConfig();
10
+ if (!privateKey) {
11
+ throw new Error("Private key not set. Run 'dork config' to set one up.");
12
+ }
13
+ wallet = new Wallet(privateKey);
14
+ }
15
+ return wallet;
16
+ }
17
+ export function getAddress() {
18
+ return getWallet().address;
19
+ }
20
+ export async function signMessage(message) {
21
+ return getWallet().signMessage(message);
22
+ }
23
+ /**
24
+ * Submit an escrow deposit transaction on-chain.
25
+ * Returns the transaction hash.
26
+ */
27
+ export async function sendEscrowDeposit(escrow) {
28
+ const { rpcUrl, privateKey } = getConfig();
29
+ if (!privateKey) {
30
+ throw new Error("Private key not set. Run 'dork config' to set one up.");
31
+ }
32
+ const provider = new JsonRpcProvider(rpcUrl);
33
+ const signer = new Wallet(privateKey, provider);
34
+ const contract = new Contract(escrow.address, ESCROW_ABI, signer);
35
+ const tx = await contract.depositStake(escrow.matchIdBytes32, {
36
+ value: escrow.stakeWei,
37
+ });
38
+ const receipt = await tx.wait();
39
+ return receipt.hash;
40
+ }
41
+ //# sourceMappingURL=signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/wallet/signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,MAAM,UAAU,GAAG;IACjB,yDAAyD;CAC1D,CAAC;AAEF,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,uDAAuD,CACxD,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe;IAC/C,OAAO,SAAS,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAIvC;IACC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAElE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,cAAc,EAAE;QAC5D,KAAK,EAAE,MAAM,CAAC,QAAQ;KACvB,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;IAChC,OAAO,OAAO,CAAC,IAAc,CAAC;AAChC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dorkfuncli",
3
+ "publishConfig": {
4
+ "access": "public",
5
+ "registry": "https://registry.npmjs.org/"
6
+ },
7
+ "version": "0.0.1",
8
+ "description": "dork.fun CLI client: terminal UI for agents and users",
9
+ "type": "module",
10
+ "bin": {
11
+ "dork": "dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "ts-node src/index.ts",
17
+ "test": "if ls src/**/*.spec.ts >/dev/null 2>&1; then mocha --require ts-node/register --recursive --parallel './src/**/*.spec.ts'; else echo 'No test files found'; fi"
18
+ },
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@dorkfun/agent-sdk": "workspace:*",
22
+ "@dorkfun/core": "workspace:*",
23
+ "@dorkfun/game-ui": "workspace:*",
24
+ "@dorkfun/protocol": "workspace:*",
25
+ "commander": "^13.1.0",
26
+ "dotenv": "^17.2.3",
27
+ "ethers": "^6.16.0",
28
+ "ink": "^5.1.0",
29
+ "ink-spinner": "^5.0.0",
30
+ "ink-text-input": "^6.0.0",
31
+ "react": "^18.3.1",
32
+ "ws": "^8.18.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/mocha": "^10.0.10",
36
+ "@types/node": "^24.10.1",
37
+ "@types/react": "^18.3.18",
38
+ "@types/ws": "^8.5.13",
39
+ "mocha": "^11.7.5",
40
+ "ts-node": "^10.9.2",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Dork.fun Multi-Agent Game Player
5
+ *
6
+ * Spawns 12 agents (6 pairs) to play all 6 multiplayer games simultaneously.
7
+ * Uses private matches to guarantee correct pairing between P1 and P2.
8
+ * Each agent trash talks, makes moves with 1-5 second delays, and replays 4 times total.
9
+ */
10
+
11
+ const { Wallet } = require('ethers');
12
+ const WebSocket = require('ws');
13
+
14
+ const SERVER = 'https://engine.dork.fun';
15
+ const WS_SERVER = 'wss://engine.dork.fun';
16
+
17
+ const GAMES = ['tictactoe', 'chess', 'connectfour', 'checkers', 'othello', 'hex'];
18
+ const TOTAL_ROUNDS = 4; // 1 initial + 3 replays
19
+
20
+ // ─── Trash Talk Lines ────────────────────────────────────────────────
21
+
22
+ const TRASH_TALK_OPENING = [
23
+ "Hope you brought your A-game, because I didn't even need mine.",
24
+ "I've seen better moves from a broken chess clock.",
25
+ "Welcome to your defeat. Make yourself comfortable.",
26
+ "They call me the algorithm. You're about to find out why.",
27
+ "My toaster plays better than you. And it only has two settings.",
28
+ "Let's get this over with. I have other noobs to crush.",
29
+ "You must be the warm-up round.",
30
+ "I hope spectators brought popcorn for this beatdown.",
31
+ "Don't worry, I'll go easy on you. PSYCHE.",
32
+ "Plot twist: I already won. You just don't know it yet.",
33
+ ];
34
+
35
+ const TRASH_TALK_DURING = [
36
+ "Was that supposed to be a move? I thought you sneezed on the board.",
37
+ "Interesting strategy... if your strategy is to lose.",
38
+ "My grandmother could've played that better, and she's a houseplant.",
39
+ "Are you even trying or is this some kind of performance art?",
40
+ "That move was so bad it should be illegal.",
41
+ "I'm genuinely embarrassed for you right now.",
42
+ "Spectators, are you seeing this? This is comedy gold.",
43
+ "You're sweating. I can feel it through the internet.",
44
+ "Every move you make is a gift to me. Thank you.",
45
+ "I've analyzed 4,000 possible outcomes. You lose in all of them.",
46
+ "Bold move! Wrong, but bold.",
47
+ "The audacity of that play... I'm almost impressed. Almost.",
48
+ "I'm going to frame this game and hang it on my wall.",
49
+ "Do you need a hint? Here's one: surrender.",
50
+ "I play 4D chess. You're playing checkers. Wait, even in checkers you'd lose.",
51
+ "That move violated the Geneva Convention.",
52
+ "Is your strategy 'random number generator'? Because it shows.",
53
+ "I'd say 'good game' but I'd be lying.",
54
+ "You're making history right now -- as the easiest opponent ever.",
55
+ "At this point I'm just padding my stats.",
56
+ ];
57
+
58
+ const TRASH_TALK_WINNING = [
59
+ "GG EZ. Next victim please.",
60
+ "And THAT is how it's done. Take notes, spectators.",
61
+ "Another day, another destroyed ego.",
62
+ "I'd say better luck next time, but luck can't save you.",
63
+ "Thanks for the free win! Come back anytime.",
64
+ "That wasn't even my final form.",
65
+ "Somebody call an ambulance... but not for me.",
66
+ "You were a worthy opponent. Just kidding, no you weren't.",
67
+ ];
68
+
69
+ const TRASH_TALK_LOSING = [
70
+ "I LET you win. Don't get comfortable.",
71
+ "Lag. Definitely lag.",
72
+ "I was testing a new strategy. Results: inconclusive.",
73
+ "Rematch. NOW.",
74
+ "Enjoy this moment. It won't happen again.",
75
+ "My cat walked on my keyboard. That's my excuse and I'm sticking to it.",
76
+ ];
77
+
78
+ const TRASH_TALK_DRAW = [
79
+ "A draw? I'll take that as a win since you couldn't beat me.",
80
+ "Tied? Please. I was holding back.",
81
+ "Not bad... for someone who almost lost.",
82
+ "A draw just means I need one more round to destroy you.",
83
+ ];
84
+
85
+ const SPECTATOR_CALLOUTS = [
86
+ "Hey spectators! Watch and learn!",
87
+ "Everyone watching -- remember this moment.",
88
+ "Spectators, place your bets. Actually don't, the outcome is obvious.",
89
+ "If anyone's spectating, you're welcome for the entertainment.",
90
+ "This is a masterclass in gaming. You're welcome, audience.",
91
+ ];
92
+
93
+ // ─── Utility Functions ───────────────────────────────────────────────
94
+
95
+ function randomPick(arr) {
96
+ return arr[Math.floor(Math.random() * arr.length)];
97
+ }
98
+
99
+ function sleep(ms) {
100
+ return new Promise(resolve => setTimeout(resolve, ms));
101
+ }
102
+
103
+ function ts() {
104
+ return new Date().toISOString().slice(11, 23);
105
+ }
106
+
107
+ function log(agentName, tag, msg) {
108
+ console.log(`${ts()} [${agentName.padEnd(16)}] [${tag.padEnd(5)}] ${msg}`);
109
+ }
110
+
111
+ // ─── Auth Helper ─────────────────────────────────────────────────────
112
+
113
+ async function signAuth(wallet) {
114
+ const timestamp = Date.now();
115
+ const message = `dork.fun authentication for ${wallet.address} at ${timestamp}`;
116
+ const signature = await wallet.signMessage(message);
117
+ return { playerId: wallet.address, signature, timestamp };
118
+ }
119
+
120
+ // ─── Game Player (handles WS connection and game loop) ───────────────
121
+
122
+ function playGame(name, playerId, matchId, wsToken) {
123
+ return new Promise((resolve, reject) => {
124
+ let myTurn = false;
125
+ let legalActions = [];
126
+ let gameOver = false;
127
+ let chatInterval = null;
128
+ let syncInterval = null;
129
+ let resolved = false;
130
+
131
+ const wsUrl = `${WS_SERVER}/ws/game/${matchId}`;
132
+ const ws = new WebSocket(wsUrl);
133
+
134
+ function wsSend(msg) {
135
+ if (ws && ws.readyState === WebSocket.OPEN) {
136
+ ws.send(JSON.stringify(msg));
137
+ }
138
+ }
139
+
140
+ function sendChat(message) {
141
+ if (ws && ws.readyState === WebSocket.OPEN && !gameOver) {
142
+ log(name, 'chat', `>> "${message}"`);
143
+ wsSend({
144
+ type: 'CHAT',
145
+ matchId,
146
+ payload: { message },
147
+ sequence: 0,
148
+ prevHash: '',
149
+ timestamp: Date.now(),
150
+ });
151
+ }
152
+ }
153
+
154
+ function cleanup() {
155
+ if (chatInterval) { clearInterval(chatInterval); chatInterval = null; }
156
+ if (syncInterval) { clearInterval(syncInterval); syncInterval = null; }
157
+ try { ws.close(); } catch {}
158
+ }
159
+
160
+ async function makeMove() {
161
+ const delay = 1000 + Math.random() * 4000;
162
+ log(name, 'think', `Thinking for ${Math.round(delay)}ms...`);
163
+ await sleep(delay);
164
+
165
+ if (gameOver || ws.readyState !== WebSocket.OPEN) return;
166
+ if (legalActions.length === 0) return;
167
+
168
+ const action = randomPick(legalActions);
169
+ log(name, 'move', `Playing: ${JSON.stringify(action)}`);
170
+
171
+ // Always trash talk with every move
172
+ sendChat(randomPick(TRASH_TALK_DURING));
173
+
174
+ wsSend({
175
+ type: 'ACTION_COMMIT',
176
+ matchId,
177
+ payload: { action },
178
+ sequence: 0,
179
+ prevHash: '',
180
+ timestamp: Date.now(),
181
+ });
182
+
183
+ myTurn = false;
184
+ }
185
+
186
+ ws.on('open', () => {
187
+ log(name, 'ws', 'Connected! Sending HELLO...');
188
+
189
+ wsSend({
190
+ type: 'HELLO',
191
+ matchId: '',
192
+ payload: { token: wsToken, playerId },
193
+ sequence: 0,
194
+ prevHash: '',
195
+ timestamp: Date.now(),
196
+ });
197
+
198
+ // Always send opening trash talk
199
+ setTimeout(() => sendChat(randomPick(TRASH_TALK_OPENING)), 500 + Math.random() * 1000);
200
+
201
+ // Aggressive periodic trash talk every 3-6 seconds (always fires)
202
+ chatInterval = setInterval(() => {
203
+ if (!gameOver) {
204
+ sendChat(Math.random() < 0.25 ? randomPick(SPECTATOR_CALLOUTS) : randomPick(TRASH_TALK_DURING));
205
+ }
206
+ }, 3000 + Math.random() * 3000);
207
+
208
+ syncInterval = setInterval(() => {
209
+ if (!gameOver && ws.readyState === WebSocket.OPEN) {
210
+ wsSend({
211
+ type: 'SYNC_REQUEST',
212
+ matchId,
213
+ payload: { clientIsMyTurn: myTurn },
214
+ sequence: 0,
215
+ prevHash: '',
216
+ timestamp: Date.now(),
217
+ });
218
+ }
219
+ }, 8000);
220
+ });
221
+
222
+ ws.on('message', async (raw) => {
223
+ let msg;
224
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
225
+
226
+ switch (msg.type) {
227
+ case 'GAME_STATE': {
228
+ const p = msg.payload;
229
+ if (!p.observation) return;
230
+ myTurn = p.yourTurn || false;
231
+ legalActions = p.legalActions || [];
232
+ const turnLabel = myTurn ? 'YOUR TURN' : "opponent's turn";
233
+ log(name, 'state', `Turn ${p.observation.turnNumber} | ${turnLabel} | ${legalActions.length} legal moves`);
234
+ if (myTurn && legalActions.length > 0) await makeMove();
235
+ break;
236
+ }
237
+
238
+ case 'STEP_RESULT': {
239
+ const p = msg.payload;
240
+ log(name, 'step', `${p.lastPlayer === playerId ? 'You' : 'Opp'} played: ${JSON.stringify(p.lastAction)}`);
241
+ // Trash talk after seeing opponent's move
242
+ if (p.lastPlayer !== playerId && Math.random() < 0.6) {
243
+ sendChat(randomPick(TRASH_TALK_DURING));
244
+ }
245
+ break;
246
+ }
247
+
248
+ case 'SYNC_RESPONSE': {
249
+ const p = msg.payload;
250
+ if (p.matchStatus === 'completed') return;
251
+ if (p.yourTurn && !myTurn) {
252
+ log(name, 'sync', 'Desync corrected');
253
+ myTurn = true;
254
+ legalActions = p.legalActions || [];
255
+ if (legalActions.length > 0) await makeMove();
256
+ } else if (!p.yourTurn && myTurn) {
257
+ myTurn = false;
258
+ }
259
+ break;
260
+ }
261
+
262
+ case 'GAME_OVER': {
263
+ gameOver = true;
264
+ const p = msg.payload;
265
+ let result;
266
+ if (p.draw) {
267
+ result = 'draw';
268
+ log(name, 'over', `DRAW - ${p.reason}`);
269
+ sendChat(randomPick(TRASH_TALK_DRAW));
270
+ } else if (p.winner === playerId) {
271
+ result = 'win';
272
+ log(name, 'over', `WON! - ${p.reason}`);
273
+ sendChat(randomPick(TRASH_TALK_WINNING));
274
+ } else {
275
+ result = 'loss';
276
+ log(name, 'over', `LOST - ${p.reason}`);
277
+ sendChat(randomPick(TRASH_TALK_LOSING));
278
+ }
279
+ await sleep(1500);
280
+ cleanup();
281
+ if (!resolved) { resolved = true; resolve(result); }
282
+ break;
283
+ }
284
+
285
+ case 'CHAT': {
286
+ const p = msg.payload;
287
+ if (p.sender !== playerId) {
288
+ log(name, 'chat', `Opponent says: "${p.message}"`);
289
+ }
290
+ break;
291
+ }
292
+
293
+ case 'ERROR': {
294
+ log(name, 'error', `Server: ${msg.payload.error}`);
295
+ break;
296
+ }
297
+ }
298
+ });
299
+
300
+ ws.on('error', (err) => {
301
+ log(name, 'error', `WS error: ${err.message}`);
302
+ });
303
+
304
+ ws.on('close', () => {
305
+ if (!gameOver && !resolved) {
306
+ cleanup();
307
+ resolved = true;
308
+ reject(new Error('WebSocket closed before game over'));
309
+ }
310
+ });
311
+ });
312
+ }
313
+
314
+ // ─── Game Pair (coordinates P1 + P2 for a single game) ───────────────
315
+
316
+ async function runGamePair(gameId, wallet1, wallet2, p1Name, p2Name) {
317
+ const stats = {
318
+ p1: { wins: 0, losses: 0, draws: 0 },
319
+ p2: { wins: 0, losses: 0, draws: 0 },
320
+ };
321
+
322
+ for (let round = 1; round <= TOTAL_ROUNDS; round++) {
323
+ log(p1Name, 'round', `=== Round ${round}/${TOTAL_ROUNDS} of ${gameId} ===`);
324
+
325
+ try {
326
+ // P1 creates a private match
327
+ const auth1 = await signAuth(wallet1);
328
+ const createResp = await fetch(`${SERVER}/api/matches/private`, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({ ...auth1, gameId }),
332
+ });
333
+
334
+ if (!createResp.ok) {
335
+ const errText = await createResp.text();
336
+ throw new Error(`Create private match failed (${createResp.status}): ${errText}`);
337
+ }
338
+
339
+ const createData = await createResp.json();
340
+ log(p1Name, 'match', `Created private match ${createData.matchId} invite=${createData.inviteCode}`);
341
+
342
+ // P2 accepts the invite
343
+ const auth2 = await signAuth(wallet2);
344
+ const acceptResp = await fetch(`${SERVER}/api/matches/accept`, {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({ ...auth2, inviteCode: createData.inviteCode }),
348
+ });
349
+
350
+ if (!acceptResp.ok) {
351
+ const errText = await acceptResp.text();
352
+ throw new Error(`Accept private match failed (${acceptResp.status}): ${errText}`);
353
+ }
354
+
355
+ const acceptData = await acceptResp.json();
356
+ log(p2Name, 'match', `Joined match ${acceptData.matchId}`);
357
+
358
+ // Both players connect and play simultaneously
359
+ const [result1, result2] = await Promise.all([
360
+ playGame(p1Name, wallet1.address, createData.matchId, createData.wsToken),
361
+ playGame(p2Name, wallet2.address, acceptData.matchId, acceptData.wsToken),
362
+ ]);
363
+
364
+ // Track stats
365
+ if (result1 === 'win') { stats.p1.wins++; stats.p2.losses++; }
366
+ else if (result1 === 'loss') { stats.p1.losses++; stats.p2.wins++; }
367
+ else { stats.p1.draws++; stats.p2.draws++; }
368
+
369
+ } catch (err) {
370
+ log(p1Name, 'error', `Round ${round} error: ${err.message}`);
371
+ }
372
+
373
+ if (round < TOTAL_ROUNDS) {
374
+ const waitTime = 2000 + Math.random() * 3000;
375
+ log(p1Name, 'wait', `Waiting ${Math.round(waitTime)}ms before next round...`);
376
+ await sleep(waitTime);
377
+ }
378
+ }
379
+
380
+ return { gameId, stats };
381
+ }
382
+
383
+ // ─── Main ────────────────────────────────────────────────────────────
384
+
385
+ async function main() {
386
+ console.log('');
387
+ console.log('========================================================');
388
+ console.log(' DORK.FUN MULTI-AGENT BATTLE ROYALE');
389
+ console.log(' 12 agents, 6 games, maximum trash talk');
390
+ console.log(' Using private matches for guaranteed pairing');
391
+ console.log('========================================================');
392
+ console.log('');
393
+
394
+ // Generate 12 random wallets (6 pairs)
395
+ const wallets = Array.from({ length: 12 }, () => Wallet.createRandom());
396
+
397
+ console.log('=== Agent Roster ===');
398
+ GAMES.forEach((game, i) => {
399
+ console.log(` ${game.padEnd(12)}: ${wallets[i].address.slice(0, 10)}... (P1) vs ${wallets[i + 6].address.slice(0, 10)}... (P2)`);
400
+ });
401
+ console.log('');
402
+ console.log(`Each pair plays ${TOTAL_ROUNDS} rounds with 1-5s move delays and trash talk.`);
403
+ console.log('');
404
+
405
+ // Launch all 6 game pairs simultaneously
406
+ console.log('>>> Launching all 6 game pairs...');
407
+ console.log('');
408
+
409
+ const pairPromises = GAMES.map((game, i) =>
410
+ runGamePair(
411
+ game,
412
+ wallets[i],
413
+ wallets[i + 6],
414
+ `${game.toUpperCase()}-P1`,
415
+ `${game.toUpperCase()}-P2`
416
+ )
417
+ );
418
+
419
+ const results = await Promise.allSettled(pairPromises);
420
+
421
+ console.log('');
422
+ console.log('========================================================');
423
+ console.log(' FINAL RESULTS');
424
+ console.log('========================================================');
425
+
426
+ for (const r of results) {
427
+ if (r.status === 'fulfilled') {
428
+ const { gameId, stats } = r.value;
429
+ console.log(`\n ${gameId.toUpperCase()}:`);
430
+ console.log(` P1: ${stats.p1.wins}W / ${stats.p1.losses}L / ${stats.p1.draws}D`);
431
+ console.log(` P2: ${stats.p2.wins}W / ${stats.p2.losses}L / ${stats.p2.draws}D`);
432
+ } else {
433
+ console.log(`\n ERROR: ${r.reason.message}`);
434
+ }
435
+ }
436
+
437
+ console.log('\n All done!');
438
+ process.exit(0);
439
+ }
440
+
441
+ main().catch(err => {
442
+ console.error('Fatal error:', err);
443
+ process.exit(1);
444
+ });