Skip to content

Feature Request: Interactive Pygame Replay Visualizer #1164

Description

@Mark799

Could you consider adding the following functionality?

Optional interactive visualization feature (using pygame) that allows users to replay and browse through a chess.Board’s move_stack in a graphical board interface — stepping forward/backward with arrow keys, and jumping to the start or end of the game.

Example code:

"""
Chess move-stack visualizer using pygame.

Usage:
 - Place piece PNGs in a folder called 'chess_pieces' next to this script. Filenames must be:
     wP.png wN.png wB.png wR.png wQ.png wK.png
     bP.png bN.png bB.png bR.png bQ.png bK.png
 - Run: run_replay_from_board(board)

Controls:
 - Right Arrow: advance one move
 - Left Arrow: go back one move
 - Ctrl + Right Arrow: jump to end
 - Ctrl + Left Arrow: jump to start
 - Space: toggle auto-play (1s interval)
 - Esc: quit
"""

import sys
import os
import time
import pygame
import chess
import chess.pgn

# --- Configuration ---
SQUARE_SIZE = 80
BOARD_MARGIN = 20
FPS = 60
AUTOPLAY_INTERVAL = 1.0
PIECE_SHEET = "chess_pieces"

PIECE_FILENAMES = {
    "P": "wp.png", "N": "wn.png", "B": "wb.png", "R": "wr.png", "Q": "wq.png", "K": "wk.png",
    "p": "bp.png", "n": "bn.png", "b": "bb.png", "r": "br.png", "q": "bq.png", "k": "bk.png",
}

LIGHT_SQ = (240, 217, 181)
DARK_SQ = (181, 136, 99)

def load_piece_images(square_size):
    """Load and scale piece images from folder."""
    images = {}
    for sym, fname in PIECE_FILENAMES.items():
        path = os.path.join(PIECE_SHEET, fname)
        if not os.path.exists(path):
            raise FileNotFoundError(f"Missing piece image: {path}")
        img = pygame.image.load(path).convert_alpha()
        img = pygame.transform.smoothscale(img, (square_size, square_size))
        images[sym] = img
    return images


def draw_board(board, images, square_size):
    """Draw the current board position and return a pygame.Surface."""
    surf = pygame.Surface((8 * square_size, 8 * square_size))
    for rank in range(8):
        for file in range(8):
            x = file * square_size
            y = (7 - rank) * square_size
            color = LIGHT_SQ if (file + rank) % 2 == 0 else DARK_SQ
            pygame.draw.rect(surf, color, (x, y, square_size, square_size))
    for square in chess.SQUARES:
        piece = board.piece_at(square)
        if piece:
            sym = piece.symbol()
            img = images[sym]
            file = chess.square_file(square)
            rank = chess.square_rank(square)
            x = file * square_size
            y = (7 - rank) * square_size
            surf.blit(img, (x, y))
    return surf


def run_replay_from_board(src_board):
    """Visualize a chess.Board with a move_stack in pygame."""
    moves = list(src_board.move_stack)

    # Determine initial position (standard if not available)
    initial_fen = getattr(src_board, "starting_fen", chess.STARTING_FEN)
    board = chess.Board(fen=initial_fen)
    current_index = 0

    # Setup pygame
    pygame.init()
    pygame.key.set_repeat(175)
    window_size = (8 * SQUARE_SIZE + 2 * BOARD_MARGIN, 8 * SQUARE_SIZE + 2 * BOARD_MARGIN + 30)
    screen = pygame.display.set_mode(window_size)
    pygame.display.set_caption("Chess Replay")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont(None, 24)

    images = load_piece_images(SQUARE_SIZE)
    board_surface = draw_board(board, images, SQUARE_SIZE)

    autoplay = False
    last_auto = time.time()

    def set_index(target):
        nonlocal current_index, board_surface
        target = max(0, min(target, len(moves)))
        if target > current_index:
            for i in range(current_index, target):
                board.push(moves[i])
        elif target < current_index:
            for _ in range(current_index - target):
                board.pop()
        current_index = target
        board_surface = draw_board(board, images, SQUARE_SIZE)

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                mods = pygame.key.get_mods()
                ctrl = mods & pygame.KMOD_CTRL

                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_SPACE:
                    autoplay = not autoplay
                elif event.key == pygame.K_RIGHT:
                    if ctrl:
                        set_index(len(moves))
                    else:
                        set_index(current_index + 1)
                elif event.key == pygame.K_LEFT:
                    if ctrl:
                        set_index(0)
                    else:
                        set_index(current_index - 1)

        if autoplay and (time.time() - last_auto >= AUTOPLAY_INTERVAL):
            if current_index < len(moves):
                set_index(current_index + 1)
            last_auto = time.time()

        # Draw
        screen.fill((40, 40, 40))
        screen.blit(board_surface, (BOARD_MARGIN, BOARD_MARGIN))
        txt = font.render(f"Move {current_index}/{len(moves)}", True, (255, 255, 255))
        screen.blit(txt, (BOARD_MARGIN, 8 * SQUARE_SIZE + BOARD_MARGIN + 5))
        pygame.display.flip()
        clock.tick(FPS)

    pygame.quit()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions