File size: 31,353 Bytes
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
e3421f8
 
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
 
 
3100167
e3421f8
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
3100167
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
e3421f8
 
3100167
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
 
 
3100167
 
e3421f8
3100167
e3421f8
 
 
 
3100167
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
3100167
e3421f8
 
3100167
 
 
 
e3421f8
 
 
 
 
 
3100167
 
e3421f8
 
 
 
 
 
3100167
 
e3421f8
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
3100167
 
 
e3421f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3100167
 
 
 
e3421f8
 
 
 
 
 
 
 
 
 
3100167
e3421f8
 
 
 
3100167
e3421f8
 
 
 
 
 
 
3100167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3421f8
 
 
3100167
e3421f8
3100167
 
 
 
 
 
 
e3421f8
 
3100167
 
 
 
 
 
 
 
 
 
e3421f8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
import gradio as gr
import numpy as np
import torch
import torch.nn as nn
import os
import json
from pathlib import Path
import random
from typing import List, Tuple, Dict, Optional
from datetime import datetime
import threading
import time

# =============================================================================
# 1. WATER SORT ENVIRONMENT
# =============================================================================

class WaterSortEnv:
    def __init__(self, num_colors=6, bottle_height=4, num_bottles=8):
        self.num_colors = num_colors
        self.bottle_height = bottle_height
        self.num_bottles = num_bottles
        self.bottles = np.zeros((num_bottles, bottle_height), dtype=int)
        self.move_history = []
        self.state_history = []  # Lưu lịch sử các state
        self.game_started = False
        self.game_finished = False
    
    def reset(self) -> np.ndarray:
        """Reset game to solvable initial state"""
        colors = list(range(1, self.num_colors + 1)) * self.bottle_height
        random.shuffle(colors)
        
        self.bottles = np.zeros((self.num_bottles, self.bottle_height), dtype=int)
        color_idx = 0
        
        for i in range(self.num_bottles - 2):
            for j in range(self.bottle_height):
                if color_idx < len(colors):
                    self.bottles[i, self.bottle_height - 1 - j] = colors[color_idx]
                    color_idx += 1
        
        self.move_history = []
        self.state_history = [self.bottles.copy()]  # Lưu state ban đầu
        self.game_started = True
        self.game_finished = False
        return self.get_state()
    
    def get_state(self) -> np.ndarray:
        return self.bottles.copy()
    
    def get_valid_moves(self) -> List[Tuple[int, int]]:
        """Get all valid moves"""
        valid_moves = []
        for from_idx in range(self.num_bottles):
            for to_idx in range(self.num_bottles):
                if from_idx != to_idx and self._is_valid_move(from_idx, to_idx):
                    valid_moves.append((from_idx, to_idx))
        return valid_moves
    
    def _is_valid_move(self, from_idx: int, to_idx: int) -> bool:
        """Check if move is valid"""
        from_bottle = self.bottles[from_idx]
        to_bottle = self.bottles[to_idx]
        
        if np.sum(from_bottle > 0) == 0:
            return False
        if np.sum(to_bottle > 0) == self.bottle_height:
            return False
        
        source_top_idx = np.where(from_bottle > 0)[0]
        if len(source_top_idx) == 0:
            return False
        source_top_color = from_bottle[source_top_idx[0]]
        
        dest_top_idx = np.where(to_bottle > 0)[0]
        if len(dest_top_idx) == 0:
            return True
        dest_top_color = to_bottle[dest_top_idx[0]]
        
        return source_top_color == dest_top_color
    
    def step(self, action: Tuple[int, int]):
        """Execute move"""
        from_idx, to_idx = action
        
        if not self._is_valid_move(from_idx, to_idx):
            return self.get_state(), -1, False
        
        self._pour_liquid(from_idx, to_idx)
        self.move_history.append((from_idx, to_idx))
        self.state_history.append(self.bottles.copy())  # Lưu state sau mỗi nước
        
        done = self.is_solved()
        
        if done:
            self.game_finished = True
        
        reward = 10.0 if done else 0.1
        return self.get_state(), reward, done
    
    def undo_last_move(self) -> bool:
        """Quay lại nước đi trước đó"""
        if len(self.state_history) <= 1:  # Chỉ còn state ban đầu
            return False
        
        # Xóa state hiện tại
        self.state_history.pop()
        self.move_history.pop()
        
        # Khôi phục state trước đó
        self.bottles = self.state_history[-1].copy()
        self.game_finished = False
        
        return True
    
    def _pour_liquid(self, from_idx: int, to_idx: int):
        """Pour liquid from one bottle to another"""
        from_bottle = self.bottles[from_idx]
        to_bottle = self.bottles[to_idx]
        
        source_non_empty = np.where(from_bottle > 0)[0]
        if len(source_non_empty) == 0:
            return
        
        source_top_idx = source_non_empty[0]
        source_color = from_bottle[source_top_idx]
        
        pour_amount = 1
        for i in range(source_top_idx + 1, len(from_bottle)):
            if from_bottle[i] == source_color:
                pour_amount += 1
            else:
                break
        
        dest_empty = np.where(to_bottle == 0)[0]
        if len(dest_empty) == 0:
            return
        
        available_space = len(dest_empty)
        actual_pour = min(pour_amount, available_space)
        
        for i in range(actual_pour):
            from_pos = source_top_idx + i
            to_pos = dest_empty[-(i+1)]
            self.bottles[to_idx, to_pos] = source_color
            self.bottles[from_idx, from_pos] = 0
    
    def is_solved(self) -> bool:
        """Check if puzzle is solved"""
        for bottle in self.bottles:
            unique_colors = np.unique(bottle[bottle > 0])
            if len(unique_colors) > 1:
                return False
            if len(unique_colors) == 1 and np.sum(bottle > 0) != self.bottle_height and np.sum(bottle > 0) != 0:
                return False
        return True

