johnthy/Chess-Dummy-Model
0
1import streamlit as st2import chess3import chess.svg4import numpy as np5import torch6import torch.nn7from sklearn.linear_model import LinearRegression8from streamlit_extras.let_it_rain import rain9 10 11 12def evaluate_board(board: chess.Board) -> float:13 14 material = {15 chess.PAWN: 100,16 chess.KNIGHT: 320,17 chess.BISHOP: 330,18 chess.ROOK: 500,19 chess.QUEEN: 900,20 chess.KING: 0,21 }22 23 PST_PAWN = np.array([24 0, 0, 0, 0, 0, 0, 0, 0,25 50, 50, 50, 50, 50, 50, 50, 50,26 10, 10, 20, 30, 30, 20, 10, 10,27 5, 5, 10, 25, 25, 10, 5, 5,28 0, 0, 0, 20, 20, 0, 0, 0,29 5, -5,-10, 0, 0,-10, -5, 5,30 5, 10, 10,-20,-20, 10, 10, 5,31 0, 0, 0, 0, 0, 0, 0, 032 ])33 PST_KNIGHT = np.array([34 -50,-40,-30,-30,-30,-30,-40,-50,35 -40,-20, 0, 0, 0, 0,-20,-40,36 -30, 0, 10, 15, 15, 10, 0,-30,37 -30, 5, 15, 20, 20, 15, 5,-30,38 -30, 0, 15, 20, 20, 15, 0,-30,39 -30, 5, 10, 15, 15, 10, 5,-30,40 -40,-20, 0, 5, 5, 0,-20,-40,41 -50,-40,-30,-30,-30,-30,-40,-5042 ])43 PST_BISHOP = np.array([44 -20,-10,-10,-10,-10,-10,-10,-20,45 -10, 5, 0, 0, 0, 0, 5,-10,46 -10, 10, 10, 10, 10, 10, 10,-10,47 -10, 0, 10, 10, 10, 10, 0,-10,48 -10, 5, 5, 10, 10, 5, 5,-10,49 -10, 0, 5, 10, 10, 5, 0,-10,50 -10, 0, 0, 0, 0, 0, 0,-10,51 -20,-10,-10,-10,-10,-10,-10,-2052 ])53 PST_ROOK = np.array([54 0, 0, 0, 0, 0, 0, 0, 0,55 5, 10, 10, 10, 10, 10, 10, 5,56 -5, 0, 0, 0, 0, 0, 0, -5,57 -5, 0, 0, 0, 0, 0, 0, -5,58 -5, 0, 0, 0, 0, 0, 0, -5,59 -5, 0, 0, 0, 0, 0, 0, -5,60 -5, 0, 0, 0, 0, 0, 0, -5,61 0, 0, 0, 5, 5, 0, 0, 062 ])63 PST_QUEEN = np.array([64 -20,-10,-10, -5, -5,-10,-10,-20,65 -10, 0, 0, 0, 0, 0, 0,-10,66 -10, 0, 5, 5, 5, 5, 0,-10,67 -5, 0, 5, 5, 5, 5, 0, -5,68 0, 0, 5, 5, 5, 5, 0, -5,69 -10, 5, 5, 5, 5, 5, 0,-10,70 -10, 0, 5, 0, 0, 0, 0,-10,71 -20,-10,-10, -5, -5,-10,-10,-2072 ])73 PST_KING = np.array([74 -30,-40,-40,-50,-50,-40,-40,-30,75 -30,-40,-40,-50,-50,-40,-40,-30,76 -30,-40,-40,-50,-50,-40,-40,-30,77 -30,-40,-40,-50,-50,-40,-40,-30,78 -20,-30,-30,-40,-40,-30,-30,-20,79 -10,-20,-20,-20,-20,-20,-20,-10,80 20, 20, 0, 0, 0, 0, 20, 20,81 20, 30, 10, 0, 0, 10, 30, 2082 ])83 84 pst_by_piece = {85 chess.PAWN: PST_PAWN,86 chess.KNIGHT: PST_KNIGHT,87 chess.BISHOP: PST_BISHOP,88 chess.ROOK: PST_ROOK,89 chess.QUEEN: PST_QUEEN,90 chess.KING: PST_KING,91 }92 93 score = 0.094 for square in chess.SQUARES:95 piece = board.piece_at(square)96 if piece:97 base = material[piece.piece_type]98 pst = pst_by_piece[piece.piece_type]99 idx = square if piece.color == chess.WHITE else chess.square_mirror(square)100 val = base + pst[idx]101 score += val if piece.color == chess.WHITE else -val102 103 104 b_w = board.copy()105 b_w.turn = chess.WHITE106 b_b = board.copy()107 b_b.turn = chess.BLACK108 mobility = (len(list(b_w.legal_moves)) - len(list(b_b.legal_moves))) * 2.0109 score += mobility110 111 112 try:113 if board.has_kingside_castling_rights(chess.WHITE) or board.has_queenside_castling_rights(chess.WHITE):114 score += 15115 if board.has_kingside_castling_rights(chess.BLACK) or board.has_queenside_castling_rights(chess.BLACK):116 score -= 15117 except Exception:118 pass119 120 return score / 100.0 121 122 123def extract_features(board: chess.Board) -> np.ndarray:124 features = np.zeros(12)125 for square in chess.SQUARES:126 piece = board.piece_at(square)127 if piece:128 idx = (piece.piece_type - 1) + 6 * (1 if piece.color == chess.BLACK else 0)129 features[idx] += 1130 return features.reshape(1, -1)131 132 133def train_simple_model() -> LinearRegression:134 X = np.random.rand(100, 12) * 10135 y = np.sum(X[:, :6], axis=1) - np.sum(X[:, 6:], axis=1)136 model = LinearRegression()137 model.fit(X, y)138 return model139 140 141class SimpleNN(torch.nn.Module):142 def __init__(self) -> None:143 super(SimpleNN, self).__init__()144 self.fc = torch.nn.Linear(12, 1)145 146 def forward(self, x: torch.Tensor) -> torch.Tensor:147 return self.fc(x)148 149 150def train_torch_model(model, epochs=100, lr=0.01):151 152 return model153 154 155 156def evaluate_combined(board: chess.Board, sklearn_model: LinearRegression, torch_model: SimpleNN) -> float:157 158 return float(evaluate_board(board))159 160 161# Minimax with alpha-beta pruning162def minimax_best_move(board: chess.Board, depth: int, sklearn_model: LinearRegression, torch_model: SimpleNN, ai_color: bool) -> chess.Move | None:163 def search(node: chess.Board, d: int, alpha: float, beta: float) -> float:164 if d == 0 or node.is_game_over():165 score = evaluate_combined(node, sklearn_model, torch_model)166 return score if ai_color == chess.WHITE else -score167 168 legal = list(node.legal_moves)169 if not legal:170 score = evaluate_combined(node, sklearn_model, torch_model)171 return score if ai_color == chess.WHITE else -score172 173 maximizing = (node.turn == ai_color)174 best_val = -float('inf') if maximizing else float('inf')175 176 def move_key(m: chess.Move):177 return 0 if node.is_capture(m) else 1178 legal.sort(key=move_key)179 180 for mv in legal:181 node.push(mv)182 val = search(node, d - 1, alpha, beta)183 node.pop()184 if maximizing:185 if val > best_val:186 best_val = val187 if best_val > alpha:188 alpha = best_val189 if alpha >= beta:190 break191 else:192 if val < best_val:193 best_val = val194 if best_val < beta:195 beta = best_val196 if alpha >= beta:197 break198 return best_val199 200 best_move = None201 best_score = -float('inf')202 moves = list(board.legal_moves)203 if not moves:204 return None205 206 def top_key(m: chess.Move):207 return 0 if board.is_capture(m) else 1208 moves.sort(key=top_key)209 210 for mv in moves:211 board.push(mv)212 score = search(board, depth - 1, -float('inf'), float('inf'))213 board.pop()214 if score > best_score:215 best_score = score216 best_move = mv217 return best_move218 219 220 221 222 223def ai_move(board: chess.Board, sklearn_model: LinearRegression, torch_model: SimpleNN):224 legal_moves = list(board.legal_moves)225 if not legal_moves:226 return None227 228 best_move = None229 best_score = -float('inf')230 231 for move in legal_moves:232 board.push(move)233 234 basic_score = evaluate_board(board)235 236 features = extract_features(board)237 sklearn_score = sklearn_model.predict(features)[0]238 239 features_tensor = torch.tensor(features, dtype=torch.float32)240 with torch.no_grad():241 torch_score = torch_model(features_tensor).item()242 243 combined_score = (basic_score + sklearn_score + torch_score) / 3244 if combined_score > best_score:245 best_score = combined_score246 best_move = move247 248 board.pop()249 250 return best_move251 252 253def main() -> None:254 st.set_page_config(page_title=" Chess AI Dummy", page_icon="♟️", layout="centered")255 st.markdown(256 """257 <style>258 .title-box {background: linear-gradient(90deg,#00c6ff 0%,#0072ff 100%); padding: 14px 18px; border-radius: 12px; color: #fff; margin-bottom: 14px;}259 .badge {display:inline-block; padding:6px 10px; border-radius:999px; font-weight:600;}260 .badge-turn {background:#ffe08a; color:#5a3e00;}261 .badge-eval-pos {background:#b6f3c0; color:#0f5132;}262 .badge-eval-neg {background:#ffb3b3; color:#58151c;}263 .panel {background: #ffffff; border: 1px solid #ececec; border-radius: 12px; padding: 12px 14px;}264 .move-list {max-height: 320px; overflow-y: auto; line-height: 1.7;}265 hr {border: none; height: 1px; background: linear-gradient(90deg, rgba(0,0,0,0) 0%, #e6e6e6 50%, rgba(0,0,0,0) 100%);} 266 .spacer-sm {height: 12px;}267 </style>268 """,269 unsafe_allow_html=True,270 )271 st.markdown("<div class='title-box'><h2 style='margin:0'>Chess AI Dummy (VIBE CODED)</h2></div>", unsafe_allow_html=True)272 st.markdown("<div class='spacer-sm'></div>", unsafe_allow_html=True)273 274 if 'board' not in st.session_state:275 st.session_state.board = chess.Board()276 if 'player_color' not in st.session_state:277 st.session_state.player_color = chess.WHITE278 if 'sklearn_model' not in st.session_state:279 st.session_state.sklearn_model = train_simple_model()280 if 'torch_model' not in st.session_state:281 st.session_state.torch_model = SimpleNN()282 if 'move_input' not in st.session_state:283 st.session_state.move_input = ""284 if 'clear_move' not in st.session_state:285 st.session_state.clear_move = False286 if 'ai_check' not in st.session_state:287 st.session_state.ai_check = False288 if 'ai_check_san' not in st.session_state:289 st.session_state.ai_check_san = ""290 board = st.session_state.board291 player_color = st.session_state.player_color292 293 294 depth = 4295 296 # Right panel helpers297 def get_san_history(b: chess.Board):298 temp = chess.Board()299 sans = []300 for mv in b.move_stack:301 sans.append(temp.san(mv))302 temp.push(mv)303 return sans304 305 cur_eval = evaluate_combined(board, st.session_state.sklearn_model, st.session_state.torch_model)306 eval_badge = f"<span class='badge {'badge-eval-pos' if cur_eval>=0 else 'badge-eval-neg'}'>Eval: {cur_eval:+.2f}</span>"307 turn_badge = f"<span class='badge badge-turn'>{'Your' if board.turn==player_color else 'AI'} turn</span>"308 309 left, right = st.columns([2, 1], gap="large")310 with left:311 st.markdown(f"{turn_badge} {eval_badge}", unsafe_allow_html=True)312 svg = chess.svg.board(board=board, size=460)313 st.markdown(svg, unsafe_allow_html=True)314 # Label with inline info popover next to it315 lbl_col, info_col = st.columns([1, 0.15])316 with lbl_col:317 st.write("Enter Move (SAN)")318 with info_col:319 try:320 with st.popover("👇"):321 st.markdown(322 """323 Use notation such as: e4 or Nf3.324 325 Use O-O for kingside castle, O-O-O for queenside castle.326 327 For pawn captures, write from-square then to-square (e.g., c4b5).328 """329 )330 except Exception:331 with st.expander("👇 Move input help"):332 st.markdown(333 """334 Use notation such as: e4 or Nf3.335 336 Use O-O for kingside castle, O-O-O for queenside castle.337 338 For pawn captures, write from-square then to-square (e.g., c4b5).339 """340 )341 342 if st.session_state.clear_move:343 st.session_state.move_input = ""344 st.session_state.clear_move = False345 move_input = st.text_input("Your move:", key="move_input")346 # Show AI Check notification just below the input347 if st.session_state.ai_check:348 st.markdown(349 "<div style='background:#ffe3e3;border:1px solid #ffb3b3;"350 "padding:8px 12px;border-radius:10px;color:#7a1212;display:inline-block;"351 "margin-top:6px;'>"352 + f"♚ Check! ({st.session_state.ai_check_san})" + "</div>",353 unsafe_allow_html=True,354 )355 # reset flag after surfacing once356 st.session_state.ai_check = False357 st.session_state.ai_check_san = ""358 359 if board.is_game_over():360 st.markdown("<h2 style='color:#FF5733;'>🎉 Game over! 🎉</h2>", unsafe_allow_html=True)361 if board.is_checkmate():362 winner = "White" if board.turn == chess.BLACK else "Black"363 if winner == "White":364 st.markdown(365 "<div style='background: linear-gradient(90deg, #f8ffae 0%, #43c6ac 100%);"366 "padding: 1em; border-radius: 10px; color: #222; font-size: 1.2em;'>"367 "♔ <b>Checkmate! White wins.</b> <br>Great job if you played as White!<br>"368 "If not, try to spot where you could have improved your defense."369 "</div>", unsafe_allow_html=True)370 rain(371 emoji="♔",372 font_size=54,373 falling_speed=5,374 animation_length="infinite"375 )376 else:377 st.markdown(378 "<div style='background: linear-gradient(90deg, #a1c4fd 0%, #c2e9fb 100%);"379 "padding: 1em; border-radius: 10px; color: #222; font-size: 1.2em;'>"380 "♚ <b>Checkmate! Black wins.</b> <br>If you played as Black, congratulations!<br>"381 "If not, review the game to see how you can avoid checkmate next time."382 "</div>", unsafe_allow_html=True)383 rain(384 emoji="♚",385 font_size=54,386 falling_speed=5,387 animation_length="infinite"388 )389 elif board.is_stalemate():390 st.markdown(391 "<div style='background: linear-gradient(90deg, #f7971e 0%, #ffd200 100%);"392 "padding: 1em; border-radius: 10px; color: #333; font-size: 1.2em;'>"393 "🤝 <b>Stalemate!</b> <br>It's a draw. Both sides played well and neither could force a win.<br>"394 "Try to find a way to break the deadlock in your next game!"395 "</div>", unsafe_allow_html=True)396 rain(397 emoji="🤝",398 font_size=44,399 falling_speed=4,400 animation_length="infinite"401 )402 else:403 st.markdown(404 "<div style='background: linear-gradient(90deg, #e0c3fc 0%, #8ec5fc 100%);"405 "padding: 1em; border-radius: 10px; color: #333; font-size: 1.2em;'>"406 "🤷 <b>Draw!</b> <br>The game ended in a draw. Sometimes, a draw is the best result for both players.<br>"407 "Analyze the final position to see if either side missed a win."408 "</div>", unsafe_allow_html=True)409 rain(410 emoji="🤷",411 font_size=44,412 falling_speed=4,413 animation_length="infinite"414 )415 st.markdown("<hr>", unsafe_allow_html=True)416 if st.button("Reset Game"):417 st.session_state.board = chess.Board()418 st.rerun()419 return420 421 with right:422 sans = get_san_history(board)423 rows = []424 for i in range(0, len(sans), 2):425 white_mv = sans[i]426 black_mv = sans[i+1] if i+1 < len(sans) else ""427 if black_mv:428 row = f"<span style='color:#000000'>{white_mv} - {black_mv}</span>"429 else:430 row = f"<span style='color:#000000'>{white_mv}</span>"431 rows.append(row)432 history_html = "".join([f"<div>{row}</div>" for row in rows])433 st.markdown(434 "<div class='panel'>"435 "<b style='color:#000000'>Move history</b>"436 "<div class='move-list'>" + history_html + "</div>"437 "</div>",438 unsafe_allow_html=True439 )440 441 if board.turn == player_color:442 if move_input:443 try:444 move = board.parse_san(move_input.strip())445 if move in board.legal_moves:446 board.push(move)447 st.session_state.board = board448 st.session_state.clear_move = True449 st.rerun()450 else:451 st.error("Illegal move!")452 st.session_state.clear_move = True453 except ValueError:454 st.error("Invalid move format!")455 st.session_state.clear_move = True456 457 with left:458 with st.write("AI is thinking..."):459 ai_color = not player_color460 move = minimax_best_move(board, depth, st.session_state.sklearn_model, st.session_state.torch_model, ai_color)461 462 if move is None:463 move = ai_move(board, st.session_state.sklearn_model, st.session_state.torch_model)464 465 if move:466 san_text = board.san(move)467 board.push(move)468 st.session_state.board = board469 st.session_state.clear_move = True470 if board.is_check():471 st.session_state.ai_check = True472 st.session_state.ai_check_san = san_text473 st.rerun()474 475 else:476 with left:477 st.write("No legal moves for AI.")478 479 480 481 482 483if __name__ == "__main__":484 main()485 486 487 488 