khanhromvn commited on
Commit
e3421f8
·
1 Parent(s): fb1e490

📝 ✨ thêm ứng dụng giải đố Water Sort Puzzle với AI

Browse files

- Triển khai toàn bộ ứng dụng giải đố Water Sort Puzzle bằng Gradio.
- Tích hợp mô hình AI (WaterSortNet) để đưa ra gợi ý nước đi tối ưu.
- Cấu trúc lại dự án với các module cấu hình, logging và tiện ích chung mới.
- Cập nhật README chi tiết về cài đặt, sử dụng và tính năng của ứng dụng.
- Bổ sung file .env.example và .gitignore để quản lý môi trường dự án.

Files changed (8) hide show
  1. .env.example +6 -0
  2. .gitignore +1 -0
  3. README.md +60 -12
  4. app.py +571 -0
  5. config.py +54 -0
  6. logger.py +38 -0
  7. requirements.txt +0 -0
  8. utils.py +89 -0
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Environment variables
2
+ DEVICE=auto
3
+ DEBUG=False
4
+ SHARE=False
5
+ SERVER_PORT=7860
6
+ MODEL_PRECISION=fp32
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env
README.md CHANGED
@@ -1,12 +1,60 @@
1
- ---
2
- title: WaterSortSpace
3
- emoji: 🚀
4
- colorFrom: yellow
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Water Sort Puzzle Solver - Gradio App
2
+
3
+ Ứng dụng giải Water Sort Puzzle với AI sử dụng Gradio.
4
+
5
+ ## Yêu cầu
6
+
7
+ - Python 3.8+
8
+ - PyTorch (GPU hoặc CPU)
9
+ - Gradio 4.0+
10
+
11
+ ## Cài đặt
12
+
13
+ 1. Clone repo hoặc tải file
14
+ 2. Cài đặt dependencies:
15
+ ```bash
16
+ pip install -r requirements.txt
17
+ ```
18
+
19
+ 3. Tạo folder `models` và upload các file `.pth`:
20
+ ```bash
21
+ mkdir models
22
+ # Copy file .pth vào folder này
23
+ ```
24
+
25
+ 4. Chạy ứng dụng:
26
+ ```bash
27
+ python app.py
28
+ ```
29
+
30
+ 5. Mở browser: http://localhost:7860
31
+
32
+ ## Cách sử dụng
33
+
34
+ 1. **Chọn Model**: Chọn model từ dropdown và click "Tải Model"
35
+ 2. **Bắt đầu**: Click "Bắt đầu" để tạo game mới
36
+ 3. **Di chuyển**: Click hai chai liên tiếp (chai nguồn → chai đích)
37
+ 4. **Gợi ý**: Click "Gợi ý" để AI gợi ý nước đi tiếp theo
38
+ 5. **Reset**: Click "Reset" để chơi lại
39
+
40
+ ## Tính năng
41
+
42
+ - 🎮 Giao diện trực quan với Gradio
43
+ - 🤖 AI gợi ý nước đi tối ưu
44
+ - 📊 Hiển thị thống kê game (số bước, model, device)
45
+ - 💾 Hỗ trợ nhiều model khác nhau
46
+ - 🚀 Hỗ trợ GPU/CPU
47
+
48
+ ## Model cần thiết
49
+
50
+ Đặt các file model trong folder `models/`:
51
+ - `watersort_imitation.pth` (từ Imitation Learning)
52
+ - `watersort_rl_model.pth` (từ Reinforcement Learning)
53
+ - Hoặc bất kỳ model nào khác
54
+
55
+ ## Troubleshooting
56
+
57
+ ### Model không load được
58
+ - Kiểm tra đường dẫn file model
59
+ - Kiểm tra định dạng file (phải là .pth)
60
+ - Ki
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import torch
4
+ import torch.nn as nn
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+ import random
9
+ from typing import List, Tuple, Dict, Optional
10
+ from datetime import datetime
11
+ import threading
12
+ import time
13
+
14
+ # =============================================================================
15
+ # 1. WATER SORT ENVIRONMENT
16
+ # =============================================================================
17
+
18
+ class WaterSortEnv:
19
+ def __init__(self, num_colors=6, bottle_height=4, num_bottles=8):
20
+ self.num_colors = num_colors
21
+ self.bottle_height = bottle_height
22
+ self.num_bottles = num_bottles
23
+ self.bottles = np.zeros((num_bottles, bottle_height), dtype=int)
24
+ self.move_history = []
25
+ self.game_started = False
26
+ self.game_finished = False
27
+
28
+ def reset(self) -> np.ndarray:
29
+ """Reset game to solvable initial state"""
30
+ colors = list(range(1, self.num_colors + 1)) * self.bottle_height
31
+ random.shuffle(colors)
32
+
33
+ self.bottles = np.zeros((self.num_bottles, self.bottle_height), dtype=int)
34
+ color_idx = 0
35
+
36
+ for i in range(self.num_bottles - 2):
37
+ for j in range(self.bottle_height):
38
+ if color_idx < len(colors):
39
+ self.bottles[i, self.bottle_height - 1 - j] = colors[color_idx]
40
+ color_idx += 1
41
+
42
+ self.move_history = []
43
+ self.game_started = True
44
+ self.game_finished = False
45
+ return self.get_state()
46
+
47
+ def get_state(self) -> np.ndarray:
48
+ return self.bottles.copy()
49
+
50
+ def get_valid_moves(self) -> List[Tuple[int, int]]:
51
+ """Get all valid moves"""
52
+ valid_moves = []
53
+ for from_idx in range(self.num_bottles):
54
+ for to_idx in range(self.num_bottles):
55
+ if from_idx != to_idx and self._is_valid_move(from_idx, to_idx):
56
+ valid_moves.append((from_idx, to_idx))
57
+ return valid_moves
58
+
59
+ def _is_valid_move(self, from_idx: int, to_idx: int) -> bool:
60
+ """Check if move is valid"""
61
+ from_bottle = self.bottles[from_idx]
62
+ to_bottle = self.bottles[to_idx]
63
+
64
+ if np.sum(from_bottle > 0) == 0:
65
+ return False
66
+ if np.sum(to_bottle > 0) == self.bottle_height:
67
+ return False
68
+
69
+ source_top_idx = np.where(from_bottle > 0)[0]
70
+ if len(source_top_idx) == 0:
71
+ return False
72
+ source_top_color = from_bottle[source_top_idx[0]]
73
+
74
+ dest_top_idx = np.where(to_bottle > 0)[0]
75
+ if len(dest_top_idx) == 0:
76
+ return True
77
+ dest_top_color = to_bottle[dest_top_idx[0]]
78
+
79
+ return source_top_color == dest_top_color
80
+
81
+ def step(self, action: Tuple[int, int]):
82
+ """Execute move"""
83
+ from_idx, to_idx = action
84
+
85
+ if not self._is_valid_move(from_idx, to_idx):
86
+ return self.get_state(), -1, False
87
+
88
+ self._pour_liquid(from_idx, to_idx)
89
+ self.move_history.append((from_idx, to_idx))
90
+ done = self.is_solved()
91
+
92
+ if done:
93
+ self.game_finished = True
94
+
95
+ reward = 10.0 if done else 0.1
96
+ return self.get_state(), reward, done
97
+
98
+ def _pour_liquid(self, from_idx: int, to_idx: int):
99
+ """Pour liquid from one bottle to another"""
100
+ from_bottle = self.bottles[from_idx]
101
+ to_bottle = self.bottles[to_idx]
102
+
103
+ source_non_empty = np.where(from_bottle > 0)[0]
104
+ if len(source_non_empty) == 0:
105
+ return
106
+
107
+ source_top_idx = source_non_empty[0]
108
+ source_color = from_bottle[source_top_idx]
109
+
110
+ pour_amount = 1
111
+ for i in range(source_top_idx + 1, len(from_bottle)):
112
+ if from_bottle[i] == source_color:
113
+ pour_amount += 1
114
+ else:
115
+ break
116
+
117
+ dest_empty = np.where(to_bottle == 0)[0]
118
+ if len(dest_empty) == 0:
119
+ return
120
+
121
+ available_space = len(dest_empty)
122
+ actual_pour = min(pour_amount, available_space)
123
+
124
+ for i in range(actual_pour):
125
+ from_pos = source_top_idx + i
126
+ to_pos = dest_empty[-(i+1)]
127
+ self.bottles[to_idx, to_pos] = source_color
128
+ self.bottles[from_idx, from_pos] = 0
129
+
130
+ def is_solved(self) -> bool:
131
+ """Check if puzzle is solved"""
132
+ for bottle in self.bottles:
133
+ unique_colors = np.unique(bottle[bottle > 0])
134
+ if len(unique_colors) > 1:
135
+ return False
136
+ if len(unique_colors) == 1 and np.sum(bottle > 0) != self.bottle_height and np.sum(bottle > 0) != 0:
137
+ return False
138
+ return True
139
+
140
+ # =============================================================================
141
+ # 2. NEURAL NETWORK ARCHITECTURE
142
+ # =============================================================================
143
+
144
+ class WaterSortNet(nn.Module):
145
+ def __init__(self, num_bottles=8, bottle_height=4, num_colors=6):
146
+ super(WaterSortNet, self).__init__()
147
+ self.num_bottles = num_bottles
148
+ self.bottle_height = bottle_height
149
+ self.num_colors = num_colors
150
+
151
+ self.conv1 = nn.Conv2d(num_colors, 128, kernel_size=3, padding=1)
152
+ self.conv2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
153
+ self.conv3 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
154
+ self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
155
+
156
+ self.bn1 = nn.BatchNorm2d(128)
157
+ self.bn2 = nn.BatchNorm2d(128)
158
+ self.bn3 = nn.BatchNorm2d(128)
159
+ self.bn4 = nn.BatchNorm2d(128)
160
+
161
+ self.policy_conv = nn.Conv2d(128, 64, kernel_size=3, padding=1)
162
+ self.policy_fc1 = nn.Linear(64 * num_bottles * bottle_height, 512)
163
+ self.policy_fc2 = nn.Linear(512, num_bottles * num_bottles)
164
+ self.policy_bn = nn.BatchNorm1d(512)
165
+
166
+ self.value_conv = nn.Conv2d(128, 64, kernel_size=3, padding=1)
167
+ self.value_fc1 = nn.Linear(64 * num_bottles * bottle_height, 512)
168
+ self.value_fc2 = nn.Linear(512, 256)
169
+ self.value_fc3 = nn.Linear(256, 1)
170
+ self.value_bn1 = nn.BatchNorm1d(512)
171
+ self.value_bn2 = nn.BatchNorm1d(256)
172
+
173
+ self.relu = nn.ReLU()
174
+ self.dropout = nn.Dropout(0.3)
175
+
176
+ def forward(self, x):
177
+ batch_size = x.size(0)
178
+
179
+ x = self.relu(self.bn1(self.conv1(x)))
180
+ x = self.relu(self.bn2(self.conv2(x)))
181
+ x = self.relu(self.bn3(self.conv3(x)))
182
+ x = self.relu(self.bn4(self.conv4(x)))
183
+
184
+ policy = self.relu(self.policy_conv(x))
185
+ policy = policy.view(batch_size, -1)
186
+ policy = self.dropout(self.relu(self.policy_bn(self.policy_fc1(policy))))
187
+ policy = self.policy_fc2(policy)
188
+
189
+ value = self.relu(self.value_conv(x))
190
+ value = value.view(batch_size, -1)
191
+ value = self.dropout(self.relu(self.value_bn1(self.value_fc1(value))))
192
+ value = self.dropout(self.relu(self.value_bn2(self.value_fc2(value))))
193
+ value = torch.tanh(self.value_fc3(value))
194
+
195
+ return policy, value
196
+
197
+ # =============================================================================
198
+ # 3. DATA PROCESSOR
199
+ # =============================================================================
200
+
201
+ class DataProcessor:
202
+ def __init__(self, num_bottles=8, bottle_height=4, num_colors=6):
203
+ self.num_bottles = num_bottles
204
+ self.bottle_height = bottle_height
205
+ self.num_colors = num_colors
206
+
207
+ def state_to_tensor(self, state):
208
+ """Chuyển state thành one-hot encoded tensor"""
209
+ one_hot = np.zeros((self.num_colors, self.num_bottles, self.bottle_height), dtype=np.float32)
210
+
211
+ for bottle_idx in range(self.num_bottles):
212
+ for height_idx in range(self.bottle_height):
213
+ color = int(state[bottle_idx, height_idx])
214
+ if color > 0:
215
+ one_hot[color - 1, bottle_idx, height_idx] = 1.0
216
+
217
+ return torch.from_numpy(one_hot)
218
+
219
+ # =============================================================================
220
+ # 4. GAME STATE MANAGER
221
+ # =============================================================================
222
+
223
+ class GameStateManager:
224
+ def __init__(self):
225
+ self.env = WaterSortEnv()
226
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
227
+ self.model = None
228
+ self.processor = DataProcessor()
229
+ self.current_model_name = None
230
+ self.ai_running = False
231
+ self.selected_bottles = None
232
+ self.game_stats = {
233
+ 'moves_count': 0,
234
+ 'start_time': None,
235
+ 'ai_suggested_move': None,
236
+ 'valid_moves': []
237
+ }
238
+
239
+ def load_model(self, model_path: str) -> bool:
240
+ """Load model từ file"""
241
+ try:
242
+ self.model = WaterSortNet(num_bottles=8, bottle_height=4, num_colors=6).to(self.device)
243
+ checkpoint = torch.load(model_path, map_location=self.device)
244
+
245
+ if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
246
+ self.model.load_state_dict(checkpoint['model_state_dict'])
247
+ else:
248
+ self.model.load_state_dict(checkpoint)
249
+
250
+ self.model.eval()
251
+ self.current_model_name = Path(model_path).stem
252
+ return True
253
+ except Exception as e:
254
+ print(f"Error loading model: {e}")
255
+ return False
256
+
257
+ def start_game(self):
258
+ """Bắt đầu game mới"""
259
+ self.env.reset()
260
+ self.game_stats = {
261
+ 'moves_count': 0,
262
+ 'start_time': datetime.now(),
263
+ 'ai_suggested_move': None,
264
+ 'valid_moves': self.env.get_valid_moves()
265
+ }
266
+ self.selected_bottles = None
267
+ return self.env.get_state()
268
+
269
+ def reset_game(self):
270
+ """Reset game"""
271
+ self.env.game_finished = False
272
+ return self.start_game()
273
+
274
+ def get_next_move_suggestion(self) -> Optional[Tuple[int, int]]:
275
+ """Lấy gợi ý từ AI"""
276
+ if self.model is None:
277
+ return None
278
+
279
+ try:
280
+ state = self.env.get_state()
281
+ valid_moves = self.env.get_valid_moves()
282
+
283
+ if not valid_moves:
284
+ return None
285
+
286
+ state_tensor = self.processor.state_to_tensor(state).unsqueeze(0).to(self.device)
287
+
288
+ with torch.no_grad():
289
+ policy, _ = self.model(state_tensor)
290
+
291
+ policy_probs = torch.softmax(policy, dim=1).cpu().numpy()[0]
292
+
293
+ best_move = None
294
+ best_score = -float('inf')
295
+
296
+ for move in valid_moves:
297
+ from_idx, to_idx = move
298
+ move_index = from_idx * 8 + to_idx
299
+ score = policy_probs[move_index]
300
+
301
+ if score > best_score:
302
+ best_score = score
303
+ best_move = move
304
+
305
+ self.game_stats['ai_suggested_move'] = best_move
306
+ return best_move
307
+ except Exception as e:
308
+ print(f"Error getting suggestion: {e}")
309
+ return None
310
+
311
+ def make_move(self, from_bottle: int, to_bottle: int) -> Tuple[bool, str]:
312
+ """Thực hiện di chuyển"""
313
+ state, reward, done = self.env.step((from_bottle, to_bottle))
314
+
315
+ if reward < 0:
316
+ return False, "Nước không thể đổ vào chai này!"
317
+
318
+ self.game_stats['moves_count'] += 1
319
+ self.game_stats['valid_moves'] = self.env.get_valid_moves()
320
+ self.selected_bottles = None
321
+
322
+ if done:
323
+ return True, f"Chúc mừng! Bạn đã giải xong trong {self.game_stats['moves_count']} bước!"
324
+
325
+ return True, f"Bước thành công! Tổng bước: {self.game_stats['moves_count']}"
326
+
327
+ def select_bottle(self, bottle_idx: int) -> str:
328
+ """Chọn chai"""
329
+ if self.selected_bottles is None:
330
+ self.selected_bottles = bottle_idx
331
+ return f"Đã chọn chai {bottle_idx}. Chọn chai đích."
332
+ else:
333
+ from_bottle = self.selected_bottles
334
+ to_bottle = bottle_idx
335
+ success, message = self.make_move(from_bottle, to_bottle)
336
+ return message
337
+
338
+ # =============================================================================
339
+ # 5. VISUALIZATION
340
+ # =============================================================================
341
+
342
+ def draw_game_board(state: np.ndarray, selected_bottle: Optional[int] = None) -> str:
343
+ """Vẽ bảng game dưới dạng HTML"""
344
+ colors_map = {
345
+ 0: '#ffffff',
346
+ 1: '#FF6B6B',
347
+ 2: '#4ECDC4',
348
+ 3: '#45B7D1',
349
+ 4: '#FFA07A',
350
+ 5: '#98D8C8',
351
+ 6: '#F7DC6F'
352
+ }
353
+
354
+ html = '<div style="display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; padding: 20px;">'
355
+
356
+ num_bottles = state.shape[0]
357
+ bottle_height = state.shape[1]
358
+
359
+ for bottle_idx in range(num_bottles):
360
+ bottle = state[bottle_idx]
361
+ is_selected = bottle_idx == selected_bottle
362
+ border_style = 'border: 3px solid #FFD700;' if is_selected else 'border: 2px solid #333;'
363
+
364
+ html += f'<div style="text-align: center; margin: 10px;">'
365
+ html += f'<div style="width: 60px; height: 150px; {border_style} background: #f0f0f0; margin-bottom: 10px; display: flex; flex-direction: column-reverse; overflow: hidden;">'
366
+
367
+ for height_idx in range(bottle_height):
368
+ color_val = int(bottle[height_idx])
369
+ color = colors_map.get(color_val, '#ffffff')
370
+ html += f'<div style="width: 100%; height: 30px; background: {color}; border-bottom: 1px solid #999;"></div>'
371
+
372
+ html += '</div>'
373
+ html += f'<p style="margin: 5px 0; font-weight: bold;">Chai {bottle_idx}</p>'
374
+ html += '</div>'
375
+
376
+ html += '</div>'
377
+ return html
378
+
379
+ def get_game_stats_html(game_manager: GameStateManager) -> str:
380
+ """Tạo HTML hiển thị thống kê game"""
381
+ stats = game_manager.game_stats
382
+ model_info = game_manager.current_model_name if game_manager.current_model_name else "Chưa tải"
383
+
384
+ html = f"""
385
+ <div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 10px 0;">
386
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;">
387
+ <div>
388
+ <p style="margin: 0; font-size: 12px; color: #666;">📊 Số bước</p>
389
+ <p style="margin: 5px 0; font-size: 24px; font-weight: bold;">{stats['moves_count']}</p>
390
+ </div>
391
+ <div>
392
+ <p style="margin: 0; font-size: 12px; color: #666;">🤖 Model</p>
393
+ <p style="margin: 5px 0; font-size: 14px; font-weight: bold;">{model_info}</p>
394
+ </div>
395
+ <div>
396
+ <p style="margin: 0; font-size: 12px; color: #666;">💾 Thiết bị</p>
397
+ <p style="margin: 5px 0; font-size: 14px; font-weight: bold;">{'GPU' if torch.cuda.is_available() else 'CPU'}</p>
398
+ </div>
399
+ </div>
400
+ <p style="margin: 10px 0; font-size: 12px; color: #999;">
401
+ Nước hợp lệ: {len(stats['valid_moves'])} nước
402
+ </p>
403
+ </div>
404
+ """
405
+ return html
406
+
407
+ # =============================================================================
408
+ # 6. MAIN GRADIO APP
409
+ # =============================================================================
410
+
411
+ def get_available_models() -> List[str]:
412
+ """Lấy danh sách model từ folder models"""
413
+ models_dir = Path("models")
414
+ models_dir.mkdir(exist_ok=True)
415
+
416
+ model_files = list(models_dir.glob("*.pth"))
417
+ return [f.name for f in sorted(model_files)]
418
+
419
+ # Initialize global game manager
420
+ game_manager = GameStateManager()
421
+
422
+ def load_model_ui(selected_model: str) -> Tuple[str, str]:
423
+ """Load model từ UI"""
424
+ if not selected_model:
425
+ return "❌ Vui lòng chọn model", ""
426
+
427
+ model_path = Path("models") / selected_model
428
+ if game_manager.load_model(str(model_path)):
429
+ return f"✅ Tải model thành công: {selected_model}", draw_game_board(np.zeros((8, 4)))
430
+ else:
431
+ return f"❌ Lỗi khi tải model: {selected_model}", ""
432
+
433
+ def start_game_ui() -> Tuple[str, str, str]:
434
+ """Bắt đầu game mới"""
435
+ state = game_manager.start_game()
436
+ board = draw_game_board(state)
437
+ stats = get_game_stats_html(game_manager)
438
+ return stats, board, "✅ Bắt đầu game mới!"
439
+
440
+ def reset_game_ui() -> Tuple[str, str, str]:
441
+ """Reset game"""
442
+ state = game_manager.reset_game()
443
+ board = draw_game_board(state)
444
+ stats = get_game_stats_html(game_manager)
445
+ return stats, board, "🔄 Game đã được reset!"
446
+
447
+ def suggest_move_ui() -> Tuple[str, str]:
448
+ """Gợi ý di chuyển từ AI"""
449
+ if game_manager.model is None:
450
+ return "", "❌ Vui lòng tải model trước!"
451
+
452
+ if game_manager.env.game_finished:
453
+ return "", "🎉 Game đã kết thúc!"
454
+
455
+ move = game_manager.get_next_move_suggestion()
456
+ if move:
457
+ from_bottle, to_bottle = move
458
+ message = f"💡 Gợi ý: Đổ từ chai {from_bottle} sang chai {to_bottle}"
459
+ return message, message
460
+ else:
461
+ return "", "❌ Không thể tìm được nước gợi ý"
462
+
463
+ def bottle_click_ui(bottle_idx: int) -> Tuple[str, str, str]:
464
+ """Xử lý click bottle"""
465
+ if not game_manager.env.game_started:
466
+ return "", draw_game_board(game_manager.env.get_state()), "❌ Vui lòng bắt đầu game!"
467
+
468
+ if game_manager.env.game_finished:
469
+ return "", draw_game_board(game_manager.env.get_state()), "🎉 Game đã kết thúc!"
470
+
471
+ if game_manager.selected_bottles is None:
472
+ game_manager.selected_bottles = bottle_idx
473
+ state = game_manager.env.get_state()
474
+ board = draw_game_board(state, bottle_idx)
475
+ stats = get_game_stats_html(game_manager)
476
+ return stats, board, f"✓ Chọn chai {bottle_idx}. Chọn chai đích."
477
+ else:
478
+ from_bottle = game_manager.selected_bottles
479
+ to_bottle = bottle_idx
480
+ success, message = game_manager.make_move(from_bottle, to_bottle)
481
+
482
+ state = game_manager.env.get_state()
483
+ board = draw_game_board(state)
484
+ stats = get_game_stats_html(game_manager)
485
+
486
+ return stats, board, message
487
+
488
+ def create_bottle_buttons():
489
+ """Tạo buttons cho các chai"""
490
+ buttons = []
491
+ for i in range(8):
492
+ buttons.append(
493
+ gr.Button(f"Chai {i}", size="lg", scale=1)
494
+ )
495
+ return buttons
496
+
497
+ # Create Gradio Interface
498
+ with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
499
+ gr.Markdown("# 🧪 Water Sort Puzzle Solver")
500
+ gr.Markdown("Giải Water Sort Puzzle với sự trợ giúp của AI!")
501
+
502
+ with gr.Row():
503
+ with gr.Column(scale=1):
504
+ gr.Markdown("### ⚙️ Cấu hình")
505
+
506
+ model_dropdown = gr.Dropdown(
507
+ label="Chọn Model",
508
+ choices=get_available_models(),
509
+ interactive=True
510
+ )
511
+
512
+ load_model_btn = gr.Button("📥 Tải Model", variant="primary", size="lg")
513
+ model_status = gr.Textbox(label="Trạng thái", interactive=False)
514
+
515
+ gr.Markdown("### 🎮 Điều khiển")
516
+ start_btn = gr.Button("🎮 Bắt đầu", variant="primary", size="lg")
517
+ reset_btn = gr.Button("🔄 Reset", size="lg")
518
+ suggest_btn = gr.Button("💡 Gợi ý", size="lg")
519
+
520
+ gr.Markdown("### 📊 Thống kê")
521
+ game_stats = gr.HTML()
522
+
523
+ with gr.Column(scale=2):
524
+ gr.Markdown("### 🎯 Bảng trò chơi")
525
+ game_board = gr.HTML()
526
+
527
+ gr.Markdown("### Chọn chai để di chuyển")
528
+ with gr.Row():
529
+ bottle_buttons = create_bottle_buttons()
530
+
531
+ message_display = gr.Textbox(
532
+ label="Thông báo",
533
+ interactive=False,
534
+ lines=2
535
+ )
536
+
537
+ suggestion_display = gr.Textbox(
538
+ label="Gợi ý từ AI",
539
+ interactive=False
540
+ )
541
+
542
+ # Event handlers
543
+ load_model_btn.click(
544
+ fn=load_model_ui,
545
+ inputs=[model_dropdown],
546
+ outputs=[model_status, game_board]
547
+ )
548
+
549
+ start_btn.click(
550
+ fn=start_game_ui,
551
+ outputs=[game_stats, game_board, message_display]
552
+ )
553
+
554
+ reset_btn.click(
555
+ fn=reset_game_ui,
556
+ outputs=[game_stats, game_board, message_display]
557
+ )
558
+
559
+ suggest_btn.click(
560
+ fn=suggest_move_ui,
561
+ outputs=[suggestion_display, message_display]
562
+ )
563
+
564
+ for i, btn in enumerate(bottle_buttons):
565
+ btn.click(
566
+ fn=lambda idx=i: bottle_click_ui(idx),
567
+ outputs=[game_stats, game_board, message_display]
568
+ )
569
+
570
+ if __name__ == "__main__":
571
+ demo.launch(share=True)
config.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Dict
5
+
6
+ # Đường dẫn
7
+ PROJECT_ROOT = Path(__file__).parent
8
+ MODELS_DIR = PROJECT_ROOT / "models"
9
+
10
+ # Game configuration
11
+ GAME_CONFIG = {
12
+ 'num_colors': 6,
13
+ 'bottle_height': 4,
14
+ 'num_bottles': 8,
15
+ 'max_moves': 200
16
+ }
17
+
18
+ # UI configuration
19
+ UI_CONFIG = {
20
+ 'theme': 'soft',
21
+ 'server_name': '0.0.0.0',
22
+ 'server_port': 7860,
23
+ 'share': False,
24
+ 'debug': False
25
+ }
26
+
27
+ # Model configuration
28
+ MODEL_CONFIG = {
29
+ 'device': 'auto', # 'auto', 'cuda', 'cpu'
30
+ 'precision': 'fp32', # 'fp32' hoặc 'fp16'
31
+ }
32
+
33
+ # Color mapping for visualization
34
+ COLORS_MAP = {
35
+ 0: '#ffffff', # White (empty)
36
+ 1: '#FF6B6B', # Red
37
+ 2: '#4ECDC4', # Teal
38
+ 3: '#45B7D1', # Blue
39
+ 4: '#FFA07A', # Light Salmon
40
+ 5: '#98D8C8', # Mint
41
+ 6: '#F7DC6F' # Yellow
42
+ }
43
+
44
+ # Ensure models directory exists
45
+ MODELS_DIR.mkdir(exist_ok=True)
46
+
47
+ # Create config dict
48
+ CONFIG = {
49
+ 'game': GAME_CONFIG,
50
+ 'ui': UI_CONFIG,
51
+ 'model': MODEL_CONFIG,
52
+ 'colors': COLORS_MAP,
53
+ 'models_dir': str(MODELS_DIR)
54
+ }
logger.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # logger.py
2
+ import logging
3
+ import sys
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ def setup_logger(name: str, log_file: str = None) -> logging.Logger:
8
+ """Thiết lập logger"""
9
+ logger = logging.getLogger(name)
10
+ logger.setLevel(logging.DEBUG)
11
+
12
+ # Console handler
13
+ console_handler = logging.StreamHandler(sys.stdout)
14
+ console_handler.setLevel(logging.INFO)
15
+ console_format = logging.Formatter(
16
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
17
+ )
18
+ console_handler.setFormatter(console_format)
19
+ logger.addHandler(console_handler)
20
+
21
+ # File handler (optional)
22
+ if log_file:
23
+ Path(log_file).parent.mkdir(parents=True, exist_ok=True)
24
+ file_handler = logging.FileHandler(log_file)
25
+ file_handler.setLevel(logging.DEBUG)
26
+ file_format = logging.Formatter(
27
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
28
+ )
29
+ file_handler.setFormatter(file_format)
30
+ logger.addHandler(file_handler)
31
+
32
+ return logger
33
+
34
+ # Initialize main logger
35
+ logger = setup_logger(
36
+ 'water_sort_app',
37
+ log_file=f'logs/app_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
38
+ )
requirements.txt ADDED
File without changes
utils.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils.py
2
+ import numpy as np
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional
5
+ import torch
6
+ import config
7
+
8
+ def get_available_models() -> List[str]:
9
+ """Lấy danh sách tất cả model trong folder models"""
10
+ models_dir = Path(config.CONFIG['models_dir'])
11
+ model_files = list(models_dir.glob("*.pth"))
12
+ return sorted([f.name for f in model_files])
13
+
14
+ def validate_model_file(model_path: str) -> bool:
15
+ """Kiểm tra xem file model có hợp lệ không"""
16
+ path = Path(model_path)
17
+
18
+ if not path.exists():
19
+ return False
20
+
21
+ if path.suffix != '.pth':
22
+ return False
23
+
24
+ if path.stat().st_size < 1000: # File quá nhỏ
25
+ return False
26
+
27
+ return True
28
+
29
+ def format_time(seconds: float) -> str:
30
+ """Format thời gian từ giây sang HH:MM:SS"""
31
+ hours = int(seconds // 3600)
32
+ minutes = int((seconds % 3600) // 60)
33
+ secs = int(seconds % 60)
34
+
35
+ if hours > 0:
36
+ return f"{hours}h {minutes}m {secs}s"
37
+ elif minutes > 0:
38
+ return f"{minutes}m {secs}s"
39
+ else:
40
+ return f"{secs}s"
41
+
42
+ def get_color_for_value(color_val: int) -> str:
43
+ """Lấy màu hex từ giá trị color"""
44
+ return config.COLORS_MAP.get(color_val, '#ffffff')
45
+
46
+ def create_bottle_html(bottle_state: np.ndarray, bottle_idx: int,
47
+ selected: bool = False) -> str:
48
+ """Tạo HTML cho một chai"""
49
+ border_color = '#FFD700' if selected else '#333'
50
+ border_width = '3' if selected else '2'
51
+
52
+ html = f'<div style="text-align: center; margin: 10px;">'
53
+ html += f'<div style="width: 60px; height: 150px; border: {border_width}px solid {border_color}; '
54
+ html += f'background: #f0f0f0; margin-bottom: 10px; display: flex; flex-direction: column-reverse; overflow: hidden;">'
55
+
56
+ for height_idx in range(len(bottle_state)):
57
+ color_val = int(bottle_state[height_idx])
58
+ color = get_color_for_value(color_val)
59
+ html += f'<div style="width: 100%; height: 30px; background: {color}; border-bottom: 1px solid #999;"></div>'
60
+
61
+ html += '</div>'
62
+ html += f'<p style="margin: 5px 0; font-weight: bold;">Chai {bottle_idx}</p>'
63
+ html += '</div>'
64
+
65
+ return html
66
+
67
+ def get_device() -> torch.device:
68
+ """Lấy device (GPU hoặc CPU)"""
69
+ device_config = config.MODEL_CONFIG['device']
70
+
71
+ if device_config == 'auto':
72
+ return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
73
+ elif device_config == 'cuda':
74
+ if torch.cuda.is_available():
75
+ return torch.device('cuda')
76
+ else:
77
+ print("Warning: CUDA not available, falling back to CPU")
78
+ return torch.device('cpu')
79
+ else:
80
+ return torch.device('cpu')
81
+
82
+ def print_device_info():
83
+ """In thông tin về device"""
84
+ device = get_device()
85
+ print(f"Device: {device}")
86
+
87
+ if device.type == 'cuda':
88
+ print(f"GPU Name: {torch.cuda.get_device_name(0)}")
89
+ print(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")