# =============================================================================
# 2. NEURAL NETWORK ARCHITECTURE
# =============================================================================

class WaterSortNet(nn.Module):
    def __init__(self, num_bottles=8, bottle_height=4, num_colors=6):
        super(WaterSortNet, self).__init__()
        self.num_bottles = num_bottles
        self.bottle_height = bottle_height
        self.num_colors = num_colors
        
        self.conv1 = nn.Conv2d(num_colors, 128, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        
        self.bn1 = nn.BatchNorm2d(128)
        self.bn2 = nn.BatchNorm2d(128)
        self.bn3 = nn.BatchNorm2d(128)
        self.bn4 = nn.BatchNorm2d(128)
        
        self.policy_conv = nn.Conv2d(128, 64, kernel_size=3, padding=1)
        self.policy_fc1 = nn.Linear(64 * num_bottles * bottle_height, 512)
        self.policy_fc2 = nn.Linear(512, num_bottles * num_bottles)
        self.policy_bn = nn.BatchNorm1d(512)
        
        self.value_conv = nn.Conv2d(128, 64, kernel_size=3, padding=1)
        self.value_fc1 = nn.Linear(64 * num_bottles * bottle_height, 512)
        self.value_fc2 = nn.Linear(512, 256)
        self.value_fc3 = nn.Linear(256, 1)
        self.value_bn1 = nn.BatchNorm1d(512)
        self.value_bn2 = nn.BatchNorm1d(256)
        
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.3)
    
    def forward(self, x):
        batch_size = x.size(0)
        
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.relu(self.bn2(self.conv2(x)))
        x = self.relu(self.bn3(self.conv3(x)))
        x = self.relu(self.bn4(self.conv4(x)))
        
        policy = self.relu(self.policy_conv(x))
        policy = policy.view(batch_size, -1)
        policy = self.dropout(self.relu(self.policy_bn(self.policy_fc1(policy))))
        policy = self.policy_fc2(policy)
        
        value = self.relu(self.value_conv(x))
        value = value.view(batch_size, -1)
        value = self.dropout(self.relu(self.value_bn1(self.value_fc1(value))))
        value = self.dropout(self.relu(self.value_bn2(self.value_fc2(value))))
        value = torch.tanh(self.value_fc3(value))
        
        return policy, value

# =============================================================================
# 3. DATA PROCESSOR
# =============================================================================

