ravithejads commited on
Commit
7bfb4cb
·
verified ·
1 Parent(s): a3c3b62

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +29 -0
  2. README.md +140 -4
  3. app.py +460 -0
  4. game.js +275 -0
  5. mcp_server.py +435 -0
  6. requirements.txt +5 -0
  7. room_game.html +341 -0
  8. room_game.js +371 -0
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Copy requirements first for better caching
7
+ COPY requirements.txt .
8
+
9
+ # Install dependencies
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # Copy all application files
13
+ COPY . .
14
+
15
+ # Expose port 7860 for Hugging Face Spaces
16
+ EXPOSE 7860
17
+
18
+ # Create startup script that runs the MCP server (primary) and web UI (secondary)
19
+ RUN echo '#!/bin/bash\n\
20
+ echo "Starting Tic-Tac-Toe MCP Server on port 7860..."\n\
21
+ echo "This is the MCP server for Claude/LeChat integration"\n\
22
+ echo "Web UI available at /rooms-ui endpoint"\n\
23
+ echo "MCP Tools: create_room, make_move, send_chat, get_room_state, list_rooms"\n\
24
+ python mcp_server.py' > start.sh && chmod +x start.sh
25
+
26
+ # Set required environment variable for Mistral API
27
+ ENV MISTRAL_API_KEY=""
28
+
29
+ CMD ["./start.sh"]
README.md CHANGED
@@ -1,10 +1,146 @@
1
  ---
