Skip to content

Feature Request: Interactive Pygame Replay Visualizer #1164

@Mark799

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()

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