class DataProcessor:
    def __init__(self, num_bottles=8, bottle_height=4, num_colors=6):
        self.num_bottles = num_bottles
        self.bottle_height = bottle_height
        self.num_colors = num_colors
    
    def state_to_tensor(self, state):
        """Chuyển state thành one-hot encoded tensor"""
        one_hot = np.zeros((self.num_colors, self.num_bottles, self.bottle_height), dtype=np.float32)
        
        for bottle_idx in range(self.num_bottles):
            for height_idx in range(self.bottle_height):
                color = int(state[bottle_idx, height_idx])
                if color > 0:
                    one_hot[color - 1, bottle_idx, height_idx] = 1.0
        
        return torch.from_numpy(one_hot)

# =============================================================================
# 4. GAME STATE MANAGER
# =============================================================================

class GameStateManager:
    def __init__(self):
        self.env = WaterSortEnv()
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.model = None
        self.processor = DataProcessor()
        self.current_model_name = None
        self.ai_running = False
        self.ai_thread = None
        self.ai_delay = 1.0  # Delay giữa các nước (giây)
        self.selected_bottles = None
        self.game_stats = {
            'moves_count': 0,
            'start_time': None,
            'ai_suggested_move': None,
            'valid_moves': []
        }
    
    def load_model(self, model_path: str) -> bool:
        """Load model từ file"""
        try:
            self.model = WaterSortNet(num_bottles=8, bottle_height=4, num_colors=6).to(self.device)
            checkpoint = torch.load(model_path, map_location=self.device)
            
            if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
                self.model.load_state_dict(checkpoint['model_state_dict'])
            else:
                self.model.load_state_dict(checkpoint)
            
            self.model.eval()
            self.current_model_name = Path(model_path).stem
            return True
        except Exception as e:
            print(f"Error loading model: {e}")
            return False
    
    def start_game(self):
        """Bắt đầu game mới"""
        self.env.reset()
        self.game_stats = {
            'moves_count': 0,
            'start_time': datetime.now(),
            'ai_suggested_move': None,
            'valid_moves': self.env.get_valid_moves()
        }
        self.selected_bottles = None
        self.ai_running = False
        return self.env.get_state()
    
    def reset_game(self):
        """Reset game"""
        self.ai_running = False
        self.env.game_finished = False
        return self.start_game()
    
    def undo_move(self) -> Tuple[bool, str]:
        """Quay lại nước đi trước"""
        if self.ai_running:
            return False, "❌ Không thể undo khi AI đang chơi!"
        
        success = self.env.undo_last_move()
        if success:
            self.game_stats['moves_count'] = len(self.env.move_history)
            self.game_stats['valid_moves'] = self.env.get_valid_moves()
            return True, f"✅ Đã quay lại! Tổng bước: {self.game_stats['moves_count']}"
        else:
            return False, "❌ Không thể quay lại thêm!"
    
    def get_next_move_suggestion(self) -> Optional[Tuple[int, int]]:
        """Lấy gợi ý từ AI"""
        if self.model is None:
            return None
        
        try:
            state = self.env.get_state()
            valid_moves = self.env.get_valid_moves()
            
            if not valid_moves:
                return None
            
            state_tensor = self.processor.state_to_tensor(state).unsqueeze(0).to(self.device)
            
            with torch.no_grad():
                policy, _ = self.model(state_tensor)
            
            policy_probs = torch.softmax(policy, dim=1).cpu().numpy()[0]
            
            best_move = None
            best_score = -float('inf')
            
            for move in valid_moves:
                from_idx, to_idx = move
                move_index = from_idx * 8 + to_idx
                score = policy_probs[move_index]
                
                if score > best_score:
                    best_score = score
                    best_move = move
            
            self.game_stats['ai_suggested_move'] = best_move
            return best_move
        except Exception as e:
            print(f"Error getting suggestion: {e}")
            return None
    
    def make_move(self, from_bottle: int, to_bottle: int) -> Tuple[bool, str]:
        """Thực hiện di chuyển"""
        state, reward, done = self.env.step((from_bottle, to_bottle))
        
        if reward < 0:
            return False, "Nước không thể đổ vào chai này!"
        
        self.game_stats['moves_count'] += 1
        self.game_stats['valid_moves'] = self.env.get_valid_moves()
        self.selected_bottles = None
        
        if done:
            return True, f"Chúc mừng! Bạn đã giải xong trong {self.game_stats['moves_count']} bước!"
        
        return True, f"Bước thành công! Tổng bước: {self.game_stats['moves_count']}"
    
    def select_bottle(self, bottle_idx: int) -> str:
        """Chọn chai"""
        if self.selected_bottles is None:
            self.selected_bottles = bottle_idx
            return f"Đã chọn chai {bottle_idx}. Chọn chai đích."
        else:
            from_bottle = self.selected_bottles
            to_bottle = bottle_idx
            success, message = self.make_move(from_bottle, to_bottle)
            return message
    
    def start_ai_autoplay(self):
        """Bắt đầu AI tự động chơi"""
        if self.ai_running:
            return False, "AI đã đang chạy!"
        
        if self.model is None:
            return False, "Vui lòng tải model trước!"
        
        if self.env.game_finished:
            return False, "Game đã kết thúc!"
        
        self.ai_running = True
        return True, "✅ AI bắt đầu tự động chơi!"
    
    def stop_ai_autoplay(self):
        """Dừng AI tự động chơi"""
        self.ai_running = False
        return "⏸️ AI đã dừng!"
    
    def set_ai_speed(self, delay: float):
        """Đặt tốc độ AI (delay giữa các nước)"""
        self.ai_delay = delay

