Download play_gui.py from ParallaxOpen/Parallax-Chess-Preview: direct link, hf CLI and curl.
- Browser
- Download file 9.13 kB
-
https://huggingface.co/ParallaxOpen/Parallax-Chess-Preview/resolve/main/play_gui.py
- Command line
-
hf download hf://ParallaxOpen/Parallax-Chess-Preview/play_gui.py
-
curl -L -o play_gui.py https://huggingface.co/ParallaxOpen/Parallax-Chess-Preview/resolve/main/play_gui.py
9.13 kB
| #!/usr/bin/env python3 | |
| """Parallax-Chess V3: Tkinter GUI.""" | |
| import chess | |
| import tkinter as tk | |
| from tkinter import font as tkfont | |
| import sys, time | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent / "scripts")) | |
| sys.path.insert(0, str(Path(__file__).parent / "src")) | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| import torch | |
| from train_v3 import ParallaxChessV3 | |
| from centauri.models.base import SmallLMConfig | |
| from chess_encoding import VOCAB_SIZE | |
| PIECE_UNICODE = { | |
| "K": "\u2654", "Q": "\u2655", "R": "\u2656", "B": "\u2657", "N": "\u2658", "P": "\u2659", | |
| "k": "\u265A", "q": "\u265B", "r": "\u265C", "b": "\u265D", "n": "\u265E", "p": "\u265F", | |
| } | |
| LIGHT = "#F0D9B5" | |
| DARK = "#B58863" | |
| LAST_MOVE_COLOR = "#CDD26A" | |
| SELECT_COLOR = "#F6F669" | |
| MOVE_DOT_COLOR = "#7B9E57" | |
| class ChessGUI: | |
| def __init__(self): | |
| self.root = tk.Tk() | |
| self.root.title("Parallax-Chess V3") | |
| self.root.resizable(False, False) | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| cfg = SmallLMConfig( | |
| vocab_size=VOCAB_SIZE, d_model=512, n_heads=8, n_kv_heads=4, | |
| n_layers=8, intermediate_size=2048, max_seq_len=128, | |
| norm_type="rms", rope_type="neox", n_experts=0, | |
| n_loops=1, loop_mode="per_layer", | |
| ) | |
| self.model = ParallaxChessV3(cfg).to(self.device) | |
| ckpt_path = Path(__file__).parent / "checkpoints" / "parallax_chess_v3" | |
| candidates = sorted(ckpt_path.glob("step_*.pt"), reverse=True) | |
| ckpt = torch.load(str(candidates[0]), map_location=self.device) | |
| self.model.load_state_dict(ckpt["model"]) | |
| self.model.eval() | |
| print("Loaded %s" % candidates[0].name) | |
| self.board = chess.Board() | |
| self.SQ = 72 | |
| self.selected = None | |
| self.legal_targets = [] | |
| self.last_move = None | |
| self.player_color = chess.WHITE | |
| self.flipped = False | |
| self.piece_font = tkfont.Font(family="Segoe UI Symbol", size=36) | |
| self.coord_font = tkfont.Font(family="Consolas", size=10, weight="bold") | |
| self.info_font = tkfont.Font(family="Consolas", size=11) | |
| top = tk.Frame(self.root) | |
| top.pack(padx=10, pady=5, fill=tk.X) | |
| self.status_var = tk.StringVar(value="Your turn (White)") | |
| tk.Label(top, textvariable=self.status_var, font=self.info_font, width=35).pack(side=tk.LEFT) | |
| self.eval_var = tk.StringVar(value="Eval: 0.0") | |
| tk.Label(top, textvariable=self.eval_var, font=self.info_font, width=15).pack(side=tk.LEFT) | |
| canvas_size = self.SQ * 8 + 30 | |
| self.canvas = tk.Canvas(self.root, width=canvas_size, height=canvas_size, bg="#312E2B") | |
| self.canvas.pack(padx=10, pady=5) | |
| self.canvas.bind("<Button-1>", self.on_click) | |
| bottom = tk.Frame(self.root) | |
| bottom.pack(padx=10, pady=5) | |
| tk.Button(bottom, text="New Game", font=self.info_font, command=self.new_game).pack(side=tk.LEFT, padx=5) | |
| tk.Button(bottom, text="Flip Board", font=self.info_font, command=self.flip_board).pack(side=tk.LEFT, padx=5) | |
| self.draw_board() | |
| def sq_to_rc(self, sq): | |
| """Convert chess square to display (row, col) based on flip.""" | |
| file = chess.square_file(sq) | |
| rank = chess.square_rank(sq) | |
| if self.flipped: | |
| row = rank | |
| col = 7 - file | |
| else: | |
| row = 7 - rank | |
| col = file | |
| return row, col | |
| def rc_to_sq(self, row, col): | |
| """Convert display (row, col) to chess square.""" | |
| if self.flipped: | |
| file = 7 - col | |
| rank = row | |
| else: | |
| file = col | |
| rank = 7 - row | |
| return chess.square(file, rank) | |
| def new_game(self): | |
| self.board = chess.Board() | |
| self.selected = None | |
| self.legal_targets = [] | |
| self.last_move = None | |
| self.status_var.set("Your turn (White)") | |
| self.eval_var.set("Eval: 0.0") | |
| self.draw_board() | |
| def flip_board(self): | |
| self.flipped = not self.flipped | |
| self.draw_board() | |
| def draw_board(self): | |
| self.canvas.delete("all") | |
| offset = 15 | |
| for display_row in range(8): | |
| for display_col in range(8): | |
| x1 = offset + display_col * self.SQ | |
| y1 = offset + display_row * self.SQ | |
| x2 = x1 + self.SQ | |
| y2 = y1 + self.SQ | |
| sq = self.rc_to_sq(display_row, display_col) | |
| is_dark = (display_row + display_col) % 2 == 1 | |
| color = DARK if is_dark else LIGHT | |
| if self.last_move and sq in (self.last_move.from_square, self.last_move.to_square): | |
| color = LAST_MOVE_COLOR | |
| if self.selected == sq: | |
| color = SELECT_COLOR | |
| self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="") | |
| piece = self.board.piece_at(sq) | |
| if piece: | |
| symbol = PIECE_UNICODE[piece.symbol()] | |
| fill = "#FFFFFF" if piece.color == chess.WHITE else "#1a1a1a" | |
| self.canvas.create_text( | |
| x1 + self.SQ // 2, y1 + self.SQ // 2, | |
| text=symbol, font=self.piece_font, fill=fill, | |
| ) | |
| if sq in self.legal_targets: | |
| if self.board.piece_at(sq): | |
| self.canvas.create_oval( | |
| x1 + 4, y1 + 4, x2 - 4, y2 - 4, | |
| outline=MOVE_DOT_COLOR, width=3, fill="", | |
| ) | |
| else: | |
| r_dot = 8 | |
| cx, cy = x1 + self.SQ // 2, y1 + self.SQ // 2 | |
| self.canvas.create_oval( | |
| cx - r_dot, cy - r_dot, cx + r_dot, cy + r_dot, | |
| fill=MOVE_DOT_COLOR, outline="", | |
| ) | |
| # File labels (a-h) along bottom | |
| files = "abcdefgh" if not self.flipped else "hgfedcba" | |
| for i in range(8): | |
| x = offset + i * self.SQ + self.SQ // 2 | |
| self.canvas.create_text(x, offset + 8 * self.SQ + 12, text=files[i], | |
| font=self.coord_font, fill="#AAAAAA") | |
| # Rank labels (1-8) along left | |
| ranks = "87654321" if not self.flipped else "12345678" | |
| for i in range(8): | |
| y = offset + i * self.SQ + self.SQ // 2 | |
| self.canvas.create_text(offset - 8, y, text=ranks[i], | |
| font=self.coord_font, fill="#AAAAAA") | |
| def on_click(self, event): | |
| if self.board.is_game_over(): | |
| self.status_var.set("Game over: %s" % self.board.result()) | |
| return | |
| if self.board.turn != self.player_color: | |
| return | |
| offset = 15 | |
| display_col = (event.x - offset) // self.SQ | |
| display_row = (event.y - offset) // self.SQ | |
| if display_col < 0 or display_col > 7 or display_row < 0 or display_row > 7: | |
| return | |
| sq = self.rc_to_sq(display_row, display_col) | |
| if self.selected is not None and sq in self.legal_targets: | |
| move = chess.Move(self.selected, sq) | |
| piece = self.board.piece_at(self.selected) | |
| if piece and piece.piece_type == chess.PAWN: | |
| if (piece.color == chess.WHITE and chess.square_rank(sq) == 7) or \ | |
| (piece.color == chess.BLACK and chess.square_rank(sq) == 0): | |
| move = chess.Move(self.selected, sq, promotion=chess.QUEEN) | |
| self.board.push(move) | |
| self.last_move = move | |
| self.selected = None | |
| self.legal_targets = [] | |
| self.draw_board() | |
| self.root.after(50, self.ai_move) | |
| return | |
| piece = self.board.piece_at(sq) | |
| if piece and piece.color == self.board.turn: | |
| self.selected = sq | |
| self.legal_targets = [m.to_square for m in self.board.legal_moves if m.from_square == sq] | |
| else: | |
| self.selected = None | |
| self.legal_targets = [] | |
| self.draw_board() | |
| def ai_move(self): | |
| if self.board.is_game_over(): | |
| self.status_var.set("Game over: %s" % self.board.result()) | |
| return | |
| self.status_var.set("V3 thinking...") | |
| self.root.update() | |
| t0 = time.time() | |
| move = self.model.predict_move(self.board, self.device) | |
| elapsed = time.time() - t0 | |
| if move is None: | |
| self.status_var.set("V3 resigns!") | |
| return | |
| self.board.push(move) | |
| self.last_move = move | |
| self.status_var.set("V3 played %s (%.1fs)" % (move.uci(), elapsed)) | |
| eval_cp = self.model.evaluate(self.board, self.device) | |
| self.eval_var.set("Eval: %.0f cp" % eval_cp) | |
| self.draw_board() | |
| if self.board.is_game_over(): | |
| self.status_var.set("Game over: %s" % self.board.result()) | |
| def run(self): | |
| self.root.mainloop() | |
| if __name__ == "__main__": | |
| ChessGUI().run() | |