Chess_with_AI / src /streamlit_app.py
ncn2569's picture
Update
a7a4d73
"""
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 ─────────────────────────────────────────────────
@st.cache_resource(show_spinner="Đang tải Chess Engine...")
def load_engine():
from src import config, engine
return config, engine
@st.cache_resource(show_spinner="Đang tải ML Model...")
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,
)