# =============================================================================
# 5. VISUALIZATION
# =============================================================================

def draw_game_board(state: np.ndarray, selected_bottle: Optional[int] = None, 
                    last_move: Optional[Tuple[int, int]] = None) -> str:
    """Vẽ bảng game dưới dạng HTML"""
    colors_map = {
        0: '#ffffff',
        1: '#FF6B6B',
        2: '#4ECDC4',
        3: '#45B7D1',
        4: '#FFA07A',
        5: '#98D8C8',
        6: '#F7DC6F'
    }
    
    html = '<div style="display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; padding: 20px;">'
    
    num_bottles = state.shape[0]
    bottle_height = state.shape[1]
    
    for bottle_idx in range(num_bottles):
        bottle = state[bottle_idx]
        is_selected = bottle_idx == selected_bottle
        
        # Highlight nếu là nước vừa chơi
        is_last_move = False
        if last_move:
            is_last_move = bottle_idx in last_move
        
        border_color = '#FFD700' if is_selected else ('#00FF00' if is_last_move else '#333')
        border_width = '3' if (is_selected or is_last_move) else '2'
        
        html += f'<div style="text-align: center; margin: 10px;">'
        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;">'
        
        for height_idx in range(bottle_height):
            color_val = int(bottle[height_idx])
            color = colors_map.get(color_val, '#ffffff')
            html += f'<div style="width: 100%; height: 30px; background: {color}; border-bottom: 1px solid #999;"></div>'
        
        html += '</div>'
        html += f'<p style="margin: 5px 0; font-weight: bold;">Chai {bottle_idx}</p>'
        html += '</div>'
    
    html += '</div>'
    return html

def get_game_stats_html(game_manager: GameStateManager) -> str:
    """Tạo HTML hiển thị thống kê game"""
    stats = game_manager.game_stats
    model_info = game_manager.current_model_name if game_manager.current_model_name else "Chưa tải"
    
    ai_status = "🤖 Đang chơi..." if game_manager.ai_running else "⏸️ Dừng"
    history_count = len(game_manager.env.move_history)
    
    html = f"""
    <div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 10px 0;">
        <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;">
            <div>
                <p style="margin: 0; font-size: 12px; color: #666;">📊 Số bước</p>
                <p style="margin: 5px 0; font-size: 24px; font-weight: bold;">{stats['moves_count']}</p>
            </div>
            <div>
                <p style="margin: 0; font-size: 12px; color: #666;">🤖 Model</p>
                <p style="margin: 5px 0; font-size: 14px; font-weight: bold;">{model_info}</p>
            </div>
            <div>
                <p style="margin: 0; font-size: 12px; color: #666;">💾 Thiết bị</p>
                <p style="margin: 5px 0; font-size: 14px; font-weight: bold;">{'GPU' if torch.cuda.is_available() else 'CPU'}</p>
            </div>
        </div>
        <div style="margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
            <div>
                <p style="margin: 0; font-size: 12px; color: #666;">📜 Lịch sử</p>
                <p style="margin: 5px 0; font-size: 16px; font-weight: bold;">{history_count} nước đi</p>
            </div>
            <div>
                <p style="margin: 0; font-size: 12px; color: #666;">🎮 Trạng thái AI</p>
                <p style="margin: 5px 0; font-size: 16px; font-weight: bold;">{ai_status}</p>
            </div>
        </div>
        <p style="margin: 10px 0; font-size: 12px; color: #999;">
            Nước hợp lệ: {len(stats['valid_moves'])} nước
        </p>
    </div>
    """
    return html

