ravithejads commited on
Commit
faf907f
·
verified ·
1 Parent(s): ba7c76d

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +28 -0
  2. README.md +39 -5
  3. app.py +351 -0
  4. requirements.txt +4 -0
  5. room_game.html +341 -0
  6. room_game.js +371 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
19
+ RUN echo '#!/bin/bash\n\
20
+ echo "Starting Tic-Tac-Toe Web UI on port 7860..."\n\
21
+ echo "Game interface available at the root URL"\n\
22
+ echo "Features: Room management, AI gameplay, real-time chat"\n\
23
+ python app.py' > start.sh && chmod +x start.sh
24
+
25
+ # Set required environment variable for Mistral API
26
+ ENV MISTRAL_API_KEY=""
27
+
28
+ CMD ["./start.sh"]
README.md CHANGED
@@ -1,10 +1,44 @@
1
  ---
2
- title: Tictoctoe Ui
3
- emoji: 📈
4
- colorFrom: indigo
5
- colorTo: pink
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 Game UI
3
+ emoji: 🎮
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # 🎮 Tic-Tac-Toe Game UI
11
+
12
+ Visual interface for the room-based tic-tac-toe game. Play against Mistral AI with real-time chat and markdown game state visualization.
13
+
14
+ ## 🌐 **Live Game Interface**
15
+
16
+ - **Create/Join Rooms**: Multiple isolated game sessions
17
+ - **Play vs Mistral AI**: Competitive AI with personality
18
+ - **Real-time Chat**: Interactive conversations
19
+ - **Live Markdown View**: See game state as MCP tools see it
20
+ - **Room Management**: Switch between multiple games
21
+
22
+ ## 🔗 **Companion to MCP Server**
23
+
24
+ This UI works alongside the MCP server for LeChat integration:
25
+ - **MCP Server**: [Tic-Tac-Toe MCP Server](https://ravithejads-tictactoe.hf.space)
26
+ - **Web UI**: This Space (visual gameplay)
27
+
28
+ ## 🎯 **How to Play**
29
+
30
+ 1. **Create Room**: Click "Create New Room"
31
+ 2. **Play**: Click squares to make moves (you are X)
32
+ 3. **Chat**: Send messages to Mistral AI
33
+ 4. **Monitor**: Watch the markdown representation update
34
+
35
+ ## 📋 **Features**
36
+
37
+ - Room-based gameplay
38
+ - Mistral AI opponent with trash talk
39
+ - Real-time game state updates
40
+ - Markdown visualization
41
+ - Multiple concurrent games
42
+ - Responsive design
43
+
44
+ Perfect for testing and demonstrating the tic-tac-toe game logic!
app.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Serve the room-based game page
341
+ @app.route('/')
342
+ def index():
343
+ return send_from_directory('.', 'room_game.html')
344
+
345
+ # Serve static files
346
+ @app.route('/<path:path>')
347
+ def serve_static(path):
348
+ return send_from_directory('.', path)
349
+
350
+ if __name__ == '__main__':
351
+ app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask==3.0.0
2
+ flask-cors==4.0.0
3
+ mistralai
4
+ python-dotenv
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
+ });