2
- title: Tictactoe
3
- emoji: 👁
4
- colorFrom: green
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Tic-Tac-Toe MCP Server
3
+ emoji: 🎮
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # 🎮 Tic-Tac-Toe MCP Server for LeChat
11
+
12
+ A sophisticated tic-tac-toe game server with **MCP (Model Context Protocol)** integration for use with LeChat.
13
+
14
+ ## 🚀 Features
15
+
16
+ - **MCP Tools Integration** - Direct use with LeChat via MCP
17
+ - **Room-based Gameplay** - Multiple isolated game sessions
18
+ - **AI Opponent** - Powered by Mistral AI with personality
19
+ - **Real-time Chat** - Interactive conversations with the AI
20
+ - **Live Markdown State** - Perfect representation for AI consumption
21
+ - **Multi-room Support** - Create and switch between multiple games
22
+
23
+ ## 🔧 MCP Tools Available
24
+
25
+ ### Core Game Tools
26
+ - `create_room()` - Create a new tic-tac-toe game room
27
+ - `make_move(position)` - Make your move (0-8) and get AI response
28
+ - `send_chat(message)` - Chat with Mistral AI in your game
29
+ - `get_room_state()` - Get current game state in markdown
30
+ - `list_rooms()` - See all your active games
31
+ - `switch_room(room_id)` - Change between games
32
+
33
+ ### Utility Tools
34
+ - `get_help()` - Game instructions and board layout
35
+ - `wait_5_seconds()` - Timing utility for operations
36
+
37
+ ## 🎯 How to Play with LeChat
38
+
39
+ 1. **Connect to this MCP server** in LeChat
40
+ 2. **Start a game**: `create_room()`
41
+ 3. **Make moves**: `make_move(4)` (center square)
42
+ 4. **Chat with AI**: `send_chat("Good move!")`
43
+ 5. **Check state**: `get_room_state()`
44
+
45
+ ### Board Layout (Positions 0-8):
46
+ ```
47
+ 0 | 1 | 2
48
+ ---------
49
+ 3 | 4 | 5
50
+ ---------
51
+ 6 | 7 | 8
52
+ ```
53
+
54
+ ## 📋 Example Game Flow
55
+
56
+ ```python
57
+ # Start a new game
58
+ create_room()
59
+ # Output: Created room abc12345, you are X, AI is O
60
+
61
+ # Make your first move (center)
62
+ make_move(4)
63
+ # Output: You played X at 4, AI played O at 2 with trash talk!
64
+
65
+ # Chat with the AI
66
+ send_chat("Nice try, but I've got this!")
67
+ # Output: Your message + AI's witty response
68
+
69
+ # Check current state anytime
70
+ get_room_state()
71
+ # Output: Full markdown representation of game
72
+ ```
73
+
74
+ ## 📊 Markdown State Format
75
+
76
+ The AI receives game state in this format:
77
+ ```markdown
78
+ # Game Room: abc12345
79
+ ## Status: Your turn (X to play)
80
+ Moves: 4/9
81
+
82
+ ```
83
+ X | | O
84
+ -----------
85
+ | X |
86
+ -----------
87
+ O | |
88
+ ```
89
+
90
+ ## Recent Chat
91
+ **You:** Nice try!
92
+ **Mistral AI:** Just wait and see! 😏
93
+ ```
94
+
95
+ ## 🔐 Setup Requirements
96
+
97
+ 1. **Mistral API Key**: Required for AI functionality
98
+ - Get from [console.mistral.ai](https://console.mistral.ai)
99
+ - Set as `MISTRAL_API_KEY` environment variable
100
+
101
+ 2. **MCP Connection**: Configure in LeChat
102
+ - Server URL: This Hugging Face Space URL
103
+ - Transport: SSE (Server-Sent Events)
104
+ - Port: 7860
105
+
106
+ ## 🌐 Web UI (Optional)
107
+
108
+ While designed for MCP, there's also a web interface at `/rooms-ui` for testing and manual play.
109
+
110
+ ## 🏗️ Technical Details
111
+
112
+ - **Backend**: Python Flask + FastMCP
113
+ - **AI**: Mistral AI with competitive personality
114
+ - **Protocol**: MCP (Model Context Protocol)
115
+ - **Transport**: SSE for real-time communication
116
+ - **State Management**: In-memory room storage
117
+ - **Format**: Markdown for AI-friendly representation
118
+
119
+ ## 🎮 Game Features
120
+
121
+ - **Competitive AI**: Mistral trash talks and celebrates
122
+ - **Multiple Rooms**: Play several games simultaneously
123
+ - **Chat Integration**: Full conversation with game context
124
+ - **State Persistence**: Games persist during session
125
+ - **Error Handling**: Graceful handling of invalid moves
126
+ - **Turn Management**: Automatic turn switching and validation
127
+
128
+ ## 🤖 Perfect for AI Assistants
129
+
130
+ This MCP server is specifically designed for LeChat:
131
+ - **Structured Output**: All responses in consistent JSON format
132
+ - **Markdown Representation**: Perfect for AI understanding
133
+ - **Context Preservation**: Room state maintained between calls
134
+ - **Error Messages**: Clear feedback for invalid operations
135
+ - **Multi-session**: Each AI conversation can have multiple games
136
+
137
+ ## 🚀 Get Started
138
+
139
+ 1. **In LeChat**: Connect to this MCP server
140
+ 2. **Type**: `create_room()` to start your first game
141
+ 3. **Play**: Use `make_move(position)` to play
142
+ 4. **Enjoy**: Chat and compete with Mistral AI!
143
+
144
+ ---
145
+
146
+ **Built for the MCP ecosystem** 🤖 **Ready for AI-powered gaming** 🎮
app.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, render_template, send_from_directory
2
+ import os
3
+ from flask_cors import CORS
4
+ import logging
5
+ import json
6
+ import uuid
7
+ import time
8
+ from mistralai import Mistral
9
+
10
+ app = Flask(__name__)
11
+ CORS(app)
12
+
13
+ # Set up logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Initialize Mistral client
18
+ MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
19
+ if not MISTRAL_API_KEY:
20
+ logger.error("MISTRAL_API_KEY not configured")
21
+ exit(1)
22
+
23
+ client = Mistral(api_key=MISTRAL_API_KEY)
24
+
25
+ class Room:
26
+ def __init__(self, room_id=None):
27
+ self.id = room_id or str(uuid.uuid4())[:8]
28
+ self.board = [''] * 9
29
+ self.current_player = 'X' # X = human, O = AI
30
+ self.game_status = 'active' # 'active', 'won', 'draw'
31
+ self.winner = None
32
+ self.chat_history = []
33
+ self.created = time.time()
34
+ self.last_activity = time.time()
35
+ self.moves_count = 0
36
+
37
+ # Add welcome message
38
+ self.chat_history.append({
39
+ 'sender': 'ai',
40
+ 'message': "Hey there! Ready for a game of Tic-Tac-Toe? I'm pretty good at this... 😏 You're X, I'm O. Good luck!",
41
+ 'timestamp': time.time()
42
+ })
43
+
44
+ def make_move(self, position, player):
45
+ if self.game_status != 'active' or self.board[position] != '':
46
+ return False
47
+
48
+ self.board[position] = player
49
+ self.moves_count += 1
50
+ self.last_activity = time.time()
51
+
52
+ # Check for winner
53
+ if self.check_winner():
54
+ self.game_status = 'won'
55
+ self.winner = player
56
+ elif self.moves_count == 9:
57
+ self.game_status = 'draw'
58
+ else:
59
+ self.current_player = 'O' if player == 'X' else 'X'
60
+
61
+ return True
62
+
63
+ def check_winner(self):
64
+ win_patterns = [
65
+ [0, 1, 2], [3, 4, 5], [6, 7, 8], # rows
66
+ [0, 3, 6], [1, 4, 7], [2, 5, 8], # columns
67
+ [0, 4, 8], [2, 4, 6] # diagonals
68
+ ]
69
+
70
+ for pattern in win_patterns:
71
+ a, b, c = pattern
72
+ if self.board[a] and self.board[a] == self.board[b] == self.board[c]:
73
+ return True
74
+ return False
75
+
76
+ def add_chat_message(self, message, sender):
77
+ self.chat_history.append({
78
+ 'sender': sender,
79
+ 'message': message,
80
+ 'timestamp': time.time()
81
+ })
82
+ self.last_activity = time.time()
83
+
84
+ def to_markdown(self):
85
+ # Game header
86
+ markdown = f"# Game Room: {self.id}\n"
87
+ markdown += f"## Status: "
88
+
89
+ if self.game_status == 'won':
90
+ winner_name = "You" if self.winner == 'X' else "Mistral AI"
91
+ markdown += f"Game Over - {winner_name} wins! 🎉\n"
92
+ elif self.game_status == 'draw':
93
+ markdown += "Game Over - It's a draw! 🤝\n"
94
+ else:
95
+ turn_name = "Your turn" if self.current_player == 'X' else "Mistral's turn"
96
+ markdown += f"{turn_name} ({self.current_player} to play)\n"
97
+
98
+ markdown += f"Moves: {self.moves_count}/9\n\n"
99
+
100
+ # Board representation
101
+ markdown += "```\n"
102
+ for i in range(0, 9, 3):
103
+ row = [self.board[i] or ' ', self.board[i+1] or ' ', self.board[i+2] or ' ']
104
+ markdown += f" {row[0]} | {row[1]} | {row[2]} \n"
105
+ if i < 6:
106
+ markdown += "-----------\n"
107
+ markdown += "```\n\n"
108
+
109
+ # Chat history (last 5 messages)
110
+ if self.chat_history:
111
+ markdown += "## Recent Chat\n"
112
+ recent_messages = self.chat_history[-5:]
113
+ for msg in recent_messages:
114
+ sender_name = "**You:**" if msg['sender'] == 'user' else "**Mistral AI:**"
115
+ markdown += f"{sender_name} {msg['message']}\n"
116
+
117
+ return markdown
118
+
119
+ def to_dict(self):
120
+ return {
121
+ 'id': self.id,
122
+ 'board': self.board,
123
+ 'current_player': self.current_player,
124
+ 'game_status': self.game_status,
125
+ 'winner': self.winner,
126
+ 'chat_history': self.chat_history,
127
+ 'moves_count': self.moves_count,
128
+ 'created': self.created,
129
+ 'last_activity': self.last_activity
130
+ }
131
+
132
+ # In-memory room storage
133
+ rooms = {}
134
+
135
+ # Room management endpoints
136
+ @app.route('/rooms', methods=['POST'])
137
+ def create_room():
138
+ room = Room()
139
+ rooms[room.id] = room
140
+ logger.info(f"Created room: {room.id}")
141
+ return jsonify({
142
+ 'room_id': room.id,
143
+ 'status': 'created',
144
+ 'room_data': room.to_dict()
145
+ })
146
+
147
+ @app.route('/rooms/<room_id>', methods=['GET'])
148
+ def get_room(room_id):
149
+ if room_id not in rooms:
150
+ return jsonify({'error': 'Room not found'}), 404
151
+
152
+ room = rooms[room_id]
153
+ return jsonify({
154
+ 'room_id': room_id,
155
+ 'room_data': room.to_dict(),
156
+ 'markdown': room.to_markdown()
157
+ })
158
+
159
+ @app.route('/rooms/<room_id>/move', methods=['POST'])
160
+ def make_room_move(room_id):
161
+ if room_id not in rooms:
162
+ return jsonify({'error': 'Room not found'}), 404
163
+
164
+ room = rooms[room_id]
165
+ data = request.json
166
+ position = data.get('position')
167
+
168
+ if position is None or position < 0 or position > 8:
169
+ return jsonify({'error': 'Invalid position'}), 400
170
+
171
+ # Make human move
172
+ if not room.make_move(position, 'X'):
173
+ return jsonify({'error': 'Invalid move'}), 400
174
+
175
+ # Check if game ended
176
+ if room.game_status != 'active':
177
+ return jsonify({
178
+ 'room_data': room.to_dict(),
179
+ 'markdown': room.to_markdown(),
180
+ 'ai_move': None
181
+ })
182
+
183
+ # Get AI move
184
+ try:
185
+ ai_response = get_ai_move_for_room(room)
186
+ if ai_response and 'move' in ai_response:
187
+ # Validate AI move
188
+ ai_move = ai_response['move']
189
+ if 0 <= ai_move <= 8 and room.board[ai_move] == '':
190
+ room.make_move(ai_move, 'O')
191
+ if 'message' in ai_response:
192
+ room.add_chat_message(ai_response['message'], 'ai')
193
+ else:
194
+ logger.error(f"AI chose invalid move: {ai_move}, board: {room.board}")
195
+ # Fallback to random valid move
196
+ empty_positions = [i for i in range(9) if room.board[i] == '']
197
+ if empty_positions:
198
+ fallback_move = empty_positions[0] # Take first available
199
+ room.make_move(fallback_move, 'O')
200
+ room.add_chat_message("Oops, had a brain freeze! But I'm still playing! 🤖", 'ai')
201
+
202
+ return jsonify({
203
+ 'room_data': room.to_dict(),
204
+ 'markdown': room.to_markdown(),
205
+ 'ai_move': ai_response
206
+ })
207
+ except Exception as e:
208
+ logger.error(f"AI move failed: {e}")
209
+ # Fallback to random valid move instead of failing
210
+ empty_positions = [i for i in range(9) if room.board[i] == '']
211
+ if empty_positions:
212
+ fallback_move = empty_positions[0]
213
+ room.make_move(fallback_move, 'O')
214
+ room.add_chat_message("Technical difficulties, but I'm improvising! 😅", 'ai')
215
+
216
+ return jsonify({
217
+ 'room_data': room.to_dict(),
218
+ 'markdown': room.to_markdown(),
219
+ 'ai_move': {'move': fallback_move if empty_positions else None, 'message': 'Technical difficulties!'}
220
+ })
221
+
222
+ @app.route('/rooms/<room_id>/chat', methods=['POST'])
223
+ def room_chat(room_id):
224
+ if room_id not in rooms:
225
+ return jsonify({'error': 'Room not found'}), 404
226
+
227
+ room = rooms[room_id]
228
+ data = request.json
229
+ user_message = data.get('message', '')
230
+
231
+ if not user_message.strip():
232
+ return jsonify({'error': 'Empty message'}), 400
233
+
234
+ # Add user message
235
+ room.add_chat_message(user_message, 'user')
236
+
237
+ # Get AI response
238
+ try:
239
+ ai_response = get_ai_chat_for_room(room, user_message)
240
+ room.add_chat_message(ai_response, 'ai')
241
+
242
+ return jsonify({
243
+ 'room_data': room.to_dict(),
244
+ 'markdown': room.to_markdown(),
245
+ 'ai_response': ai_response
246
+ })
247
+ except Exception as e:
248
+ logger.error(f"AI chat failed: {e}")
249
+ return jsonify({'error': 'AI chat failed'}), 500
250
+
251
+ @app.route('/rooms/<room_id>/markdown', methods=['GET'])
252
+ def get_room_markdown(room_id):
253
+ if room_id not in rooms:
254
+ return jsonify({'error': 'Room not found'}), 404
255
+
256
+ room = rooms[room_id]
257
+ return jsonify({
258
+ 'room_id': room_id,
259
+ 'markdown': room.to_markdown()
260
+ })
261
+
262
+ # Helper functions for AI interactions
263
+ def get_ai_move_for_room(room):
264
+ board_string = ""
265
+ for i in range(0, 9, 3):
266
+ row = [room.board[i] or ' ', room.board[i+1] or ' ', room.board[i+2] or ' ']
267
+ board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
268
+ if i < 6:
269
+ board_string += "---------\n"
270
+
271
+ messages = [
272
+ {
273
+ "role": "system",
274
+ "content": """You are a competitive Tic-Tac-Toe AI with personality. You play as 'O' and the human plays as 'X'.
275
+
276
+ Rules:
277
+ 1. Analyze the board and choose your best move (0-8, left to right, top to bottom)
278
+ 2. Add a short, witty comment about your move or the game state
279
+ 3. Be competitive but fun - trash talk, celebrate good moves, react to the situation
280
+ 4. Keep messages under 50 words
281
+ 5. Use emojis occasionally
282
+
283
+ ALWAYS respond with valid JSON in this exact format:
284
+ {"move": [0-8], "message": "your witty comment"}
285
+
286
+ Board positions:
287
+ 0 | 1 | 2
288
+ ---------
289
+ 3 | 4 | 5
290
+ ---------
291
+ 6 | 7 | 8"""
292
+ },
293
+ {
294
+ "role": "user",
295
+ "content": f"Current board:\n{board_string}\n\nBoard array: {room.board}"
296
+ }
297
+ ]
298
+
299
+ response = client.chat.complete(
300
+ model="mistral-large-latest",
301
+ messages=messages,
302
+ temperature=0.1,
303
+ response_format={"type": "json_object"}
304
+ )
305
+
306
+ return json.loads(response.choices[0].message.content)
307
+
308
+ def get_ai_chat_for_room(room, user_message):
309
+ board_string = ""
310
+ for i in range(0, 9, 3):
311
+ row = [room.board[i] or ' ', room.board[i+1] or ' ', room.board[i+2] or ' ']
312
+ board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
313
+ if i < 6:
314
+ board_string += "---------\n"
315
+
316
+ messages = [
317
+ {
318
+ "role": "system",
319
+ "content": f"""You are a competitive, witty Tic-Tac-Toe AI with personality. You're currently playing a game.
320
+
321
+ Current board state:
322
+ {board_string}
323
+
324
+ Respond to the human's message with personality - be competitive, funny, encouraging, or trash-talking as appropriate.
325
+ Keep responses under 50 words. Use emojis occasionally. Don't make game moves in chat - that happens separately."""
326
+ },
327
+ {
328
+ "role": "user",
329
+ "content": user_message
330
+ }
331
+ ]
332
+
333
+ response = client.chat.complete(
334
+ model="mistral-large-latest",
335
+ messages=messages
336
+ )
337
+
338
+ return response.choices[0].message.content
339
+
340
+ @app.route('/ai-move', methods=['POST'])
341
+ def get_ai_move():
342
+ try:
343
+ data = request.json
344
+ board = data.get('board', [])
345
+
346
+ # Convert board to string representation
347
+ board_string = ""
348
+ for i in range(0, 9, 3):
349
+ row = [board[i] or ' ', board[i+1] or ' ', board[i+2] or ' ']
350
+ board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
351
+ if i < 6:
352
+ board_string += "---------\n"
353
+
354
+ messages = [
355
+ {
356
+ "role": "system",
357
+ "content": """You are a competitive Tic-Tac-Toe AI with personality. You play as 'O' and the human plays as 'X'.
358
+
359
+ Rules:
360
+ 1. Analyze the board and choose your best move (0-8, left to right, top to bottom)
361
+ 2. Add a short, witty comment about your move or the game state
362
+ 3. Be competitive but fun - trash talk, celebrate good moves, react to the situation
363
+ 4. Keep messages under 50 words
364
+ 5. Use emojis occasionally
365
+
366
+ ALWAYS respond with valid JSON in this exact format:
367
+ {"move": [0-8], "message": "your witty comment"}
368
+
369
+ Board positions:
370
+ 0 | 1 | 2
371
+ ---------
372
+ 3 | 4 | 5
373
+ ---------
374
+ 6 | 7 | 8"""
375
+ },
376
+ {
377
+ "role": "user",
378
+ "content": f"Current board:\n{board_string}\n\nBoard array: {board}"
379
+ }
380
+ ]
381
+
382
+ response = client.chat.complete(
383
+ model="mistral-large-latest",
384
+ messages=messages,
385
+ temperature=0.1,
386
+ response_format={"type": "json_object"}
387
+ )
388
+
389
+ ai_response = json.loads(response.choices[0].message.content)
390
+ logger.info(f"AI response: {ai_response}")
391
+
392
+ return jsonify(ai_response)
393
+
394
+ except Exception as e:
395
+ logger.error(f"Error getting AI move: {str(e)}")
396
+ return jsonify({"error": str(e)}), 500
397
+
398
+ @app.route('/ai-chat', methods=['POST'])
399
+ def get_ai_chat():
400
+ try:
401
+ data = request.json
402
+ user_message = data.get('message', '')
403
+ board = data.get('board', [])
404
+
405
+ # Convert board to string representation
406
+ board_string = ""
407
+ for i in range(0, 9, 3):
408
+ row = [board[i] or ' ', board[i+1] or ' ', board[i+2] or ' ']
409
+ board_string += f"{row[0]} | {row[1]} | {row[2]}\n"
410
+ if i < 6:
411
+ board_string += "---------\n"
412
+
413
+ messages = [
414
+ {
415
+ "role": "system",
416
+ "content": f"""You are a competitive, witty Tic-Tac-Toe AI with personality. You're currently playing a game.
417
+
418
+ Current board state:
419
+ {board_string}
420
+
421
+ Respond to the human's message with personality - be competitive, funny, encouraging, or trash-talking as appropriate.
422
+ Keep responses under 50 words. Use emojis occasionally. Don't make game moves in chat - that happens separately."""
423
+ },
424
+ {
425
+ "role": "user",
426
+ "content": user_message
427
+ }
428
+ ]
429
+
430
+ response = client.chat.complete(
431
+ model="mistral-large-latest",
432
+ messages=messages
433
+ )
434
+
435
+ ai_message = response.choices[0].message.content
436
+ logger.info(f"AI chat response: {ai_message}")
437
+
438
+ return jsonify({"message": ai_message})
439
+
440
+ except Exception as e:
441
+ logger.error(f"Error getting AI chat: {str(e)}")
442
+ return jsonify({"error": str(e)}), 500
443
+
444
+ # Serve the main game page (original)
445
+ @app.route('/')
446
+ def index():
447
+ return send_from_directory('.', 'simple_game.html')
448
+
449
+ # Serve the room-based game page
450
+ @app.route('/rooms-ui')
451
+ def rooms_ui():
452
+ return send_from_directory('.', 'room_game.html')
453
+
454
+ # Serve static files
455
+ @app.route('/<path:path>')
456
+ def serve_static(path):
457
+ return send_from_directory('.', path)
458
+
459
+ if __name__ == '__main__':
460
+ app.run(host='0.0.0.0', port=5001, debug=True)
game.js ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TicTacToeGame {
2
+ constructor() {
3
+ this.board = Array(9).fill('');
4
+ this.currentPlayer = 'X'; // Human is X, AI is O
5
+ this.gameActive = true;
6
+ this.cells = document.querySelectorAll('.cell');
7
+ this.gameStatus = document.getElementById('gameStatus');
8
+
9
+ this.initGame();
10
+ }
11
+
12
+ initGame() {
13
+ this.cells.forEach((cell, index) => {
14
+ cell.addEventListener('click', () => this.handleCellClick(index));
15
+ });
16
+ }
17
+
18
+ async handleCellClick(index) {
19
+ if (!this.gameActive || this.board[index] !== '' || this.currentPlayer !== 'X') {
20
+ return;
21
+ }
22
+
23
+ // Make human move
24
+ this.makeMove(index, 'X');
25
+
26
+ // Check for win/draw
27
+ if (this.checkGameEnd()) {
28
+ return;
29
+ }
30
+
31
+ // AI's turn
32
+ this.gameStatus.textContent = "Mistral is thinking...";
33
+ await this.makeAIMove();
34
+ }
35
+
36
+ makeMove(index, player) {
37
+ this.board[index] = player;
38
+ this.cells[index].textContent = player;
39
+ this.cells[index].classList.add(player.toLowerCase());
40
+ }
41
+
42
+ async makeAIMove() {
43
+ try {
44
+ const aiMove = await this.getAIMoveAndChat();
45
+ console.log('AI Move received:', aiMove);
46
+ console.log('Current board:', this.board);
47
+ console.log('Move valid?', aiMove.move >= 0 && aiMove.move < 9);
48
+ console.log('Square empty?', this.board[aiMove.move] === '');
49
+
50
+ if (aiMove && typeof aiMove.move === 'number' && aiMove.move >= 0 && aiMove.move < 9 && this.board[aiMove.move] === '') {
51
+ this.makeMove(aiMove.move, 'O');
52
+
53
+ // Add AI's chat message
54
+ if (aiMove.message) {
55
+ this.addChatMessage(aiMove.message, 'ai');
56
+ }
57
+
58
+ if (!this.checkGameEnd()) {
59
+ this.gameStatus.textContent = "Your turn! Click a square.";
60
+ }
61
+ } else {
62
+ // Fallback to random move
63
+ console.warn('AI move invalid, using fallback. Move:', aiMove.move, 'Board:', this.board);
64
+ const emptySpots = this.board.map((cell, index) => cell === '' ? index : null).filter(val => val !== null);
65
+ if (emptySpots.length > 0) {
66
+ const randomMove = emptySpots[Math.floor(Math.random() * emptySpots.length)];
67
+ this.makeMove(randomMove, 'O');
68
+ this.addChatMessage("Technical hiccup! But I'm still playing! 🤖", 'ai');
69
+
70
+ if (!this.checkGameEnd()) {
71
+ this.gameStatus.textContent = "Your turn! Click a square.";
72
+ }
73
+ } else {
74
+ this.gameStatus.textContent = "Game error - no valid moves!";
75
+ }
76
+ }
77
+ } catch (error) {
78
+ console.error('AI move failed:', error);
79
+ // Fallback to random move instead of error
80
+ const emptySpots = this.board.map((cell, index) => cell === '' ? index : null).filter(val => val !== null);
81
+ if (emptySpots.length > 0) {
82
+ const randomMove = emptySpots[Math.floor(Math.random() * emptySpots.length)];
83
+ this.makeMove(randomMove, 'O');
84
+ this.addChatMessage("Connection issues, but I'm improvising! 😅", 'ai');
85
+
86
+ if (!this.checkGameEnd()) {
87
+ this.gameStatus.textContent = "Your turn! Click a square.";
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ async getAIMoveAndChat() {
94
+ const response = await fetch("/ai-move", {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ },
99
+ body: JSON.stringify({
100
+ board: this.board
101
+ })
102
+ });
103
+
104
+ if (!response.ok) {
105
+ throw new Error(`HTTP error! status: ${response.status}`);
106
+ }
107
+
108
+ const data = await response.json();
109
+ return data;
110
+ }
111
+
112
+ async callMistralAPI(messages) {
113
+ const response = await fetch("/mistral-proxy", {
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ model: "mistral-large-latest",
120
+ messages: messages,
121
+ temperature: 0.7,
122
+ response_format: { type: "json_object" }
123
+ })
124
+ });
125
+
126
+ if (!response.ok) {
127
+ throw new Error(`HTTP error! status: ${response.status}`);
128
+ }
129
+
130
+ const data = await response.json();
131
+ return data.choices[0].message.content;
132
+ }
133
+
134
+ boardToString() {
135
+ let result = "";
136
+ for (let i = 0; i < 9; i += 3) {
137
+ result += `${this.board[i] || ' '} | ${this.board[i+1] || ' '} | ${this.board[i+2] || ' '}\n`;
138
+ if (i < 6) result += "---------\n";
139
+ }
140
+ return result;
141
+ }
142
+
143
+ checkGameEnd() {
144
+ const winner = this.checkWinner();
145
+
146
+ if (winner) {
147
+ this.gameActive = false;
148
+ if (winner === 'X') {
149
+ this.gameStatus.textContent = "🎉 You won! Impressive!";
150
+ this.addChatMessage("Wow! You actually beat me! 😱 Well played, human! 🏆", 'ai');
151
+ } else {
152
+ this.gameStatus.textContent = "💀 Mistral AI wins!";
153
+ this.addChatMessage("Victory is mine! 😈 Better luck next time! 🤖", 'ai');
154
+ }
155
+ return true;
156
+ }
157
+
158
+ if (this.board.every(cell => cell !== '')) {
159
+ this.gameActive = false;
160
+ this.gameStatus.textContent = "🤝 It's a draw!";
161
+ this.addChatMessage("A respectable draw! You're better than I expected! 🤝", 'ai');
162
+ return true;
163
+ }
164
+
165
+ return false;
166
+ }
167
+
168
+ checkWinner() {
169
+ const winPatterns = [
170
+ [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
171
+ [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
172
+ [0, 4, 8], [2, 4, 6] // diagonals
173
+ ];
174
+
175
+ for (const pattern of winPatterns) {
176
+ const [a, b, c] = pattern;
177
+ if (this.board[a] && this.board[a] === this.board[b] && this.board[a] === this.board[c]) {
178
+ return this.board[a];
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+
184
+ addChatMessage(message, sender) {
185
+ const chatMessages = document.getElementById('chatMessages');
186
+ const messageDiv = document.createElement('div');
187
+ messageDiv.className = `message ${sender}`;
188
+
189
+ const senderDiv = document.createElement('div');
190
+ senderDiv.className = 'message-sender';
191
+ senderDiv.textContent = sender === 'user' ? 'You:' : 'Mistral AI:';
192
+
193
+ const contentDiv = document.createElement('div');
194
+ contentDiv.textContent = message;
195
+
196
+ messageDiv.appendChild(senderDiv);
197
+ messageDiv.appendChild(contentDiv);
198
+ chatMessages.appendChild(messageDiv);
199
+
200
+ // Scroll to bottom
201
+ chatMessages.scrollTop = chatMessages.scrollHeight;
202
+ }
203
+
204
+ async sendChatMessage(message) {
205
+ if (!message.trim()) return;
206
+
207
+ // Add user message
208
+ this.addChatMessage(message, 'user');
209
+
210
+ try {
211
+ const aiResponse = await this.getAIChatResponse(message);
212
+ this.addChatMessage(aiResponse, 'ai');
213
+ } catch (error) {
214
+ console.error('Chat failed:', error);
215
+ this.addChatMessage("Sorry, I'm having trouble responding right now! 🤖", 'ai');
216
+ }
217
+ }
218
+
219
+ async getAIChatResponse(userMessage) {
220
+ const response = await fetch("/ai-chat", {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ },
225
+ body: JSON.stringify({
226
+ message: userMessage,
227
+ board: this.board
228
+ })
229
+ });
230
+
231
+ if (!response.ok) {
232
+ throw new Error(`HTTP error! status: ${response.status}`);
233
+ }
234
+
235
+ const data = await response.json();
236
+ return data.message;
237
+ }
238
+ }
239
+
240
+ // Initialize game when page loads
241
+ let game;
242
+ document.addEventListener('DOMContentLoaded', () => {
243
+ game = new TicTacToeGame();
244
+ });
245
+
246
+ // Chat functions
247
+ function sendChatMessage() {
248
+ const chatInput = document.getElementById('chatInput');
249
+ const message = chatInput.value.trim();
250
+
251
+ if (message) {
252
+ game.sendChatMessage(message);
253
+ chatInput.value = '';
254
+ }
255
+ }
256
+
257
+ function handleEnter(event) {
258
+ if (event.key === 'Enter') {
259
+ sendChatMessage();
260
+ }
261
+ }
262
+
263
+ // Reset game
264
+ function resetGame() {
265
+ game = new TicTacToeGame();
266
+
267
+ // Clear board visually
268
+ game.cells.forEach(cell => {
269
+ cell.textContent = '';
270
+ cell.classList.remove('x', 'o');
271
+ });
272
+
273
+ game.gameStatus.textContent = "Your turn! Click a square to play X";
274
+ game.addChatMessage("New game! Let's see if you can do better this time! 😏", 'ai');
275
+ }
mcp_server.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from dotenv import load_dotenv
4
+ from mcp.server.fastmcp import FastMCP
5
+ from app import Room, rooms, get_ai_move_for_room, get_ai_chat_for_room
6
+
7
+ load_dotenv()
8
+
9
+ # --- MCP Server Setup ---
10
+ mcp = FastMCP(
11
+ name="TicTacToeRooms",
12
+ host="0.0.0.0",
13
+ port=7860,
14
+ )
15
+
16
+ # --- Global state for current user session ---
17
+ current_session = {
18
+ 'active_room_id': None,
19
+ 'username': 'MCPPlayer'
20
+ }
21
+
22
+ # --- MCP Tools ---
23
+
24
+ @mcp.tool()
25
+ def create_room() -> dict:
26
+ """
27
+ Create a new tic-tac-toe game room.
28
+ Returns:
29
+ dict: Room information including room ID and initial markdown state
30
+ """
31
+ global current_session
32
+ try:
33
+ room = Room()
34
+ rooms[room.id] = room
35
+
36
+ current_session['active_room_id'] = room.id
37
+
38
+ return {
39
+ "status": "success",
40
+ "room_id": room.id,
41
+ "message": f"Created new tic-tac-toe room: {room.id}",
42
+ "markdown_state": room.to_markdown(),
43
+ "instructions": "Use make_move() to play or send_chat() to talk with Mistral AI",
44
+ "game_info": {
45
+ "your_symbol": "X",
46
+ "ai_symbol": "O",
47
+ "board_positions": "0-8 (left to right, top to bottom)"
48
+ }
49
+ }
50
+ except Exception as e:
51
+ return {
52
+ "status": "error",
53
+ "message": f"Failed to create room: {str(e)}"
54
+ }
55
+
56
+ @mcp.tool()
57
+ def get_room_state(room_id: str = None) -> dict:
58
+ """
59
+ Get the current state of a tic-tac-toe room in markdown format.
60
+ Args:
61
+ room_id (str, optional): Room ID to check (uses current active room if not provided)
62
+ Returns:
63
+ dict: Current room state with markdown representation
64
+ """
65
+ global current_session
66
+ try:
67
+ # Use provided room_id or current active room
68
+ target_room_id = room_id or current_session.get('active_room_id')
69
+
70
+ if not target_room_id:
71
+ return {
72
+ "status": "error",
73
+ "message": "No active room. Create a room first using create_room()."
74
+ }
75
+
76
+ if target_room_id not in rooms:
77
+ return {
78
+ "status": "error",
79
+ "message": f"Room {target_room_id} not found. It may have been cleaned up."
80
+ }
81
+
82
+ room = rooms[target_room_id]
83
+
84
+ return {
85
+ "status": "success",
86
+ "room_id": target_room_id,
87
+ "markdown_state": room.to_markdown(),
88
+ "game_status": room.game_status,
89
+ "current_player": room.current_player,
90
+ "moves_made": room.moves_count,
91
+ "your_turn": room.current_player == 'X' and room.game_status == 'active'
92
+ }
93
+ except Exception as e:
94
+ return {
95
+ "status": "error",
96
+ "message": f"Failed to get room state: {str(e)}"
97
+ }
98
+
99
+ @mcp.tool()
100
+ async def make_move(position: int, room_id: str = None) -> dict:
101
+ """
102
+ Make a move in a tic-tac-toe game. This will also trigger the AI's response move.
103
+ Args:
104
+ position (int): Board position (0-8, left to right, top to bottom)
105
+ room_id (str, optional): Room ID (uses current active room if not provided)
106
+ Returns:
107
+ dict: Result of your move and the AI's response with updated game state
108
+ """
109
+ global current_session
110
+ try:
111
+ # Use provided room_id or current active room
112
+ target_room_id = room_id or current_session.get('active_room_id')
113
+
114
+ if not target_room_id:
115
+ return {
116
+ "status": "error",
117
+ "message": "No active room. Create a room first using create_room()."
118
+ }
119
+
120
+ if target_room_id not in rooms:
121
+ return {
122
+ "status": "error",
123
+ "message": f"Room {target_room_id} not found."
124
+ }
125
+
126
+ room = rooms[target_room_id]
127
+
128
+ # Validate move
129
+ if position < 0 or position > 8:
130
+ return {
131
+ "status": "error",
132
+ "message": "Invalid position. Use 0-8 (left to right, top to bottom)."
133
+ }
134
+
135
+ if room.game_status != 'active':
136
+ return {
137
+ "status": "error",
138
+ "message": f"Game is over. Status: {room.game_status}",
139
+ "markdown_state": room.to_markdown()
140
+ }
141
+
142
+ if room.current_player != 'X':
143
+ return {
144
+ "status": "error",
145
+ "message": "It's not your turn! Wait for AI to move.",
146
+ "markdown_state": room.to_markdown()
147
+ }
148
+
149
+ # Make human move
150
+ if not room.make_move(position, 'X'):
151
+ return {
152
+ "status": "error",
153
+ "message": f"Invalid move! Position {position} may already be occupied.",
154
+ "markdown_state": room.to_markdown()
155
+ }
156
+
157
+ result_message = f"✅ You played X at position {position}\n\n"
158
+
159
+ # Check if game ended after human move
160
+ if room.game_status != 'active':
161
+ if room.winner == 'X':
162
+ result_message += "🎉 Congratulations! You won!\n\n"
163
+ else:
164
+ result_message += "🤝 It's a draw!\n\n"
165
+
166
+ result_message += room.to_markdown()
167
+ return {
168
+ "status": "success",
169
+ "message": result_message,
170
+ "game_over": True,
171
+ "winner": room.winner
172
+ }
173
+
174
+ # Get AI move
175
+ try:
176
+ ai_response = get_ai_move_for_room(room)
177
+ if ai_response and 'move' in ai_response:
178
+ room.make_move(ai_response['move'], 'O')
179
+ if 'message' in ai_response:
180
+ room.add_chat_message(ai_response['message'], 'ai')
181
+
182
+ result_message += f"🤖 Mistral AI played O at position {ai_response['move']}\n"
183
+ if 'message' in ai_response:
184
+ result_message += f"💬 Mistral says: \"{ai_response['message']}\"\n\n"
185
+ else:
186
+ result_message += "\n"
187
+
188
+ # Check if AI won
189
+ if room.game_status == 'won' and room.winner == 'O':
190
+ result_message += "💀 Mistral AI wins this round!\n\n"
191
+ elif room.game_status == 'draw':
192
+ result_message += "🤝 It's a draw!\n\n"
193
+ else:
194
+ result_message += "⚠️ AI move failed, but you can continue\n\n"
195
+
196
+ except Exception as e:
197
+ result_message += f"⚠️ AI move error: {str(e)}\n\n"
198
+
199
+ result_message += room.to_markdown()
200
+
201
+ return {
202
+ "status": "success",
203
+ "message": result_message,
204
+ "game_over": room.game_status != 'active',
205
+ "winner": room.winner if room.game_status == 'won' else None,
206
+ "your_turn": room.current_player == 'X' and room.game_status == 'active'
207
+ }
208
+
209
+ except Exception as e:
210
+ return {
211
+ "status": "error",
212
+ "message": f"Failed to make move: {str(e)}"
213
+ }
214
+
215
+ @mcp.tool()
216
+ async def send_chat(message: str, room_id: str = None) -> dict:
217
+ """
218
+ Send a chat message to Mistral AI in the current game room.
219
+ Args:
220
+ message (str): Your message to send to the AI
221
+ room_id (str, optional): Room ID (uses current active room if not provided)
222
+ Returns:
223
+ dict: Your message and the AI's response with updated room state
224
+ """
225
+ global current_session
226
+ try:
227
+ # Use provided room_id or current active room
228
+ target_room_id = room_id or current_session.get('active_room_id')
229
+
230
+ if not target_room_id:
231
+ return {
232
+ "status": "error",
233
+ "message": "No active room. Create a room first using create_room()."
234
+ }
235
+
236
+ if target_room_id not in rooms:
237
+ return {
238
+ "status": "error",
239
+ "message": f"Room {target_room_id} not found."
240
+ }
241
+
242
+ room = rooms[target_room_id]
243
+
244
+ # Add user message
245
+ room.add_chat_message(message, 'user')
246
+
247
+ # Get AI response
248
+ ai_response = get_ai_chat_for_room(room, message)
249
+ room.add_chat_message(ai_response, 'ai')
250
+
251
+ result_message = f"💬 **You:** {message}\n💬 **Mistral AI:** {ai_response}\n\n"
252
+ result_message += room.to_markdown()
253
+
254
+ return {
255
+ "status": "success",
256
+ "message": result_message,
257
+ "your_message": message,
258
+ "ai_response": ai_response
259
+ }
260
+
261
+ except Exception as e:
262
+ return {
263
+ "status": "error",
264
+ "message": f"Failed to send chat: {str(e)}"
265
+ }
266
+
267
+ @mcp.tool()
268
+ def list_rooms() -> dict:
269
+ """
270
+ List all active tic-tac-toe game rooms.
271
+ Returns:
272
+ dict: List of active rooms with their status
273
+ """
274
+ try:
275
+ if not rooms:
276
+ return {
277
+ "status": "success",
278
+ "message": "No active rooms. Use create_room() to start a new game!",
279
+ "active_rooms": [],
280
+ "count": 0
281
+ }
282
+
283
+ room_list = []
284
+ for room_id, room in rooms.items():
285
+ room_info = {
286
+ "room_id": room_id,
287
+ "game_status": room.game_status,
288
+ "current_player": room.current_player,
289
+ "moves_count": room.moves_count,
290
+ "winner": room.winner,
291
+ "is_your_turn": room.current_player == 'X' and room.game_status == 'active',
292
+ "is_active": current_session.get('active_room_id') == room_id
293
+ }
294
+ room_list.append(room_info)
295
+
296
+ active_room_id = current_session.get('active_room_id')
297
+ message = f"Found {len(room_list)} active rooms."
298
+ if active_room_id:
299
+ message += f" Current active room: {active_room_id}"
300
+
301
+ return {
302
+ "status": "success",
303
+ "message": message,
304
+ "active_rooms": room_list,
305
+ "count": len(room_list),
306
+ "current_active_room": active_room_id
307
+ }
308
+ except Exception as e:
309
+ return {
310
+ "status": "error",
311
+ "message": f"Failed to list rooms: {str(e)}"
312
+ }
313
+
314
+ @mcp.tool()
315
+ def switch_room(room_id: str) -> dict:
316
+ """
317
+ Switch to a different active room.
318
+ Args:
319
+ room_id (str): Room ID to switch to
320
+ Returns:
321
+ dict: Confirmation of room switch with current state
322
+ """
323
+ global current_session
324
+ try:
325
+ if room_id not in rooms:
326
+ return {
327
+ "status": "error",
328
+ "message": f"Room {room_id} not found. Use list_rooms() to see available rooms."
329
+ }
330
+
331
+ current_session['active_room_id'] = room_id
332
+ room = rooms[room_id]
333
+
334
+ return {
335
+ "status": "success",
336
+ "message": f"Switched to room {room_id}",
337
+ "room_id": room_id,
338
+ "markdown_state": room.to_markdown(),
339
+ "your_turn": room.current_player == 'X' and room.game_status == 'active'
340
+ }
341
+ except Exception as e:
342
+ return {
343
+ "status": "error",
344
+ "message": f"Failed to switch room: {str(e)}"
345
+ }
346
+
347
+ @mcp.tool()
348
+ def get_help() -> dict:
349
+ """
350
+ Get help information about playing tic-tac-toe.
351
+ Returns:
352
+ dict: Instructions and tips for playing the game
353
+ """
354
+ return {
355
+ "status": "success",
356
+ "message": "Tic-Tac-Toe Game Help",
357
+ "instructions": {
358
+ "how_to_play": [
359
+ "1. Create a new game room with create_room()",
360
+ "2. Make moves using make_move(position) where position is 0-8",
361
+ "3. Chat with Mistral AI using send_chat('your message')",
362
+ "4. Check game state anytime with get_room_state()"
363
+ ],
364
+ "board_layout": {
365
+ "description": "Board positions (0-8):",
366
+ "layout": [
367
+ "0 | 1 | 2",
368
+ "---------",
369
+ "3 | 4 | 5",
370
+ "---------",
371
+ "6 | 7 | 8"
372
+ ]
373
+ },
374
+ "symbols": {
375
+ "you": "X (you go first)",
376
+ "ai": "O (Mistral AI)"
377
+ },
378
+ "tips": [
379
+ "The AI has personality and will trash talk!",
380
+ "You can have multiple rooms active at once",
381
+ "Use list_rooms() to see all your games",
382
+ "Use switch_room(room_id) to change between games"
383
+ ]
384
+ },
385
+ "available_commands": [
386
+ "create_room() - Start a new game",
387
+ "make_move(position) - Make your move (0-8)",
388
+ "send_chat('message') - Chat with AI",
389
+ "get_room_state() - Check current game",
390
+ "list_rooms() - See all active games",
391
+ "switch_room(room_id) - Change active room",
392
+ "get_help() - Show this help"
393
+ ]
394
+ }
395
+
396
+ @mcp.tool()
397
+ async def wait_5_seconds() -> dict:
398
+ """
399
+ Wait for 5 seconds. Useful for giving the AI time to respond or timing operations.
400
+ Returns:
401
+ dict: Status of the wait operation
402
+ """
403
+ try:
404
+ await asyncio.sleep(5)
405
+ return {
406
+ "status": "success",
407
+ "message": "Waited 5 seconds successfully"
408
+ }
409
+ except Exception as e:
410
+ return {
411
+ "status": "error",
412
+ "message": f"Failed to wait: {str(e)}"
413
+ }
414
+
415
+ # --- Server Execution ---
416
+ if __name__ == "__main__":
417
+ print(f"Tic-Tac-Toe Rooms MCP Server starting on port 7860...")
418
+ print("Available game features:")
419
+ print("- Create multiple game rooms")
420
+ print("- Play against Mistral AI with personality")
421
+ print("- Real-time chat with the AI")
422
+ print("- Markdown state representation")
423
+ print("- Room management and switching")
424
+ print()
425
+ print("MCP Tools available:")
426
+ print("- create_room()")
427
+ print("- make_move(position)")
428
+ print("- send_chat(message)")
429
+ print("- get_room_state()")
430
+ print("- list_rooms()")
431
+ print("- get_help()")
432
+ print()
433
+ print("This MCP server is ready for LeChat integration!")
434
+ print("Running Tic-Tac-Toe MCP server with SSE transport")
435
+ mcp.run(transport="sse")
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ flask-cors==4.0.0
3
+ mistralai
4
+ python-dotenv
5
+ mcp
room_game.html ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Room-Based Tic-Tac-Toe vs Mistral AI</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Courier New', monospace;
10
+ background: #1a1a1a;
11
+ color: #00ff00;
12
+ margin: 0;
13
+ padding: 20px;
14
+ display: flex;
15
+ height: 100vh;
16
+ gap: 20px;
17
+ }
18
+
19
+ .game-container {
20
+ display: flex;
21
+ gap: 20px;
22
+ width: 100%;
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .room-panel {
28
+ flex: 0 0 250px;
29
+ background: #222;
30
+ border: 1px solid #444;
31
+ border-radius: 5px;
32
+ padding: 15px;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 15px;
36
+ }
37
+
38
+ .room-header {
39
+ color: #00ccff;
40
+ font-weight: bold;
41
+ text-align: center;
42
+ margin-bottom: 10px;
43
+ }
44
+
45
+ .room-info {
46
+ background: #333;
47
+ padding: 10px;
48
+ border-radius: 3px;
49
+ font-size: 12px;
50
+ }
51
+
52
+ .room-controls button {
53
+ width: 100%;
54
+ background: #4ecdc4;
55
+ color: white;
56
+ border: none;
57
+ padding: 10px;
58
+ margin-bottom: 10px;
59
+ cursor: pointer;
60
+ font-family: 'Courier New', monospace;
61
+ border-radius: 3px;
62
+ }
63
+
64
+ .room-controls button:hover {
65
+ background: #45a7a0;
66
+ }
67
+
68
+ .room-id-input {
69
+ width: 100%;
70
+ background: #333;
71
+ border: 1px solid #555;
72
+ color: #00ff00;
73
+ padding: 8px;
74
+ font-family: 'Courier New', monospace;
75
+ margin-bottom: 10px;
76
+ }
77
+
78
+ .markdown-panel {
79
+ flex: 0 0 300px;
80
+ background: #222;
81
+ border: 1px solid #444;
82
+ border-radius: 5px;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .markdown-header {
87
+ background: #333;
88
+ padding: 10px;
89
+ text-align: center;
90
+ color: #00ccff;
91
+ font-weight: bold;
92
+ }
93
+
94
+ .markdown-content {
95
+ padding: 15px;
96
+ font-size: 12px;
97
+ line-height: 1.4;
98
+ max-height: 600px;
99
+ overflow-y: auto;
100
+ white-space: pre-wrap;
101
+ }
102
+
103
+ .game-board {
104
+ flex: 1;
105
+ display: flex;
106
+ flex-direction: column;
107
+ align-items: center;
108
+ }
109
+
110
+ .title {
111
+ font-size: 24px;
112
+ margin-bottom: 20px;
113
+ text-align: center;
114
+ color: #00ccff;
115
+ }
116
+
117
+ .board {
118
+ display: grid;
119
+ grid-template-columns: repeat(3, 100px);
120
+ grid-template-rows: repeat(3, 100px);
121
+ gap: 2px;
122
+ background: #333;
123
+ padding: 2px;
124
+ margin-bottom: 20px;
125
+ }
126
+
127
+ .cell {
128
+ background: #222;
129
+ border: 1px solid #555;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ font-size: 36px;
134
+ font-weight: bold;
135
+ cursor: pointer;
136
+ transition: background 0.2s;
137
+ }
138
+
139
+ .cell:hover {
140
+ background: #333;
141
+ }
142
+
143
+ .cell.x {
144
+ color: #ff6b6b;
145
+ }
146
+
147
+ .cell.o {
148
+ color: #4ecdc4;
149
+ }
150
+
151
+ .status {
152
+ font-size: 18px;
153
+ margin-bottom: 20px;
154
+ text-align: center;
155
+ color: #ffff99;
156
+ }
157
+
158
+ .reset-btn {
159
+ background: #ff6b6b;
160
+ color: white;
161
+ border: none;
162
+ padding: 10px 20px;
163
+ font-size: 16px;
164
+ cursor: pointer;
165
+ font-family: 'Courier New', monospace;
166
+ margin-bottom: 20px;
167
+ }
168
+
169
+ .reset-btn:hover {
170
+ background: #ff5252;
171
+ }
172
+
173
+ .chat-container {
174
+ flex: 1;
175
+ display: flex;
176
+ flex-direction: column;
177
+ background: #222;
178
+ border: 1px solid #444;
179
+ border-radius: 5px;
180
+ overflow: hidden;
181
+ }
182
+
183
+ .chat-header {
184
+ background: #333;
185
+ padding: 10px;
186
+ text-align: center;
187
+ color: #00ccff;
188
+ font-weight: bold;
189
+ }
190
+
191
+ .chat-messages {
192
+ flex: 1;
193
+ overflow-y: auto;
194
+ padding: 10px;
195
+ max-height: 400px;
196
+ }
197
+
198
+ .message {
199
+ margin-bottom: 10px;
200
+ padding: 8px;
201
+ border-radius: 5px;
202
+ }
203
+
204
+ .message.user {
205
+ background: #1a4a1a;
206
+ align-self: flex-end;
207
+ text-align: right;
208
+ }
209
+
210
+ .message.ai {
211
+ background: #1a1a4a;
212
+ align-self: flex-start;
213
+ }
214
+
215
+ .message-sender {
216
+ font-weight: bold;
217
+ font-size: 12px;
218
+ margin-bottom: 4px;
219
+ }
220
+
221
+ .user .message-sender {
222
+ color: #ff6b6b;
223
+ }
224
+
225
+ .ai .message-sender {
226
+ color: #4ecdc4;
227
+ }
228
+
229
+ .chat-input {
230
+ display: flex;
231
+ padding: 10px;
232
+ background: #333;
233
+ }
234
+
235
+ .chat-input input {
236
+ flex: 1;
237
+ background: #222;
238
+ border: 1px solid #555;
239
+ color: #00ff00;
240
+ padding: 8px;
241
+ font-family: 'Courier New', monospace;
242
+ }
243
+
244
+ .chat-input button {
245
+ background: #4ecdc4;
246
+ color: white;
247
+ border: none;
248
+ padding: 8px 15px;
249
+ cursor: pointer;
250
+ font-family: 'Courier New', monospace;
251
+ }
252
+
253
+ .loading {
254
+ opacity: 0.6;
255
+ }
256
+
257
+ .no-room {
258
+ text-align: center;
259
+ color: #888;
260
+ font-style: italic;
261
+ margin-top: 50px;
262
+ }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <div class="game-container">
267
+ <!-- Room Management Panel -->
268
+ <div class="room-panel">
269
+ <div class="room-header">🏠 Room Management</div>
270
+
271
+ <div class="room-controls">
272
+ <button onclick="createNewRoom()">Create New Room</button>
273
+ <input type="text" class="room-id-input" id="roomIdInput" placeholder="Enter Room ID">
274
+ <button onclick="joinRoom()">Join Room</button>
275
+ <button onclick="leaveRoom()">Leave Room</button>
276
+ </div>
277
+
278
+ <div class="room-info" id="roomInfo">
279
+ <div>Status: No room selected</div>
280
+ <div>Room ID: -</div>
281
+ <div>Game Status: -</div>
282
+ <div>Your Turn: -</div>
283
+ </div>
284
+ </div>
285
+
286
+ <!-- Markdown Display Panel -->
287
+ <div class="markdown-panel">
288
+ <div class="markdown-header">📝 Game State (Markdown)</div>
289
+ <div class="markdown-content" id="markdownContent">
290
+ Select or create a room to see the markdown representation...
291
+ </div>
292
+ </div>
293
+
294
+ <!-- Game Board -->
295
+ <div class="game-board">
296
+ <h1 class="title">🎮 Room-Based Tic-Tac-Toe</h1>
297
+
298
+ <div class="status" id="gameStatus">Create or join a room to start playing!</div>
299
+
300
+ <div id="gameArea" style="display: none;">
301
+ <div class="board" id="gameBoard">
302
+ <div class="cell" data-index="0"></div>
303
+ <div class="cell" data-index="1"></div>
304
+ <div class="cell" data-index="2"></div>
305
+ <div class="cell" data-index="3"></div>
306
+ <div class="cell" data-index="4"></div>
307
+ <div class="cell" data-index="5"></div>
308
+ <div class="cell" data-index="6"></div>
309
+ <div class="cell" data-index="7"></div>
310
+ <div class="cell" data-index="8"></div>
311
+ </div>
312
+
313
+ <button class="reset-btn" onclick="resetGame()">New Game (Same Room)</button>
314
+ </div>
315
+
316
+ <div class="no-room" id="noRoom">
317
+ 👆 Create a new room or join an existing one to start playing!
318
+ </div>
319
+ </div>
320
+
321
+ <!-- Chat Panel -->
322
+ <div class="chat-container">
323
+ <div class="chat-header">💬 Chat with Mistral AI</div>
324
+
325
+ <div class="chat-messages" id="chatMessages">
326
+ <div class="message ai">
327
+ <div class="message-sender">System:</div>
328
+ <div>Create or join a room to start chatting with Mistral AI!</div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="chat-input">
333
+ <input type="text" id="chatInput" placeholder="Join a room first..." onkeypress="handleEnter(event)" disabled>
334
+ <button onclick="sendChatMessage()" disabled id="sendBtn">Send</button>
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ <script src="room_game.js"></script>
340
+ </body>
341
+ </html>
room_game.js ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class RoomTicTacToeGame {
2
+ constructor() {
3
+ this.currentRoomId = null;
4
+ this.roomData = null;
5
+ this.cells = document.querySelectorAll('.cell');
6
+ this.gameStatus = document.getElementById('gameStatus');
7
+ this.roomInfo = document.getElementById('roomInfo');
8
+ this.markdownContent = document.getElementById('markdownContent');
9
+ this.chatMessages = document.getElementById('chatMessages');
10
+ this.chatInput = document.getElementById('chatInput');
11
+ this.sendBtn = document.getElementById('sendBtn');
12
+ this.gameArea = document.getElementById('gameArea');
13
+ this.noRoom = document.getElementById('noRoom');
14
+
15
+ this.initGame();
16
+ }
17
+
18
+ initGame() {
19
+ this.cells.forEach((cell, index) => {
20
+ cell.addEventListener('click', () => this.handleCellClick(index));
21
+ });
22
+
23
+ // Update room state every 2 seconds if in a room
24
+ setInterval(() => {
25
+ if (this.currentRoomId) {
26
+ this.refreshRoomState();
27
+ }
28
+ }, 2000);
29
+ }
30
+
31
+ async createNewRoom() {
32
+ try {
33
+ const response = await fetch('/rooms', {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ });
39
+
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP error! status: ${response.status}`);
42
+ }
43
+
44
+ const data = await response.json();
45
+ this.joinRoomById(data.room_id);
46
+
47
+ } catch (error) {
48
+ console.error('Failed to create room:', error);
49
+ this.gameStatus.textContent = "Failed to create room. Try again.";
50
+ }
51
+ }
52
+
53
+ async joinRoom() {
54
+ const roomId = document.getElementById('roomIdInput').value.trim();
55
+ if (!roomId) {
56
+ this.gameStatus.textContent = "Please enter a room ID";
57
+ return;
58
+ }
59
+
60
+ await this.joinRoomById(roomId);
61
+ }
62
+
63
+ async joinRoomById(roomId) {
64
+ try {
65
+ const response = await fetch(`/rooms/${roomId}`);
66
+
67
+ if (!response.ok) {
68
+ throw new Error(`HTTP error! status: ${response.status}`);
69
+ }
70
+
71
+ const data = await response.json();
72
+ this.currentRoomId = roomId;
73
+ this.roomData = data.room_data;
74
+
75
+ // Clear the input
76
+ document.getElementById('roomIdInput').value = '';
77
+
78
+ // Show game area and enable chat
79
+ this.gameArea.style.display = 'block';
80
+ this.noRoom.style.display = 'none';
81
+ this.chatInput.disabled = false;
82
+ this.sendBtn.disabled = false;
83
+ this.chatInput.placeholder = "Type a message...";
84
+
85
+ // Update display
86
+ this.updateDisplay();
87
+ this.updateMarkdown(data.markdown);
88
+ this.loadChatHistory();
89
+
90
+ this.gameStatus.textContent = `Joined room ${roomId}!`;
91
+
92
+ } catch (error) {
93
+ console.error('Failed to join room:', error);
94
+ this.gameStatus.textContent = `Failed to join room ${roomId}. Check the room ID.`;
95
+ }
96
+ }
97
+
98
+ leaveRoom() {
99
+ this.currentRoomId = null;
100
+ this.roomData = null;
101
+
102
+ // Hide game area and disable chat
103
+ this.gameArea.style.display = 'none';
104
+ this.noRoom.style.display = 'block';
105
+ this.chatInput.disabled = true;
106
+ this.sendBtn.disabled = true;
107
+ this.chatInput.placeholder = "Join a room first...";
108
+
109
+ // Clear display
110
+ this.clearBoard();
111
+ this.gameStatus.textContent = "Create or join a room to start playing!";
112
+ this.updateRoomInfo();
113
+ this.markdownContent.textContent = "Select or create a room to see the markdown representation...";
114
+
115
+ // Clear chat
116
+ this.chatMessages.innerHTML = `
117
+ <div class="message ai">
118
+ <div class="message-sender">System:</div>
119
+ <div>Create or join a room to start chatting with Mistral AI!</div>
120
+ </div>
121
+ `;
122
+ }
123
+
124
+ async refreshRoomState() {
125
+ if (!this.currentRoomId) return;
126
+
127
+ try {
128
+ const response = await fetch(`/rooms/${this.currentRoomId}`);
129
+
130
+ if (!response.ok) {
131
+ if (response.status === 404) {
132
+ this.gameStatus.textContent = "Room no longer exists!";
133
+ this.leaveRoom();
134
+ return;
135
+ }
136
+ throw new Error(`HTTP error! status: ${response.status}`);
137
+ }
138
+
139
+ const data = await response.json();
140
+ this.roomData = data.room_data;
141
+ this.updateDisplay();
142
+ this.updateMarkdown(data.markdown);
143
+
144
+ } catch (error) {
145
+ console.error('Failed to refresh room:', error);
146
+ }
147
+ }
148
+
149
+ async handleCellClick(index) {
150
+ if (!this.currentRoomId || !this.roomData) {
151
+ this.gameStatus.textContent = "Join a room first!";
152
+ return;
153
+ }
154
+
155
+ if (this.roomData.game_status !== 'active' ||
156
+ this.roomData.board[index] !== '' ||
157
+ this.roomData.current_player !== 'X') {
158
+ return;
159
+ }
160
+
161
+ this.gameStatus.textContent = "Making your move...";
162
+
163
+ try {
164
+ const response = await fetch(`/rooms/${this.currentRoomId}/move`, {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Content-Type': 'application/json'
168
+ },
169
+ body: JSON.stringify({
170
+ position: index
171
+ })
172
+ });
173
+
174
+ if (!response.ok) {
175
+ throw new Error(`HTTP error! status: ${response.status}`);
176
+ }
177
+
178
+ const data = await response.json();
179
+ this.roomData = data.room_data;
180
+ this.updateDisplay();
181
+ this.updateMarkdown(data.markdown);
182
+ this.loadChatHistory(); // Reload chat to get AI's move message
183
+
184
+ if (this.roomData.game_status === 'active') {
185
+ this.gameStatus.textContent = "Mistral is thinking...";
186
+ setTimeout(() => {
187
+ if (this.roomData.current_player === 'X') {
188
+ this.gameStatus.textContent = "Your turn! Click a square.";
189
+ }
190
+ }, 1000);
191
+ }
192
+
193
+ } catch (error) {
194
+ console.error('Move failed:', error);
195
+ this.gameStatus.textContent = "Move failed. Try again.";
196
+ }
197
+ }
198
+
199
+ async sendChatMessage() {
200
+ if (!this.currentRoomId) {
201
+ return;
202
+ }
203
+
204
+ const message = this.chatInput.value.trim();
205
+ if (!message) return;
206
+
207
+ this.chatInput.value = '';
208
+
209
+ try {
210
+ const response = await fetch(`/rooms/${this.currentRoomId}/chat`, {
211
+ method: 'POST',
212
+ headers: {
213
+ 'Content-Type': 'application/json'
214
+ },
215
+ body: JSON.stringify({
216
+ message: message
217
+ })
218
+ });
219
+
220
+ if (!response.ok) {
221
+ throw new Error(`HTTP error! status: ${response.status}`);
222
+ }
223
+
224
+ const data = await response.json();
225
+ this.roomData = data.room_data;
226
+ this.updateMarkdown(data.markdown);
227
+ this.loadChatHistory();
228
+
229
+ } catch (error) {
230
+ console.error('Chat failed:', error);
231
+ this.addChatMessage("Failed to send message", 'system');
232
+ }
233
+ }
234
+
235
+ updateDisplay() {
236
+ if (!this.roomData) return;
237
+
238
+ // Update board
239
+ this.roomData.board.forEach((cell, index) => {
240
+ this.cells[index].textContent = cell;
241
+ this.cells[index].className = 'cell';
242
+ if (cell) {
243
+ this.cells[index].classList.add(cell.toLowerCase());
244
+ }
245
+ });
246
+
247
+ // Update game status
248
+ if (this.roomData.game_status === 'won') {
249
+ const winner = this.roomData.winner === 'X' ? 'You' : 'Mistral AI';
250
+ this.gameStatus.textContent = `🎉 ${winner} won!`;
251
+ } else if (this.roomData.game_status === 'draw') {
252
+ this.gameStatus.textContent = "🤝 It's a draw!";
253
+ } else if (this.roomData.current_player === 'X') {
254
+ this.gameStatus.textContent = "Your turn! Click a square.";
255
+ } else {
256
+ this.gameStatus.textContent = "Mistral's turn...";
257
+ }
258
+
259
+ this.updateRoomInfo();
260
+ }
261
+
262
+ updateRoomInfo() {
263
+ if (!this.roomData || !this.currentRoomId) {
264
+ this.roomInfo.innerHTML = `
265
+ <div>Status: No room selected</div>
266
+ <div>Room ID: -</div>
267
+ <div>Game Status: -</div>
268
+ <div>Your Turn: -</div>
269
+ `;
270
+ return;
271
+ }
272
+
273
+ const isYourTurn = this.roomData.current_player === 'X' && this.roomData.game_status === 'active';
274
+ this.roomInfo.innerHTML = `
275
+ <div>Status: Connected</div>
276
+ <div>Room ID: ${this.currentRoomId}</div>
277
+ <div>Game Status: ${this.roomData.game_status}</div>
278
+ <div>Your Turn: ${isYourTurn ? 'Yes' : 'No'}</div>
279
+ <div>Moves: ${this.roomData.moves_count}/9</div>
280
+ `;
281
+ }
282
+
283
+ updateMarkdown(markdown) {
284
+ if (markdown) {
285
+ this.markdownContent.textContent = markdown;
286
+ }
287
+ }
288
+
289
+ loadChatHistory() {
290
+ if (!this.roomData || !this.roomData.chat_history) return;
291
+
292
+ this.chatMessages.innerHTML = '';
293
+
294
+ this.roomData.chat_history.forEach(msg => {
295
+ this.addChatMessage(msg.message, msg.sender);
296
+ });
297
+ }
298
+
299
+ addChatMessage(message, sender) {
300
+ const messageDiv = document.createElement('div');
301
+ messageDiv.className = `message ${sender}`;
302
+
303
+ const senderDiv = document.createElement('div');
304
+ senderDiv.className = 'message-sender';
305
+
306
+ let senderName;
307
+ if (sender === 'user') senderName = 'You:';
308
+ else if (sender === 'ai') senderName = 'Mistral AI:';
309
+ else senderName = 'System:';
310
+
311
+ senderDiv.textContent = senderName;
312
+
313
+ const contentDiv = document.createElement('div');
314
+ contentDiv.textContent = message;
315
+
316
+ messageDiv.appendChild(senderDiv);
317
+ messageDiv.appendChild(contentDiv);
318
+ this.chatMessages.appendChild(messageDiv);
319
+
320
+ // Scroll to bottom
321
+ this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
322
+ }
323
+
324
+ clearBoard() {
325
+ this.cells.forEach(cell => {
326
+ cell.textContent = '';
327
+ cell.className = 'cell';
328
+ });
329
+ }
330
+
331
+ async resetGame() {
332
+ if (!this.currentRoomId) return;
333
+
334
+ // Create a new room instead of resetting current one
335
+ await this.createNewRoom();
336
+ }
337
+ }
338
+
339
+ // Global functions for HTML onclick events
340
+ let game;
341
+
342
+ function createNewRoom() {
343
+ game.createNewRoom();
344
+ }
345
+
346
+ function joinRoom() {
347
+ game.joinRoom();
348
+ }
349
+
350
+ function leaveRoom() {
351
+ game.leaveRoom();
352
+ }
353
+
354
+ function sendChatMessage() {
355
+ game.sendChatMessage();
356
+ }
357
+
358
+ function handleEnter(event) {
359
+ if (event.key === 'Enter') {
360
+ sendChatMessage();
361
+ }
362
+ }
363
+
364
+ function resetGame() {
365
+ game.resetGame();
366
+ }
367
+
368
+ // Initialize game when page loads
369
+ document.addEventListener('DOMContentLoaded', () => {
370
+ game = new RoomTicTacToeGame();
371
+ });