def get_move_history_html(game_manager: GameStateManager) -> str:
    """Hiển thị lịch sử các nước đi"""
    moves = game_manager.env.move_history
    
    if not moves:
        return "<p style='text-align: center; color: #999;'>Chưa có nước đi nào</p>"
    
    html = "<div style='max-height: 300px; overflow-y: auto; padding: 10px;'>"
    html += "<table style='width: 100%; border-collapse: collapse;'>"
    html += "<tr style='background: #f0f0f0; font-weight: bold;'>"
    html += "<th style='padding: 8px; border: 1px solid #ddd;'>Bước</th>"
    html += "<th style='padding: 8px; border: 1px solid #ddd;'>Từ chai</th>"
    html += "<th style='padding: 8px; border: 1px solid #ddd;'>Đến chai</th>"
    html += "</tr>"
    
    for i, (from_idx, to_idx) in enumerate(moves, 1):
        html += f"<tr style='background: {'#fff' if i % 2 == 0 else '#f9f9f9'};'>"
        html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{i}</td>"
        html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{from_idx}</td>"
        html += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>{to_idx}</td>"
        html += "</tr>"
    
    html += "</table></div>"
    return html

# =============================================================================
# 6. MAIN GRADIO APP
# =============================================================================

def get_available_models() -> List[str]:
    """Lấy danh sách model từ folder models"""
    models_dir = Path("models")
    models_dir.mkdir(exist_ok=True)
    
    model_files = list(models_dir.glob("*.pth"))
    return [f.name for f in sorted(model_files)]

# Initialize global game manager
game_manager = GameStateManager()

def load_model_ui(selected_model: str) -> Tuple[str, str]:
    """Load model từ UI"""
    if not selected_model:
        return "❌ Vui lòng chọn model", ""
    
    model_path = Path("models") / selected_model
    if game_manager.load_model(str(model_path)):
        return f"✅ Tải model thành công: {selected_model}", draw_game_board(np.zeros((8, 4)))
    else:
        return f"❌ Lỗi khi tải model: {selected_model}", ""

def start_game_ui() -> Tuple[str, str, str, str]:
    """Bắt đầu game mới"""
    state = game_manager.start_game()
    board = draw_game_board(state)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    return stats, board, "✅ Bắt đầu game mới!", history

def reset_game_ui() -> Tuple[str, str, str, str]:
    """Reset game"""
    state = game_manager.reset_game()
    board = draw_game_board(state)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    return stats, board, "🔄 Game đã được reset!", history

def suggest_move_ui() -> Tuple[str, str]:
    """Gợi ý di chuyển từ AI"""
    if game_manager.model is None:
        return "", "❌ Vui lòng tải model trước!"
    
    if game_manager.env.game_finished:
        return "", "🎉 Game đã kết thúc!"
    
    move = game_manager.get_next_move_suggestion()
    if move:
        from_bottle, to_bottle = move
        message = f"💡 Gợi ý: Đổ từ chai {from_bottle} sang chai {to_bottle}"
        return message, message
    else:
        return "", "❌ Không thể tìm được nước gợi ý"

