Spaces:
Sleeping
Sleeping
Commit ·
3100167
1
Parent(s): bb1d378
📝 ✨ thêm hoàn tác và chế độ AI tự động chơi
Browse files- Triển khai chức năng hoàn tác nước đi, lưu trữ lịch sử trạng thái game.
- Bổ sung chế độ AI tự động chơi theo gợi ý của mô hình với tốc độ điều chỉnh được.
- Hiển thị lịch sử các nước đi và trạng thái hoạt động của AI trong giao diện người dùng.
- Đánh dấu nước đi cuối cùng trên bảng game để người chơi dễ dàng theo dõi.
- Cập nhật UI định kỳ và ngăn người dùng thao tác khi AI đang chạy tự động.
app.py
CHANGED
|
@@ -22,6 +22,7 @@ class WaterSortEnv:
|
|
| 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 |
|
|
@@ -40,6 +41,7 @@ class WaterSortEnv:
|
|
| 40 |
color_idx += 1
|
| 41 |
|
| 42 |
self.move_history = []
|
|
|
|
| 43 |
self.game_started = True
|
| 44 |
self.game_finished = False
|
| 45 |
return self.get_state()
|
|
@@ -87,6 +89,8 @@ class WaterSortEnv:
|
|
| 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:
|
|
@@ -95,6 +99,21 @@ class WaterSortEnv:
|
|
| 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]
|
|
@@ -228,6 +247,8 @@ class GameStateManager:
|
|
| 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,
|
|
@@ -264,13 +285,28 @@ class GameStateManager:
|
|
| 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:
|
|
@@ -334,12 +370,36 @@ class GameStateManager:
|
|
| 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
|
|
|
|
| 343 |
"""Vẽ bảng game dưới dạng HTML"""
|
| 344 |
colors_map = {
|
| 345 |
0: '#ffffff',
|
|
@@ -359,10 +419,17 @@ def draw_game_board(state: np.ndarray, selected_bottle: Optional[int] = None) ->
|
|
| 359 |
for bottle_idx in range(num_bottles):
|
| 360 |
bottle = state[bottle_idx]
|
| 361 |
is_selected = bottle_idx == selected_bottle
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
html += f'<div style="text-align: center; margin: 10px;">'
|
| 365 |
-
html += f'<div style="width: 60px; height: 150px; {
|
| 366 |
|
| 367 |
for height_idx in range(bottle_height):
|
| 368 |
color_val = int(bottle[height_idx])
|
|
@@ -381,6 +448,9 @@ def get_game_stats_html(game_manager: GameStateManager) -> str:
|
|
| 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;">
|
|
@@ -397,6 +467,16 @@ def get_game_stats_html(game_manager: GameStateManager) -> str:
|
|
| 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>
|
|
@@ -404,6 +484,31 @@ def get_game_stats_html(game_manager: GameStateManager) -> str:
|
|
| 404 |
"""
|
| 405 |
return html
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
# =============================================================================
|
| 408 |
# 6. MAIN GRADIO APP
|
| 409 |
# =============================================================================
|
|
@@ -430,19 +535,21 @@ def load_model_ui(selected_model: str) -> Tuple[str, str]:
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 446 |
|
| 447 |
def suggest_move_ui() -> Tuple[str, str]:
|
| 448 |
"""Gợi ý di chuyển từ AI"""
|
|
@@ -460,30 +567,120 @@ def suggest_move_ui() -> Tuple[str, str]:
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 484 |
stats = get_game_stats_html(game_manager)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
def create_bottle_buttons():
|
| 489 |
"""Tạo buttons cho các chai"""
|
|
@@ -515,10 +712,32 @@ with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|
|
@@ -538,6 +757,10 @@ with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
|
|
| 538 |
label="Gợi ý từ AI",
|
| 539 |
interactive=False
|
| 540 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
|
| 542 |
# Event handlers
|
| 543 |
load_model_btn.click(
|
|
@@ -548,12 +771,12 @@ with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
|
|
| 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(
|
|
@@ -561,11 +784,54 @@ with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
|
|
| 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)
|
|
|
|
| 22 |
self.num_bottles = num_bottles
|
| 23 |
self.bottles = np.zeros((num_bottles, bottle_height), dtype=int)
|
| 24 |
self.move_history = []
|
| 25 |
+
self.state_history = [] # Lưu lịch sử các state
|
| 26 |
self.game_started = False
|
| 27 |
self.game_finished = False
|
| 28 |
|
|
|
|
| 41 |
color_idx += 1
|
| 42 |
|
| 43 |
self.move_history = []
|
| 44 |
+
self.state_history = [self.bottles.copy()] # Lưu state ban đầu
|
| 45 |
self.game_started = True
|
| 46 |
self.game_finished = False
|
| 47 |
return self.get_state()
|
|
|
|
| 89 |
|
| 90 |
self._pour_liquid(from_idx, to_idx)
|
| 91 |
self.move_history.append((from_idx, to_idx))
|
| 92 |
+
self.state_history.append(self.bottles.copy()) # Lưu state sau mỗi nước
|
| 93 |
+
|
| 94 |
done = self.is_solved()
|
| 95 |
|
| 96 |
if done:
|
|
|
|
| 99 |
reward = 10.0 if done else 0.1
|
| 100 |
return self.get_state(), reward, done
|
| 101 |
|
| 102 |
+
def undo_last_move(self) -> bool:
|
| 103 |
+
"""Quay lại nước đi trước đó"""
|
| 104 |
+
if len(self.state_history) <= 1: # Chỉ còn state ban đầu
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
# Xóa state hiện tại
|
| 108 |
+
self.state_history.pop()
|
| 109 |
+
self.move_history.pop()
|
| 110 |
+
|
| 111 |
+
# Khôi phục state trước đó
|
| 112 |
+
self.bottles = self.state_history[-1].copy()
|
| 113 |
+
self.game_finished = False
|
| 114 |
+
|
| 115 |
+
return True
|
| 116 |
+
|
| 117 |
def _pour_liquid(self, from_idx: int, to_idx: int):
|
| 118 |
"""Pour liquid from one bottle to another"""
|
| 119 |
from_bottle = self.bottles[from_idx]
|
|
|
|
| 247 |
self.processor = DataProcessor()
|
| 248 |
self.current_model_name = None
|
| 249 |
self.ai_running = False
|
| 250 |
+
self.ai_thread = None
|
| 251 |
+
self.ai_delay = 1.0 # Delay giữa các nước (giây)
|
| 252 |
self.selected_bottles = None
|
| 253 |
self.game_stats = {
|
| 254 |
'moves_count': 0,
|
|
|
|
| 285 |
'valid_moves': self.env.get_valid_moves()
|
| 286 |
}
|
| 287 |
self.selected_bottles = None
|
| 288 |
+
self.ai_running = False
|
| 289 |
return self.env.get_state()
|
| 290 |
|
| 291 |
def reset_game(self):
|
| 292 |
"""Reset game"""
|
| 293 |
+
self.ai_running = False
|
| 294 |
self.env.game_finished = False
|
| 295 |
return self.start_game()
|
| 296 |
|
| 297 |
+
def undo_move(self) -> Tuple[bool, str]:
|
| 298 |
+
"""Quay lại nước đi trước"""
|
| 299 |
+
if self.ai_running:
|
| 300 |
+
return False, "❌ Không thể undo khi AI đang chơi!"
|
| 301 |
+
|
| 302 |
+
success = self.env.undo_last_move()
|
| 303 |
+
if success:
|
| 304 |
+
self.game_stats['moves_count'] = len(self.env.move_history)
|
| 305 |
+
self.game_stats['valid_moves'] = self.env.get_valid_moves()
|
| 306 |
+
return True, f"✅ Đã quay lại! Tổng bước: {self.game_stats['moves_count']}"
|
| 307 |
+
else:
|
| 308 |
+
return False, "❌ Không thể quay lại thêm!"
|
| 309 |
+
|
| 310 |
def get_next_move_suggestion(self) -> Optional[Tuple[int, int]]:
|
| 311 |
"""Lấy gợi ý từ AI"""
|
| 312 |
if self.model is None:
|
|
|
|
| 370 |
to_bottle = bottle_idx
|
| 371 |
success, message = self.make_move(from_bottle, to_bottle)
|
| 372 |
return message
|
| 373 |
+
|
| 374 |
+
def start_ai_autoplay(self):
|
| 375 |
+
"""Bắt đầu AI tự động chơi"""
|
| 376 |
+
if self.ai_running:
|
| 377 |
+
return False, "AI đã đang chạy!"
|
| 378 |
+
|
| 379 |
+
if self.model is None:
|
| 380 |
+
return False, "Vui lòng tải model trước!"
|
| 381 |
+
|
| 382 |
+
if self.env.game_finished:
|
| 383 |
+
return False, "Game đã kết thúc!"
|
| 384 |
+
|
| 385 |
+
self.ai_running = True
|
| 386 |
+
return True, "✅ AI bắt đầu tự động chơi!"
|
| 387 |
+
|
| 388 |
+
def stop_ai_autoplay(self):
|
| 389 |
+
"""Dừng AI tự động chơi"""
|
| 390 |
+
self.ai_running = False
|
| 391 |
+
return "⏸️ AI đã dừng!"
|
| 392 |
+
|
| 393 |
+
def set_ai_speed(self, delay: float):
|
| 394 |
+
"""Đặt tốc độ AI (delay giữa các nước)"""
|
| 395 |
+
self.ai_delay = delay
|
| 396 |
|
| 397 |
# =============================================================================
|
| 398 |
# 5. VISUALIZATION
|
| 399 |
# =============================================================================
|
| 400 |
|
| 401 |
+
def draw_game_board(state: np.ndarray, selected_bottle: Optional[int] = None,
|
| 402 |
+
last_move: Optional[Tuple[int, int]] = None) -> str:
|
| 403 |
"""Vẽ bảng game dưới dạng HTML"""
|
| 404 |
colors_map = {
|
| 405 |
0: '#ffffff',
|
|
|
|
| 419 |
for bottle_idx in range(num_bottles):
|
| 420 |
bottle = state[bottle_idx]
|
| 421 |
is_selected = bottle_idx == selected_bottle
|
| 422 |
+
|
| 423 |
+
# Highlight nếu là nước vừa chơi
|
| 424 |
+
is_last_move = False
|
| 425 |
+
if last_move:
|
| 426 |
+
is_last_move = bottle_idx in last_move
|
| 427 |
+
|
| 428 |
+
border_color = '#FFD700' if is_selected else ('#00FF00' if is_last_move else '#333')
|
| 429 |
+
border_width = '3' if (is_selected or is_last_move) else '2'
|
| 430 |
|
| 431 |
html += f'<div style="text-align: center; margin: 10px;">'
|
| 432 |
+
html += f'<div style="width: 60px; height: 150px; border: {border_width}px solid {border_color}; background: #f0f0f0; margin-bottom: 10px; display: flex; flex-direction: column-reverse; overflow: hidden;">'
|
| 433 |
|
| 434 |
for height_idx in range(bottle_height):
|
| 435 |
color_val = int(bottle[height_idx])
|
|
|
|
| 448 |
stats = game_manager.game_stats
|
| 449 |
model_info = game_manager.current_model_name if game_manager.current_model_name else "Chưa tải"
|
| 450 |
|
| 451 |
+
ai_status = "🤖 Đang chơi..." if game_manager.ai_running else "⏸️ Dừng"
|
| 452 |
+
history_count = len(game_manager.env.move_history)
|
| 453 |
+
|
| 454 |
html = f"""
|
| 455 |
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 10px 0;">
|
| 456 |
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;">
|
|
|
|
| 467 |
<p style="margin: 5px 0; font-size: 14px; font-weight: bold;">{'GPU' if torch.cuda.is_available() else 'CPU'}</p>
|
| 468 |
</div>
|
| 469 |
</div>
|
| 470 |
+
<div style="margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
| 471 |
+
<div>
|
| 472 |
+
<p style="margin: 0; font-size: 12px; color: #666;">📜 Lịch sử</p>
|
| 473 |
+
<p style="margin: 5px 0; font-size: 16px; font-weight: bold;">{history_count} nước đi</p>
|
| 474 |
+
</div>
|
| 475 |
+
<div>
|
| 476 |
+
<p style="margin: 0; font-size: 12px; color: #666;">🎮 Trạng thái AI</p>
|
| 477 |
+
<p style="margin: 5px 0; font-size: 16px; font-weight: bold;">{ai_status}</p>
|
| 478 |
+
</div>
|
| 479 |
+
</div>
|
| 480 |
<p style="margin: 10px 0; font-size: 12px; color: #999;">
|
| 481 |
Nước hợp lệ: {len(stats['valid_moves'])} nước
|
| 482 |
</p>
|
|
|
|
| 484 |
"""
|
| 485 |
return html
|
| 486 |
|
| 487 |
+
def get_move_history_html(game_manager: GameStateManager) -> str:
|
| 488 |
+
"""Hiển thị lịch sử các nước đi"""
|
| 489 |
+
moves = game_manager.env.move_history
|
| 490 |
+
|
| 491 |
+
if not moves:
|
| 492 |
+
return "<p style='text-align: center; color: #999;'>Chưa có nước đi nào</p>"
|
| 493 |
+
|
| 494 |
+
html = "<div style='max-height: 300px; overflow-y: auto; padding: 10px;'>"
|
| 495 |
+
html += "<table style='width: 100%; border-collapse: collapse;'>"
|
| 496 |
+
html += "<tr style='background: #f0f0f0; font-weight: bold;'>"
|
| 497 |
+
html += "<th style='padding: 8px; border: 1px solid #ddd;'>Bước</th>"
|
| 498 |
+
html += "<th style='padding: 8px; border: 1px solid #ddd;'>Từ chai</th>"
|
| 499 |
+
html += "<th style='padding: 8px; border: 1px solid #ddd;'>Đến chai</th>"
|
| 500 |
+
html += "</tr>"
|
| 501 |
+
|
| 502 |
+
for i, (from_idx, to_idx) in enumerate(moves, 1):
|
| 503 |
+
html += f"<tr style='background: {'#fff' if i % 2 == 0 else '#f9f9f9'};'>"
|
| 504 |
+
html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{i}</td>"
|
| 505 |
+
html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{from_idx}</td>"
|
| 506 |
+
html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{to_idx}</td>"
|
| 507 |
+
html += "</tr>"
|
| 508 |
+
|
| 509 |
+
html += "</table></div>"
|
| 510 |
+
return html
|
| 511 |
+
|
| 512 |
# =============================================================================
|
| 513 |
# 6. MAIN GRADIO APP
|
| 514 |
# =============================================================================
|
|
|
|
| 535 |
else:
|
| 536 |
return f"❌ Lỗi khi tải model: {selected_model}", ""
|
| 537 |
|
| 538 |
+
def start_game_ui() -> Tuple[str, str, str, str]:
|
| 539 |
"""Bắt đầu game mới"""
|
| 540 |
state = game_manager.start_game()
|
| 541 |
board = draw_game_board(state)
|
| 542 |
stats = get_game_stats_html(game_manager)
|
| 543 |
+
history = get_move_history_html(game_manager)
|
| 544 |
+
return stats, board, "✅ Bắt đầu game mới!", history
|
| 545 |
|
| 546 |
+
def reset_game_ui() -> Tuple[str, str, str, str]:
|
| 547 |
"""Reset game"""
|
| 548 |
state = game_manager.reset_game()
|
| 549 |
board = draw_game_board(state)
|
| 550 |
stats = get_game_stats_html(game_manager)
|
| 551 |
+
history = get_move_history_html(game_manager)
|
| 552 |
+
return stats, board, "🔄 Game đã được reset!", history
|
| 553 |
|
| 554 |
def suggest_move_ui() -> Tuple[str, str]:
|
| 555 |
"""Gợi ý di chuyển từ AI"""
|
|
|
|
| 567 |
else:
|
| 568 |
return "", "❌ Không thể tìm được nước gợi ý"
|
| 569 |
|
| 570 |
+
def bottle_click_ui(bottle_idx: int) -> Tuple[str, str, str, str]:
|
| 571 |
"""Xử lý click bottle"""
|
| 572 |
if not game_manager.env.game_started:
|
| 573 |
+
return "", draw_game_board(game_manager.env.get_state()), "❌ Vui lòng bắt đầu game!", ""
|
| 574 |
|
| 575 |
if game_manager.env.game_finished:
|
| 576 |
+
return "", draw_game_board(game_manager.env.get_state()), "🎉 Game đã kết thúc!", ""
|
| 577 |
+
|
| 578 |
+
if game_manager.ai_running:
|
| 579 |
+
return "", draw_game_board(game_manager.env.get_state()), "❌ AI đang chơi, không thể thao tác!", ""
|
| 580 |
|
| 581 |
if game_manager.selected_bottles is None:
|
| 582 |
game_manager.selected_bottles = bottle_idx
|
| 583 |
state = game_manager.env.get_state()
|
| 584 |
board = draw_game_board(state, bottle_idx)
|
| 585 |
stats = get_game_stats_html(game_manager)
|
| 586 |
+
history = get_move_history_html(game_manager)
|
| 587 |
+
return stats, board, f"✓ Chọn chai {bottle_idx}. Chọn chai đích.", history
|
| 588 |
else:
|
| 589 |
from_bottle = game_manager.selected_bottles
|
| 590 |
to_bottle = bottle_idx
|
| 591 |
success, message = game_manager.make_move(from_bottle, to_bottle)
|
| 592 |
|
| 593 |
state = game_manager.env.get_state()
|
| 594 |
+
last_move = (from_bottle, to_bottle) if success else None
|
| 595 |
+
board = draw_game_board(state, last_move=last_move)
|
| 596 |
stats = get_game_stats_html(game_manager)
|
| 597 |
+
history = get_move_history_html(game_manager)
|
| 598 |
+
|
| 599 |
+
return stats, board, message, history
|
| 600 |
+
|
| 601 |
+
def undo_move_ui() -> Tuple[str, str, str, str]:
|
| 602 |
+
"""Undo nước đi trước"""
|
| 603 |
+
success, message = game_manager.undo_move()
|
| 604 |
+
|
| 605 |
+
state = game_manager.env.get_state()
|
| 606 |
+
board = draw_game_board(state)
|
| 607 |
+
stats = get_game_stats_html(game_manager)
|
| 608 |
+
history = get_move_history_html(game_manager)
|
| 609 |
+
|
| 610 |
+
return stats, board, message, history
|
| 611 |
+
|
| 612 |
+
def ai_autoplay_ui() -> Tuple[str, str, str, str]:
|
| 613 |
+
"""Bắt đầu AI tự động chơi"""
|
| 614 |
+
success, message = game_manager.start_ai_autoplay()
|
| 615 |
+
|
| 616 |
+
state = game_manager.env.get_state()
|
| 617 |
+
board = draw_game_board(state)
|
| 618 |
+
stats = get_game_stats_html(game_manager)
|
| 619 |
+
history = get_move_history_html(game_manager)
|
| 620 |
+
|
| 621 |
+
return stats, board, message, history
|
| 622 |
+
|
| 623 |
+
def stop_ai_ui() -> Tuple[str, str, str, str]:
|
| 624 |
+
"""Dừng AI"""
|
| 625 |
+
message = game_manager.stop_ai_autoplay()
|
| 626 |
+
|
| 627 |
+
state = game_manager.env.get_state()
|
| 628 |
+
board = draw_game_board(state)
|
| 629 |
+
stats = get_game_stats_html(game_manager)
|
| 630 |
+
history = get_move_history_html(game_manager)
|
| 631 |
+
|
| 632 |
+
return stats, board, message, history
|
| 633 |
+
|
| 634 |
+
def ai_step_loop():
|
| 635 |
+
"""Vòng lặp AI tự động chơi (chạy trong background)"""
|
| 636 |
+
while game_manager.ai_running:
|
| 637 |
+
if game_manager.env.game_finished:
|
| 638 |
+
game_manager.ai_running = False
|
| 639 |
+
break
|
| 640 |
+
|
| 641 |
+
# Lấy nước đi từ AI
|
| 642 |
+
move = game_manager.get_next_move_suggestion()
|
| 643 |
+
|
| 644 |
+
if move is None:
|
| 645 |
+
game_manager.ai_running = False
|
| 646 |
+
break
|
| 647 |
|
| 648 |
+
from_bottle, to_bottle = move
|
| 649 |
+
success, message = game_manager.make_move(from_bottle, to_bottle)
|
| 650 |
+
|
| 651 |
+
if not success:
|
| 652 |
+
game_manager.ai_running = False
|
| 653 |
+
break
|
| 654 |
+
|
| 655 |
+
# Delay để người xem thấy được
|
| 656 |
+
time.sleep(game_manager.ai_delay)
|
| 657 |
+
|
| 658 |
+
game_manager.ai_running = False
|
| 659 |
+
|
| 660 |
+
def update_ai_speed_ui(speed_value: float) -> str:
|
| 661 |
+
"""Cập nhật tốc độ AI"""
|
| 662 |
+
game_manager.set_ai_speed(speed_value)
|
| 663 |
+
return f"⚡ Tốc độ AI: {speed_value:.1f}s/bước"
|
| 664 |
+
|
| 665 |
+
def get_current_state():
|
| 666 |
+
"""Lấy trạng thái hiện tại để refresh UI"""
|
| 667 |
+
state = game_manager.env.get_state()
|
| 668 |
+
|
| 669 |
+
# Highlight nước vừa chơi nếu có
|
| 670 |
+
last_move = None
|
| 671 |
+
if len(game_manager.env.move_history) > 0:
|
| 672 |
+
last_move = game_manager.env.move_history[-1]
|
| 673 |
+
|
| 674 |
+
board = draw_game_board(state, last_move=last_move)
|
| 675 |
+
stats = get_game_stats_html(game_manager)
|
| 676 |
+
history = get_move_history_html(game_manager)
|
| 677 |
+
|
| 678 |
+
if game_manager.env.game_finished:
|
| 679 |
+
message = f"🎉 Hoàn thành trong {game_manager.game_stats['moves_count']} bước!"
|
| 680 |
+
else:
|
| 681 |
+
message = f"📊 Bước hiện tại: {game_manager.game_stats['moves_count']}"
|
| 682 |
+
|
| 683 |
+
return stats, board, message, history
|
| 684 |
|
| 685 |
def create_bottle_buttons():
|
| 686 |
"""Tạo buttons cho các chai"""
|
|
|
|
| 712 |
gr.Markdown("### 🎮 Điều khiển")
|
| 713 |
start_btn = gr.Button("🎮 B���t đầu", variant="primary", size="lg")
|
| 714 |
reset_btn = gr.Button("🔄 Reset", size="lg")
|
| 715 |
+
|
| 716 |
+
with gr.Row():
|
| 717 |
+
undo_btn = gr.Button("↩️ Undo", size="lg", scale=1)
|
| 718 |
+
suggest_btn = gr.Button("💡 Gợi ý", size="lg", scale=1)
|
| 719 |
+
|
| 720 |
+
gr.Markdown("### 🤖 AI Auto Play")
|
| 721 |
+
|
| 722 |
+
ai_speed_slider = gr.Slider(
|
| 723 |
+
minimum=0.1,
|
| 724 |
+
maximum=3.0,
|
| 725 |
+
value=1.0,
|
| 726 |
+
step=0.1,
|
| 727 |
+
label="Tốc độ AI (giây/bước)",
|
| 728 |
+
interactive=True
|
| 729 |
+
)
|
| 730 |
+
speed_status = gr.Textbox(label="Trạng thái tốc độ", value="⚡ Tốc độ AI: 1.0s/bước", interactive=False)
|
| 731 |
+
|
| 732 |
+
with gr.Row():
|
| 733 |
+
ai_play_btn = gr.Button("▶️ AI Tự Chơi", variant="primary", size="lg", scale=1)
|
| 734 |
+
ai_stop_btn = gr.Button("⏸️ Dừng AI", variant="stop", size="lg", scale=1)
|
| 735 |
|
| 736 |
gr.Markdown("### 📊 Thống kê")
|
| 737 |
game_stats = gr.HTML()
|
| 738 |
+
|
| 739 |
+
gr.Markdown("### 📜 Lịch sử nước đi")
|
| 740 |
+
move_history = gr.HTML()
|
| 741 |
|
| 742 |
with gr.Column(scale=2):
|
| 743 |
gr.Markdown("### 🎯 Bảng trò chơi")
|
|
|
|
| 757 |
label="Gợi ý từ AI",
|
| 758 |
interactive=False
|
| 759 |
)
|
| 760 |
+
|
| 761 |
+
# Thêm nút refresh để cập nhật UI khi AI đang chạy
|
| 762 |
+
with gr.Row():
|
| 763 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm")
|
| 764 |
|
| 765 |
# Event handlers
|
| 766 |
load_model_btn.click(
|
|
|
|
| 771 |
|
| 772 |
start_btn.click(
|
| 773 |
fn=start_game_ui,
|
| 774 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 775 |
)
|
| 776 |
|
| 777 |
reset_btn.click(
|
| 778 |
fn=reset_game_ui,
|
| 779 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 780 |
)
|
| 781 |
|
| 782 |
suggest_btn.click(
|
|
|
|
| 784 |
outputs=[suggestion_display, message_display]
|
| 785 |
)
|
| 786 |
|
| 787 |
+
undo_btn.click(
|
| 788 |
+
fn=undo_move_ui,
|
| 789 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
ai_speed_slider.change(
|
| 793 |
+
fn=update_ai_speed_ui,
|
| 794 |
+
inputs=[ai_speed_slider],
|
| 795 |
+
outputs=[speed_status]
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
ai_play_btn.click(
|
| 799 |
+
fn=ai_autoplay_ui,
|
| 800 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 801 |
+
)
|
| 802 |
+
|
| 803 |
+
ai_stop_btn.click(
|
| 804 |
+
fn=stop_ai_ui,
|
| 805 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
refresh_btn.click(
|
| 809 |
+
fn=get_current_state,
|
| 810 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 811 |
+
)
|
| 812 |
+
|
| 813 |
for i, btn in enumerate(bottle_buttons):
|
| 814 |
btn.click(
|
| 815 |
fn=lambda idx=i: bottle_click_ui(idx),
|
| 816 |
+
outputs=[game_stats, game_board, message_display, move_history]
|
| 817 |
)
|
| 818 |
+
|
| 819 |
+
# Timer để tự động refresh khi AI đang chạy
|
| 820 |
+
demo.load(
|
| 821 |
+
fn=get_current_state,
|
| 822 |
+
outputs=[game_stats, game_board, message_display, move_history],
|
| 823 |
+
every=1 # Refresh mỗi 1 giây
|
| 824 |
+
)
|
| 825 |
|
| 826 |
if __name__ == "__main__":
|
| 827 |
+
# Khởi chạy AI thread
|
| 828 |
+
def run_ai_background():
|
| 829 |
+
while True:
|
| 830 |
+
if game_manager.ai_running:
|
| 831 |
+
ai_step_loop()
|
| 832 |
+
time.sleep(0.1)
|
| 833 |
+
|
| 834 |
+
ai_thread = threading.Thread(target=run_ai_background, daemon=True)
|
| 835 |
+
ai_thread.start()
|
| 836 |
+
|
| 837 |
demo.launch(share=True)
|