Parallax-Chess-Preview / play_gui.py
ParallaxOpen's picture
Upload play_gui.py with huggingface_hub
fe8739d verified
Raw History Blame Contribute Delete
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()