def bottle_click_ui(bottle_idx: int) -> Tuple[str, str, str, str]:
    """Xử lý click bottle"""
    if not game_manager.env.game_started:
        return "", draw_game_board(game_manager.env.get_state()), "❌ Vui lòng bắt đầu game!", ""
    
    if game_manager.env.game_finished:
        return "", draw_game_board(game_manager.env.get_state()), "🎉 Game đã kết thúc!", ""
    
    if game_manager.ai_running:
        return "", draw_game_board(game_manager.env.get_state()), "❌ AI đang chơi, không thể thao tác!", ""
    
    if game_manager.selected_bottles is None:
        game_manager.selected_bottles = bottle_idx
        state = game_manager.env.get_state()
        board = draw_game_board(state, bottle_idx)
        stats = get_game_stats_html(game_manager)
        history = get_move_history_html(game_manager)
        return stats, board, f"✓ Chọn chai {bottle_idx}. Chọn chai đích.", history
    else:
        from_bottle = game_manager.selected_bottles
        to_bottle = bottle_idx
        success, message = game_manager.make_move(from_bottle, to_bottle)
        
        state = game_manager.env.get_state()
        last_move = (from_bottle, to_bottle) if success else None
        board = draw_game_board(state, last_move=last_move)
        stats = get_game_stats_html(game_manager)
        history = get_move_history_html(game_manager)
        
        return stats, board, message, history

def undo_move_ui() -> Tuple[str, str, str, str]:
    """Undo nước đi trước"""
    success, message = game_manager.undo_move()
    
    state = game_manager.env.get_state()
    board = draw_game_board(state)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    
    return stats, board, message, history

def ai_autoplay_ui() -> Tuple[str, str, str, str]:
    """Bắt đầu AI tự động chơi"""
    success, message = game_manager.start_ai_autoplay()
    
    state = game_manager.env.get_state()
    board = draw_game_board(state)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    
    return stats, board, message, history

def stop_ai_ui() -> Tuple[str, str, str, str]:
    """Dừng AI"""
    message = game_manager.stop_ai_autoplay()
    
    state = game_manager.env.get_state()
    board = draw_game_board(state)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    
    return stats, board, message, history

def ai_step_loop():
    """Vòng lặp AI tự động chơi (chạy trong background)"""
    while game_manager.ai_running:
        if game_manager.env.game_finished:
            game_manager.ai_running = False
            break
        
        # Lấy nước đi từ AI
        move = game_manager.get_next_move_suggestion()
        
        if move is None:
            game_manager.ai_running = False
            break
        
        from_bottle, to_bottle = move
        success, message = game_manager.make_move(from_bottle, to_bottle)
        
        if not success:
            game_manager.ai_running = False
            break
        
        # Delay để người xem thấy được
        time.sleep(game_manager.ai_delay)
    
    game_manager.ai_running = False

def update_ai_speed_ui(speed_value: float) -> str:
    """Cập nhật tốc độ AI"""
    game_manager.set_ai_speed(speed_value)
    return f"⚡ Tốc độ AI: {speed_value:.1f}s/bước"

def get_current_state():
    """Lấy trạng thái hiện tại để refresh UI"""
    state = game_manager.env.get_state()
    
    # Highlight nước vừa chơi nếu có
    last_move = None
    if len(game_manager.env.move_history) > 0:
        last_move = game_manager.env.move_history[-1]
    
    board = draw_game_board(state, last_move=last_move)
    stats = get_game_stats_html(game_manager)
    history = get_move_history_html(game_manager)
    
    if game_manager.env.game_finished:
        message = f"🎉 Hoàn thành trong {game_manager.game_stats['moves_count']} bước!"
    else:
        message = f"📊 Bước hiện tại: {game_manager.game_stats['moves_count']}"
    
    return stats, board, message, history

def create_bottle_buttons():
    """Tạo buttons cho các chai"""
    buttons = []
    for i in range(8):
        buttons.append(
            gr.Button(f"Chai {i}", size="lg", scale=1)
        )
    return buttons

