Spaces:
Sleeping
Sleeping
| """ | |
| Chess AI - Hugging Face Spaces | |
| Entry point: app.py | |
| """ | |
| import sys | |
| import os | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| import streamlit as st | |
| import chess | |
| import chess.svg | |
| import base64 | |
| st.set_page_config( | |
| page_title="Chess AI", | |
| page_icon="♟️", | |
| layout="centered", | |
| initial_sidebar_state="expanded", | |
| ) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=IBM+Plex+Mono:wght@400;500&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'IBM Plex Mono', monospace; | |
| background-color: #0f0f0f; | |
| color: #e8e0d0; | |
| } | |
| h1, h2, h3 { font-family: 'Playfair Display', serif; color: #f0c060; } | |
| .stButton > button { | |
| background: #1a1a1a; color: #f0c060; | |
| border: 1px solid #f0c060; border-radius: 2px; | |
| font-family: 'IBM Plex Mono', monospace; font-size: 13px; | |
| transition: all 0.2s; | |
| } | |
| .stButton > button:hover { background: #f0c060; color: #0f0f0f; } | |
| .move-log { | |
| background: #1a1a1a; border: 1px solid #333; border-radius: 4px; | |
| padding: 10px; height: 200px; overflow-y: auto; | |
| font-size: 12px; color: #aaa; font-family: 'IBM Plex Mono', monospace; | |
| } | |
| .status-bar { | |
| background: #1a1a1a; border-left: 3px solid #f0c060; | |
| padding: 8px 14px; margin: 10px 0; font-size: 13px; | |
| } | |
| .info-chip { | |
| display: inline-block; background: #222; border: 1px solid #444; | |
| border-radius: 2px; padding: 2px 8px; font-size: 11px; margin: 2px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ─── Cached resource loading ───────────────────────────────────────────────── | |
| def load_engine(): | |
| from src import config, engine | |
| return config, engine | |
| def load_ml_agent(): | |
| try: | |
| from src.ai_battle import MLAgent | |
| return MLAgent(), None | |
| except Exception as e: | |
| return None, str(e) | |
| # ─── Session state ──────────────────────────────────────────────────────────── | |
| def init_state(): | |
| defaults = { | |
| "board": chess.Board(), | |
| "move_log": [], | |
| "game_over": False, | |
| "status_msg": "Lượt của bạn ♙", | |
| "depth": 2, | |
| "game_mode": "human_vs_minimax", | |
| "input_key": 0, | |
| } | |
| for k, v in defaults.items(): | |
| if k not in st.session_state: | |
| st.session_state[k] = v | |
| init_state() | |
| # ─── Helpers ───────────────────────────────────────────────────────────────── | |
| def board_to_html(board: chess.Board, last_move: chess.Move = None) -> str: | |
| arrows = [] | |
| if last_move: | |
| arrows.append(chess.svg.Arrow(last_move.from_square, last_move.to_square, color="#f0c06088")) | |
| svg = chess.svg.board( | |
| board, size=460, arrows=arrows, | |
| colors={ | |
| "square light": "#ede0c8", | |
| "square dark": "#b58863", | |
| "square light lastmove": "#cdd16e", | |
| "square dark lastmove": "#aaa23a", | |
| } | |
| ) | |
| b64 = base64.b64encode(svg.encode()).decode() | |
| return f'<img src="data:image/svg+xml;base64,{b64}" width="460"/>' | |
| def get_last_move(board): | |
| try: | |
| return board.peek() | |
| except IndexError: | |
| return None | |
| def reset_game(): | |
| st.session_state.board = chess.Board() | |
| st.session_state.move_log = [] | |
| st.session_state.game_over = False | |
| st.session_state.status_msg = "Lượt của bạn ♙" | |
| st.session_state.input_key = 0 | |
| def check_game_over(board: chess.Board) -> bool: | |
| if board.is_checkmate(): | |
| winner = "Đen" if board.turn == chess.WHITE else "Trắng" | |
| st.session_state.status_msg = f"♚ Chiếu hết! {winner} thắng!" | |
| st.session_state.game_over = True | |
| elif board.is_stalemate(): | |
| st.session_state.status_msg = "🤝 Hòa (Stalemate)" | |
| st.session_state.game_over = True | |
| elif board.is_insufficient_material(): | |
| st.session_state.status_msg = "🤝 Hòa (thiếu quân)" | |
| st.session_state.game_over = True | |
| elif board.is_seventyfive_moves(): | |
| st.session_state.status_msg = "🤝 Hòa (75 nước)" | |
| st.session_state.game_over = True | |
| return st.session_state.game_over | |
| def do_minimax_move(board: chess.Board): | |
| config, engine = load_engine() | |
| move = engine.find_best_move(board, config.STANDARD_WEIGHTS, st.session_state.depth) | |
| if move and move in board.legal_moves: | |
| st.session_state.move_log.append(f"Minimax (d{st.session_state.depth}): {move.uci()}") | |
| board.push(move) | |
| if not check_game_over(board): | |
| st.session_state.status_msg = "Lượt của bạn ♙" | |
| def do_ml_move(board: chess.Board): | |
| ml_agent, err = load_ml_agent() | |
| if err: | |
| st.error(f"ML Agent lỗi: {err}") | |
| return | |
| move = ml_agent.get_move(board, time_limit=15.0) | |
| if move and move in board.legal_moves: | |
| st.session_state.move_log.append(f"ML Agent: {move.uci()}") | |
| board.push(move) | |
| if not check_game_over(board): | |
| st.session_state.status_msg = "Lượt của bạn ♙" | |
| else: | |
| import random | |
| legal = list(board.legal_moves) | |
| if legal: | |
| fb = random.choice(legal) | |
| board.push(fb) | |
| st.session_state.move_log.append(f"ML Agent (fallback): {fb.uci()}") | |
| check_game_over(board) | |
| # ─── Sidebar ────────────────────────────────────────────────────────────────── | |
| with st.sidebar: | |
| st.markdown("## ♟️ Chess AI") | |
| st.markdown("---") | |
| new_mode = st.radio( | |
| "Chế độ chơi", | |
| ["Người vs Minimax", "Người vs ML Agent"], | |
| ) | |
| new_mode_key = "human_vs_minimax" if "Minimax" in new_mode else "human_vs_ml" | |
| # Reset tự động nếu đổi chế độ | |
| if new_mode_key != st.session_state.game_mode: | |
| st.session_state.game_mode = new_mode_key | |
| reset_game() | |
| if new_mode_key == "human_vs_minimax": | |
| st.session_state.depth = st.slider("Độ sâu Minimax", 1, 4, 2) | |
| st.caption("Depth 1-2: nhanh · Depth 3-4: mạnh hơn nhưng chậm") | |
| st.markdown("---") | |
| if st.button("🔄 Ván mới"): | |
| reset_game() | |
| st.rerun() | |
| st.markdown("---") | |
| st.markdown("**Hướng dẫn**") | |
| st.markdown("Chọn ô nguồn → ô đích → nhấn **Đi** \nPhong cấp tự động lên Hậu") | |
| st.markdown("---") | |
| st.markdown( | |
| '<a href="https://github.com/ncn2569/Chess-game-with-AI-and-ML" ' | |
| 'target="_blank">📂 GitHub</a>', | |
| unsafe_allow_html=True, | |
| ) | |
| # ─── Main ───────────────────────────────────────────────────────────────────── | |
| st.markdown("# Chess AI") | |
| board = st.session_state.board | |
| col_board, col_ctrl = st.columns([3, 2], gap="medium") | |
| with col_board: | |
| st.markdown(board_to_html(board, get_last_move(board)), unsafe_allow_html=True) | |
| st.markdown( | |
| f'<div class="status-bar">{st.session_state.status_msg}</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| if board.is_check() and not st.session_state.game_over: | |
| st.warning("⚠️ Chiếu!") | |
| with col_ctrl: | |
| # ── Input nước đi (người luôn chơi Trắng) | |
| if not st.session_state.game_over and board.turn == chess.WHITE: | |
| st.markdown("### Nước đi của bạn") | |
| legal_from = sorted({ | |
| chess.SQUARE_NAMES[m.from_square] for m in board.legal_moves | |
| }) | |
| from_sq = st.selectbox("Từ ô", [""] + legal_from, key=f"from_sq_{st.session_state.input_key}") | |
| valid_to = [] | |
| if from_sq: | |
| try: | |
| from_idx = chess.parse_square(from_sq) | |
| valid_to = sorted({ | |
| chess.SQUARE_NAMES[m.to_square] | |
| for m in board.legal_moves if m.from_square == from_idx | |
| }) | |
| except ValueError: | |
| pass | |
| to_sq = st.selectbox("Đến ô", [""] + valid_to, key=f"to_sq_{st.session_state.input_key}") | |
| if st.button("▶ Đi", disabled=not (from_sq and to_sq)): | |
| try: | |
| uci = f"{from_sq}{to_sq}" | |
| piece = board.piece_at(chess.parse_square(from_sq)) | |
| if piece and piece.piece_type == chess.PAWN: | |
| if (board.turn == chess.WHITE and to_sq[1] == "8") or \ | |
| (board.turn == chess.BLACK and to_sq[1] == "1"): | |
| uci += "q" | |
| move = chess.Move.from_uci(uci) | |
| if move in board.legal_moves: | |
| st.session_state.move_log.append(f"Bạn: {uci}") | |
| board.push(move) | |
| st.session_state.input_key += 1 | |
| if not check_game_over(board): | |
| st.session_state.status_msg = "AI đang suy nghĩ..." | |
| st.rerun() | |
| else: | |
| st.error("Nước đi không hợp lệ!") | |
| except ValueError: | |
| pass # from_sq rỗng hoặc không hợp lệ, im lặng bỏ qua | |
| except Exception as e: | |
| st.error(f"Lỗi: {e}") | |
| # ── AI tự động đi khi đến lượt Đen | |
| if not st.session_state.game_over and board.turn == chess.BLACK: | |
| placeholder = st.empty() | |
| with st.spinner("AI đang suy nghĩ..."): | |
| if st.session_state.game_mode == "human_vs_minimax": | |
| do_minimax_move(board) | |
| else: | |
| do_ml_move(board) | |
| placeholder.empty() | |
| st.rerun() | |
| # ── Lịch sử | |
| st.markdown("---") | |
| st.markdown("### Lịch sử") | |
| log_html = "<br>".join(st.session_state.move_log[-30:][::-1]) or "<i>Chưa có</i>" | |
| st.markdown(f'<div class="move-log">{log_html}</div>', unsafe_allow_html=True) | |
| # ── Thông tin quân còn lại | |
| st.markdown("---") | |
| for name, pt in [("♙Tốt", chess.PAWN), ("♘Mã", chess.KNIGHT), | |
| ("♗Tượng", chess.BISHOP), ("♖Xe", chess.ROOK), ("♛Hậu", chess.QUEEN)]: | |
| w = len(board.pieces(pt, chess.WHITE)) | |
| b = len(board.pieces(pt, chess.BLACK)) | |
| st.markdown( | |
| f'<span class="info-chip">{name} ⬜{w} ⬛{b}</span>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| f'<span class="info-chip">Nước #{board.fullmove_number}</span>', | |
| unsafe_allow_html=True, | |
| ) |