#!/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("", 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()