# Create Gradio Interface
with gr.Blocks(title="Water Sort Puzzle", theme=gr.themes.Soft()) as demo:
    gr.Markdown("# 🧪 Water Sort Puzzle Solver")
    gr.Markdown("Giải Water Sort Puzzle với sự trợ giúp của AI!")
    
    with gr.Row():
        with gr.Column(scale=1):
            gr.Markdown("### ⚙️ Cấu hình")
            
            model_dropdown = gr.Dropdown(
                label="Chọn Model",
                choices=get_available_models(),
                interactive=True
            )
            
            load_model_btn = gr.Button("📥 Tải Model", variant="primary", size="lg")
            model_status = gr.Textbox(label="Trạng thái", interactive=False)
            
            gr.Markdown("### 🎮 Điều khiển")
            start_btn = gr.Button("🎮 Bắt đầu", variant="primary", size="lg")
            reset_btn = gr.Button("🔄 Reset", size="lg")
            
            with gr.Row():
                undo_btn = gr.Button("↩️ Undo", size="lg", scale=1)
                suggest_btn = gr.Button("💡 Gợi ý", size="lg", scale=1)
            
            gr.Markdown("### 🤖 AI Auto Play")
            
            ai_speed_slider = gr.Slider(
                minimum=0.1,
                maximum=3.0,
                value=1.0,
                step=0.1,
                label="Tốc độ AI (giây/bước)",
                interactive=True
            )
            speed_status = gr.Textbox(label="Trạng thái tốc độ", value="⚡ Tốc độ AI: 1.0s/bước", interactive=False)
            
            with gr.Row():
                ai_play_btn = gr.Button("▶️ AI Tự Chơi", variant="primary", size="lg", scale=1)
                ai_stop_btn = gr.Button("⏸️ Dừng AI", variant="stop", size="lg", scale=1)
            
            gr.Markdown("### 📊 Thống kê")
            game_stats = gr.HTML()
            
            gr.Markdown("### 📜 Lịch sử nước đi")
            move_history = gr.HTML()
        
        with gr.Column(scale=2):
            gr.Markdown("### 🎯 Bảng trò chơi")
            game_board = gr.HTML()
            
            gr.Markdown("### Chọn chai để di chuyển")
            with gr.Row():
                bottle_buttons = create_bottle_buttons()
            
            message_display = gr.Textbox(
                label="Thông báo",
                interactive=False,
                lines=2
            )
            
            suggestion_display = gr.Textbox(
                label="Gợi ý từ AI",
                interactive=False
            )
            
            # Thêm nút refresh để cập nhật UI khi AI đang chạy
            with gr.Row():
                refresh_btn = gr.Button("🔄 Refresh", size="sm")
    
    # Event handlers
    load_model_btn.click(
        fn=load_model_ui,
        inputs=[model_dropdown],
        outputs=[model_status, game_board]
    )
    
    start_btn.click(
        fn=start_game_ui,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    reset_btn.click(
        fn=reset_game_ui,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    suggest_btn.click(
        fn=suggest_move_ui,
        outputs=[suggestion_display, message_display]
    )
    
    undo_btn.click(
        fn=undo_move_ui,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    ai_speed_slider.change(
        fn=update_ai_speed_ui,
        inputs=[ai_speed_slider],
        outputs=[speed_status]
    )
    
    ai_play_btn.click(
        fn=ai_autoplay_ui,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    ai_stop_btn.click(
        fn=stop_ai_ui,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    refresh_btn.click(
        fn=get_current_state,
        outputs=[game_stats, game_board, message_display, move_history]
    )
    
    for i, btn in enumerate(bottle_buttons):
        btn.click(
            fn=lambda idx=i: bottle_click_ui(idx),
            outputs=[game_stats, game_board, message_display, move_history]
        )
    
    # Timer để tự động refresh khi AI đang chạy
    demo.load(
        fn=get_current_state,
        outputs=[game_stats, game_board, message_display, move_history],
        every=1  # Refresh mỗi 1 giây
    )

if __name__ == "__main__":
    # Khởi chạy AI thread
    def run_ai_background():
        while True:
            if game_manager.ai_running:
                ai_step_loop()
            time.sleep(0.1)
    
    ai_thread = threading.Thread(target=run_ai_background, daemon=True)
    ai_thread.start()
    
    demo.launch(share=True)