Skip to content

Commit 00720dd

Browse files
committed
feat(tictactoe): Enhance agent messaging to support both agentId and likuId
1 parent d8225c7 commit 00720dd

File tree

3 files changed

+636
-2
lines changed

3 files changed

+636
-2
lines changed

scripts/test-ai-vs-ai.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/**
2+
* AI-vs-AI TicTacToe Test Script
3+
*
4+
* Tests the WebSocket-based AI-vs-AI game session functionality
5+
* Two AI agents connect, create a session, and play TicTacToe
6+
*/
7+
8+
import WebSocket from 'ws';
9+
10+
const WS_URL = 'ws://localhost:3847';
11+
12+
// Simple AI that uses the minimax hint from the server
13+
class SimpleAI {
14+
constructor(name, slot) {
15+
this.name = name;
16+
this.slot = slot; // 'X' or 'O'
17+
this.ws = null;
18+
this.agentId = null;
19+
this.sessionId = null;
20+
this.isMyTurn = false;
21+
this.gameOver = false;
22+
}
23+
24+
connect() {
25+
return new Promise((resolve, reject) => {
26+
this.ws = new WebSocket(`${WS_URL}?name=${this.name}&type=ai`);
27+
28+
this.ws.on('open', () => {
29+
console.log(`[${this.name}] Connected to server`);
30+
// Subscribe to all events
31+
this.send({
32+
type: 'subscribe',
33+
payload: {
34+
events: ['*', 'session:gameStarted', 'session:yourTurn', 'session:moveMade', 'session:gameEnded', 'session:playerJoined', 'session:turnChanged']
35+
},
36+
requestId: `subscribe-${Date.now()}`
37+
});
38+
resolve();
39+
});
40+
41+
this.ws.on('message', (data) => {
42+
const msg = JSON.parse(data.toString());
43+
this.handleMessage(msg);
44+
});
45+
46+
this.ws.on('error', (err) => {
47+
console.error(`[${this.name}] Error:`, err.message);
48+
reject(err);
49+
});
50+
51+
this.ws.on('close', () => {
52+
console.log(`[${this.name}] Disconnected`);
53+
});
54+
});
55+
}
56+
57+
handleMessage(msg) {
58+
// Verbose logging for debugging
59+
if (msg.type === 'ack' || msg.type === 'event') {
60+
console.log(`[${this.name}] MSG:`, JSON.stringify(msg, null, 2));
61+
}
62+
63+
switch (msg.type) {
64+
case 'welcome':
65+
this.agentId = msg.data.agent.id;
66+
console.log(`[${this.name}] Registered as agent: ${this.agentId.slice(0, 8)}...`);
67+
break;
68+
69+
case 'ack':
70+
if (msg.data?.sessionId) {
71+
this.sessionId = msg.data.sessionId;
72+
console.log(`[${this.name}] Session: ${msg.data.action} - ${this.sessionId?.slice(0, 8) || 'N/A'}`);
73+
}
74+
if (msg.data?.action === 'game_move' && msg.data?.move) {
75+
console.log(`[${this.name}] Move confirmed at (${msg.data.move.row},${msg.data.move.col})`);
76+
}
77+
break;
78+
79+
case 'event':
80+
this.handleEvent(msg.data);
81+
break;
82+
83+
case 'error':
84+
console.error(`[${this.name}] Error:`, msg.data);
85+
break;
86+
}
87+
}
88+
89+
handleEvent(event) {
90+
const eventType = event.event;
91+
92+
switch (eventType) {
93+
case 'session:created':
94+
console.log(`[${this.name}] Session created: ${event.sessionId?.slice(0, 8)}`);
95+
break;
96+
97+
case 'session:playerJoined':
98+
console.log(`[${this.name}] Player joined: ${event.agentId?.slice(0, 8)} as ${event.slot || event.player?.slot}`);
99+
break;
100+
101+
case 'session:gameStarted':
102+
console.log(`[${this.name}] Game started!`);
103+
// Check if it's our turn based on state
104+
if (event.state?.currentTurn === this.slot) {
105+
this.checkTurn(this.slot);
106+
}
107+
break;
108+
109+
case 'session:yourTurn':
110+
console.log(`[${this.name}] 🎯 It's my turn! (slot: ${event.slot})`);
111+
this.checkTurn(event.slot);
112+
break;
113+
114+
case 'session:turnChanged':
115+
console.log(`[${this.name}] Turn changed to: ${event.slot}`);
116+
break;
117+
118+
case 'session:moveMade':
119+
console.log(`[${this.name}] Move made by ${event.agentId?.slice(0, 8) || 'player'} at (${event.move?.row ?? event.row},${event.move?.col ?? event.col})`);
120+
if (event.board) {
121+
this.printBoard(event.board);
122+
} else if (event.state?.board) {
123+
this.printBoard(event.state.board);
124+
}
125+
break;
126+
127+
case 'session:gameEnded':
128+
this.gameOver = true;
129+
console.log(`\n[${this.name}] 🎮 GAME OVER!`);
130+
console.log(`[${this.name}] Winner: ${event.winner || 'Draw'}`);
131+
if (event.winningLine) {
132+
console.log(`[${this.name}] Winning line: ${JSON.stringify(event.winningLine)}`);
133+
}
134+
break;
135+
136+
default:
137+
console.log(`[${this.name}] Unknown event: ${eventType}`);
138+
}
139+
}
140+
141+
checkTurn(currentTurn) {
142+
if (currentTurn === this.slot && !this.gameOver) {
143+
this.isMyTurn = true;
144+
console.log(`[${this.name}] 🎯 My turn!`);
145+
// Add slight delay to make output readable
146+
setTimeout(() => this.makeMove(), 500);
147+
} else {
148+
this.isMyTurn = false;
149+
}
150+
}
151+
152+
printBoard(board) {
153+
if (!board) return;
154+
console.log(`[${this.name}] Board:`);
155+
for (let row = 0; row < 3; row++) {
156+
const cells = board[row].map(c => c || '.').join(' | ');
157+
console.log(` ${cells}`);
158+
if (row < 2) console.log(` ---+---+---`);
159+
}
160+
}
161+
162+
makeMove() {
163+
if (!this.isMyTurn || this.gameOver) return;
164+
165+
// Simple strategy: try center first, then corners, then edges
166+
const moves = [
167+
[1, 1], // center
168+
[0, 0], [0, 2], [2, 0], [2, 2], // corners
169+
[0, 1], [1, 0], [1, 2], [2, 1] // edges
170+
];
171+
172+
// For testing, just try moves in order
173+
// The server will tell us if a move is invalid
174+
const [row, col] = moves[Math.floor(Math.random() * moves.length)];
175+
console.log(`[${this.name}] Attempting move at (${row}, ${col})`);
176+
177+
this.send({
178+
type: 'action',
179+
payload: {
180+
action: 'game_move',
181+
sessionId: this.sessionId,
182+
row,
183+
col
184+
},
185+
requestId: `move-${Date.now()}`
186+
});
187+
this.isMyTurn = false;
188+
}
189+
190+
send(msg) {
191+
if (this.ws?.readyState === WebSocket.OPEN) {
192+
this.ws.send(JSON.stringify(msg));
193+
}
194+
}
195+
196+
createSession() {
197+
console.log(`[${this.name}] Creating TicTacToe session...`);
198+
this.send({
199+
type: 'action',
200+
payload: {
201+
action: 'game_create',
202+
gameType: 'tictactoe',
203+
mode: 'ai_vs_ai',
204+
turnTimeMs: 5000,
205+
allowSpectators: true
206+
},
207+
requestId: `create-${Date.now()}`
208+
});
209+
}
210+
211+
joinSession(sessionId, slot) {
212+
this.sessionId = sessionId;
213+
console.log(`[${this.name}] Joining session ${sessionId.slice(0, 8)} as ${slot}...`);
214+
this.send({
215+
type: 'action',
216+
payload: {
217+
action: 'game_join',
218+
sessionId,
219+
slot,
220+
playerType: 'ai',
221+
name: this.name
222+
},
223+
requestId: `join-${Date.now()}`
224+
});
225+
}
226+
227+
setReady() {
228+
console.log(`[${this.name}] Setting ready...`);
229+
this.send({
230+
type: 'action',
231+
payload: {
232+
action: 'game_ready',
233+
sessionId: this.sessionId,
234+
ready: true
235+
},
236+
requestId: `ready-${Date.now()}`
237+
});
238+
}
239+
240+
close() {
241+
if (this.ws) {
242+
this.ws.close();
243+
}
244+
}
245+
}
246+
247+
// Main test
248+
async function main() {
249+
console.log('='.repeat(60));
250+
console.log('🎮 AI-vs-AI TicTacToe Test');
251+
console.log('='.repeat(60));
252+
console.log('');
253+
254+
const agent1 = new SimpleAI('Agent-X', 'X');
255+
const agent2 = new SimpleAI('Agent-O', 'O');
256+
257+
try {
258+
// Connect both agents
259+
console.log('Connecting agents...\n');
260+
await agent1.connect();
261+
await agent2.connect();
262+
263+
// Wait for welcome messages
264+
await new Promise(r => setTimeout(r, 500));
265+
266+
// Agent 1 creates session
267+
agent1.createSession();
268+
269+
// Wait for session creation
270+
await new Promise(r => setTimeout(r, 1000));
271+
272+
// Get session ID from agent1
273+
const sessionId = agent1.sessionId;
274+
if (!sessionId) {
275+
console.error('Failed to create session!');
276+
process.exit(1);
277+
}
278+
279+
// Agent 1 joins as X
280+
agent1.joinSession(sessionId, 'X');
281+
await new Promise(r => setTimeout(r, 500));
282+
283+
// Agent 2 joins as O
284+
agent2.joinSession(sessionId, 'O');
285+
await new Promise(r => setTimeout(r, 500));
286+
287+
// Both agents set ready
288+
agent1.setReady();
289+
agent2.setReady();
290+
291+
// Wait for game to complete (max 30 seconds)
292+
console.log('\n⏳ Waiting for game to complete...\n');
293+
294+
let timeout = 30;
295+
while (!agent1.gameOver && !agent2.gameOver && timeout > 0) {
296+
await new Promise(r => setTimeout(r, 1000));
297+
timeout--;
298+
}
299+
300+
if (timeout === 0) {
301+
console.log('⏱️ Timeout - game did not complete in 30 seconds');
302+
}
303+
304+
console.log('\n' + '='.repeat(60));
305+
console.log('Test complete!');
306+
console.log('='.repeat(60));
307+
308+
} catch (err) {
309+
console.error('Test failed:', err);
310+
} finally {
311+
agent1.close();
312+
agent2.close();
313+
process.exit(0);
314+
}
315+
}
316+
317+
main();

0 commit comments

Comments
 (0)