ncn2569 commited on
Commit
94cea3e
·
1 Parent(s): 09d76f8

add app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chess AI - Hugging Face Spaces
3
+ Entry point: app.py
4
+ """
5
+
6
+ import streamlit as st
7
+ import chess
8
+ import chess.svg
9
+ import base64
10
+
11
+ st.set_page_config(
12
+ page_title="Chess AI",
13
+ page_icon="♟️",
14
+ layout="centered",
15
+ initial_sidebar_state="expanded",
16
+ )
17
+
18
+ st.markdown("""
19
+ <style>
20
+ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=IBM+Plex+Mono:wght@400;500&display=swap');
21
+
22
+ html, body, [class*="css"] {
23
+ font-family: 'IBM Plex Mono', monospace;
24
+ background-color: #0f0f0f;
25
+ color: #e8e0d0;
26
+ }
27
+ h1, h2, h3 { font-family: 'Playfair Display', serif; color: #f0c060; }
28
+ .stButton > button {
29
+ background: #1a1a1a; color: #f0c060;
30
+ border: 1px solid #f0c060; border-radius: 2px;
31
+ font-family: 'IBM Plex Mono', monospace; font-size: 13px;
32
+ transition: all 0.2s;
33
+ }
34
+ .stButton > button:hover { background: #f0c060; color: #0f0f0f; }
35
+ .move-log {
36
+ background: #1a1a1a; border: 1px solid #333; border-radius: 4px;
37
+ padding: 10px; height: 200px; overflow-y: auto;
38
+ font-size: 12px; color: #aaa; font-family: 'IBM Plex Mono', monospace;
39
+ }
40
+ .status-bar {
41
+ background: #1a1a1a; border-left: 3px solid #f0c060;
42
+ padding: 8px 14px; margin: 10px 0; font-size: 13px;
43
+ }
44
+ .info-chip {
45
+ display: inline-block; background: #222; border: 1px solid #444;
46
+ border-radius: 2px; padding: 2px 8px; font-size: 11px; margin: 2px;
47
+ }
48
+ </style>
49
+ """, unsafe_allow_html=True)
50
+
51
+
52
+ # ─── Cached resource loading ─────────────────────────────────────────────────
53
+ @st.cache_resource(show_spinner="Đang tải Chess Engine...")
54
+ def load_engine():
55
+ from src import config, engine
56
+ return config, engine
57
+
58
+ @st.cache_resource(show_spinner="Đang tải ML Model...")
59
+ def load_ml_agent():
60
+ try:
61
+ from src.ai_battle import MLAgent
62
+ return MLAgent(), None
63
+ except Exception as e:
64
+ return None, str(e)
65
+
66
+
67
+ # ─── Session state ────────────────────────────────────────────────────────────
68
+ def init_state():
69
+ defaults = {
70
+ "board": chess.Board(),
71
+ "move_log": [],
72
+ "game_over": False,
73
+ "status_msg": "Lượt của bạn ♙",
74
+ "depth": 2,
75
+ "game_mode": "human_vs_minimax",
76
+ }
77
+ for k, v in defaults.items():
78
+ if k not in st.session_state:
79
+ st.session_state[k] = v
80
+
81
+ init_state()
82
+
83
+
84
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
85
+ def board_to_html(board: chess.Board, last_move: chess.Move = None) -> str:
86
+ arrows = []
87
+ if last_move:
88
+ arrows.append(chess.svg.Arrow(last_move.from_square, last_move.to_square, color="#f0c06088"))
89
+ svg = chess.svg.board(
90
+ board, size=460, arrows=arrows,
91
+ colors={
92
+ "square light": "#ede0c8",
93
+ "square dark": "#b58863",
94
+ "square light lastmove": "#cdd16e",
95
+ "square dark lastmove": "#aaa23a",
96
+ }
97
+ )
98
+ b64 = base64.b64encode(svg.encode()).decode()
99
+ return f'<img src="data:image/svg+xml;base64,{b64}" width="460"/>'
100
+
101
+ def get_last_move(board):
102
+ try:
103
+ return board.peek()
104
+ except IndexError:
105
+ return None
106
+
107
+ def reset_game():
108
+ st.session_state.board = chess.Board()
109
+ st.session_state.move_log = []
110
+ st.session_state.game_over = False
111
+ st.session_state.status_msg = "Lượt của bạn ♙"
112
+
113
+ def check_game_over(board: chess.Board) -> bool:
114
+ if board.is_checkmate():
115
+ winner = "Đen" if board.turn == chess.WHITE else "Trắng"
116
+ st.session_state.status_msg = f"♚ Chiếu hết! {winner} thắng!"
117
+ st.session_state.game_over = True
118
+ elif board.is_stalemate():
119
+ st.session_state.status_msg = "🤝 Hòa (Stalemate)"
120
+ st.session_state.game_over = True
121
+ elif board.is_insufficient_material():
122
+ st.session_state.status_msg = "🤝 Hòa (thiếu quân)"
123
+ st.session_state.game_over = True
124
+ elif board.is_seventyfive_moves():
125
+ st.session_state.status_msg = "🤝 Hòa (75 nước)"
126
+ st.session_state.game_over = True
127
+ return st.session_state.game_over
128
+
129
+ def do_minimax_move(board: chess.Board):
130
+ config, engine = load_engine()
131
+ move = engine.find_best_move(board, config.STANDARD_WEIGHTS, st.session_state.depth)
132
+ if move and move in board.legal_moves:
133
+ st.session_state.move_log.append(f"Minimax (d{st.session_state.depth}): {move.uci()}")
134
+ board.push(move)
135
+ if not check_game_over(board):
136
+ st.session_state.status_msg = "Lượt của bạn ♙"
137
+
138
+ def do_ml_move(board: chess.Board):
139
+ ml_agent, err = load_ml_agent()
140
+ if err:
141
+ st.error(f"ML Agent lỗi: {err}")
142
+ return
143
+ move = ml_agent.get_move(board, time_limit=15.0)
144
+ if move and move in board.legal_moves:
145
+ st.session_state.move_log.append(f"ML Agent: {move.uci()}")
146
+ board.push(move)
147
+ if not check_game_over(board):
148
+ st.session_state.status_msg = "Lượt của bạn ♙"
149
+ else:
150
+ import random
151
+ legal = list(board.legal_moves)
152
+ if legal:
153
+ fb = random.choice(legal)
154
+ board.push(fb)
155
+ st.session_state.move_log.append(f"ML Agent (fallback): {fb.uci()}")
156
+ check_game_over(board)
157
+
158
+
159
+ # ─── Sidebar ──────────────────────────────────────────────────────────────────
160
+ with st.sidebar:
161
+ st.markdown("## ♟️ Chess AI")
162
+ st.markdown("---")
163
+
164
+ new_mode = st.radio(
165
+ "Chế độ chơi",
166
+ ["Người vs Minimax", "Người vs ML Agent"],
167
+ )
168
+ new_mode_key = "human_vs_minimax" if "Minimax" in new_mode else "human_vs_ml"
169
+
170
+ # Reset tự động nếu đổi chế độ
171
+ if new_mode_key != st.session_state.game_mode:
172
+ st.session_state.game_mode = new_mode_key
173
+ reset_game()
174
+
175
+ if new_mode_key == "human_vs_minimax":
176
+ st.session_state.depth = st.slider("Độ sâu Minimax", 1, 4, 2)
177
+ st.caption("Depth 1-2: nhanh · Depth 3-4: mạnh hơn nhưng chậm")
178
+
179
+ st.markdown("---")
180
+ if st.button("🔄 Ván mới"):
181
+ reset_game()
182
+ st.rerun()
183
+
184
+ st.markdown("---")
185
+ st.markdown("**Hướng dẫn**")
186
+ st.markdown("Chọn ô nguồn → ô đích → nhấn **Đi** \nPhong cấp tự động lên Hậu")
187
+ st.markdown("---")
188
+ st.markdown(
189
+ '<a href="https://github.com/ncn2569/Chess-game-with-AI-and-ML" '
190
+ 'target="_blank">📂 GitHub</a>',
191
+ unsafe_allow_html=True,
192
+ )
193
+
194
+
195
+ # ─── Main ─────────────────────────────────────────────────────────────────────
196
+ st.markdown("# Chess AI")
197
+
198
+ board = st.session_state.board
199
+ col_board, col_ctrl = st.columns([3, 2], gap="medium")
200
+
201
+ with col_board:
202
+ st.markdown(board_to_html(board, get_last_move(board)), unsafe_allow_html=True)
203
+ st.markdown(
204
+ f'<div class="status-bar">{st.session_state.status_msg}</div>',
205
+ unsafe_allow_html=True,
206
+ )
207
+ if board.is_check() and not st.session_state.game_over:
208
+ st.warning("⚠️ Chiếu!")
209
+
210
+ with col_ctrl:
211
+ # ── Input nước đi (người luôn chơi Trắng)
212
+ if not st.session_state.game_over and board.turn == chess.WHITE:
213
+ st.markdown("### Nước đi của bạn")
214
+
215
+ legal_from = sorted({
216
+ chess.SQUARE_NAMES[m.from_square] for m in board.legal_moves
217
+ })
218
+ from_sq = st.selectbox("Từ ô", [""] + legal_from, key="from_sq")
219
+
220
+ valid_to = []
221
+ if from_sq:
222
+ from_idx = chess.parse_square(from_sq)
223
+ valid_to = sorted({
224
+ chess.SQUARE_NAMES[m.to_square]
225
+ for m in board.legal_moves if m.from_square == from_idx
226
+ })
227
+ to_sq = st.selectbox("Đến ô", [""] + valid_to, key="to_sq")
228
+
229
+ if st.button("▶ Đi", disabled=not (from_sq and to_sq)):
230
+ uci = f"{from_sq}{to_sq}"
231
+ piece = board.piece_at(chess.parse_square(from_sq))
232
+ if piece and piece.piece_type == chess.PAWN:
233
+ if (board.turn == chess.WHITE and to_sq[1] == "8") or \
234
+ (board.turn == chess.BLACK and to_sq[1] == "1"):
235
+ uci += "q"
236
+ try:
237
+ move = chess.Move.from_uci(uci)
238
+ if move in board.legal_moves:
239
+ st.session_state.move_log.append(f"Bạn: {uci}")
240
+ board.push(move)
241
+ if not check_game_over(board):
242
+ st.session_state.status_msg = "AI đang suy nghĩ..."
243
+ st.rerun()
244
+ else:
245
+ st.error("Nước đi không hợp lệ!")
246
+ except Exception as e:
247
+ st.error(f"Lỗi: {e}")
248
+
249
+ # ── AI tự động đi khi đến lượt Đen
250
+ if not st.session_state.game_over and board.turn == chess.BLACK:
251
+ with st.spinner("AI đang suy nghĩ..."):
252
+ if st.session_state.game_mode == "human_vs_minimax":
253
+ do_minimax_move(board)
254
+ else:
255
+ do_ml_move(board)
256
+ st.rerun()
257
+
258
+ # ── Lịch sử
259
+ st.markdown("---")
260
+ st.markdown("### Lịch sử")
261
+ log_html = "<br>".join(st.session_state.move_log[-30:][::-1]) or "<i>Chưa có</i>"
262
+ st.markdown(f'<div class="move-log">{log_html}</div>', unsafe_allow_html=True)
263
+
264
+ # ── Thông tin quân còn lại
265
+ st.markdown("---")
266
+ for name, pt in [("♙Tốt", chess.PAWN), ("♘Mã", chess.KNIGHT),
267
+ ("♗Tượng", chess.BISHOP), ("♖Xe", chess.ROOK), ("♛Hậu", chess.QUEEN)]:
268
+ w = len(board.pieces(pt, chess.WHITE))
269
+ b = len(board.pieces(pt, chess.BLACK))
270
+ st.markdown(
271
+ f'<span class="info-chip">{name} ⬜{w} ⬛{b}</span>',
272
+ unsafe_allow_html=True,
273
+ )
274
+ st.markdown(
275
+ f'<span class="info-chip">Nước #{board.fullmove_number}</span>',
276
+ unsafe_allow_html=True,
277
+ )