khanhromvn commited on
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.

Files changed (1) hide show
  1. app.py +283 -17
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) -> str:
 
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
- 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])
@@ -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
- 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"""
@@ -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
- 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"""
@@ -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
- 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")
@@ -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)