From the archive

How to Create a Classic Solitaire Game Using Pygame in Python: A Step-by-Step Python Guide

Review a Pygame prototype that loads card images, deals a tableau, draws from stock, and moves top cards without implementing full Solitaire rules.

Building a card-table prototype is a useful way to practice Python, Pygame drawing, image loading, rectangle collision checks, and mouse events. The archived draft introduced those ideas, but it did not contain a complete or rules-valid game of Solitaire.

Scope correction: The reviewed program below is a board prototype. It displays a seven-column tableau, shows a stock and waste area, draws cards from the stock, and lets you place a top tableau card into another column. It does not enforce legal moves, track face-up and face-down state, build foundations, move card stacks, recycle the stock, or detect a win.

Install Pygame Safely

Pygame is a third-party package, not part of Python’s standard library. A virtual environment keeps project packages separate from the rest of your system:

python -m venv .venv

Activate the environment using the command appropriate for your operating system, then install Pygame through that interpreter:

python -m pip install pygame

Refer to the current Python virtual-environment documentation and official Pygame installation guidance if your platform uses a different Python command or requires platform-specific setup.

Required Card Assets

The WordPress export did not contain the artwork required to run the example. Create a cards directory beside the Python file and supply 52 card images plus one card-back image:

cards/A_of_hearts.png
cards/2_of_hearts.png
cards/10_of_spades.png
cards/K_of_clubs.png
cards/back.png

Every combination of rank (A, 2 through 10, J, Q, K) and suit (hearts, diamonds, clubs, spades) must exist. Use only artwork you have permission to use. The program cannot run without these files.

Reviewed Board Prototype

The archived draft contained two separate game loops, loaded the card-back image during every frame, duplicated dealt cards in the stock, and tested mouse coordinates against rectangles that were never positioned. The reviewed version below makes the intended corrections while keeping the original limited behavior.

import random
from pathlib import Path

import pygame


SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
CARD_WIDTH = 72
CARD_HEIGHT = 96
GREEN = (0, 128, 0)

SUITS = ("hearts", "diamonds", "clubs", "spades")
RANKS = ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")
CARD_DIRECTORY = Path(__file__).with_name("cards")


class Card:
    def __init__(self, rank, suit, image):
        self.rank = rank
        self.suit = suit
        self.image = image
        self.rect = self.image.get_rect()

    def draw(self, surface, x, y):
        self.rect.topleft = (x, y)
        surface.blit(self.image, self.rect)


def load_card_images():
    images = {}

    for suit in SUITS:
        for rank in RANKS:
            card_name = f"{rank}_of_{suit}"
            path = CARD_DIRECTORY / f"{card_name}.png"
            image = pygame.image.load(str(path)).convert_alpha()
            images[card_name] = pygame.transform.scale(
                image,
                (CARD_WIDTH, CARD_HEIGHT),
            )

    return images


def create_deck():
    return [f"{rank}_of_{suit}" for suit in SUITS for rank in RANKS]


def make_card(card_name, card_images):
    rank, suit = card_name.split("_of_")
    return Card(rank, suit, card_images[card_name])


def deal_tableau(deck, card_images):
    tableau = [[] for _ in range(7)]

    for column_index in range(7):
        for _ in range(column_index + 1):
            card_name = deck.pop()
            tableau[column_index].append(make_card(card_name, card_images))

    return tableau


pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Solitaire Board Prototype")
clock = pygame.time.Clock()

card_images = load_card_images()
card_back_path = CARD_DIRECTORY / "back.png"
card_back = pygame.image.load(str(card_back_path)).convert_alpha()
card_back = pygame.transform.scale(card_back, (CARD_WIDTH, CARD_HEIGHT))

deck = create_deck()
random.shuffle(deck)
tableau = deal_tableau(deck, card_images)
stock = deck.copy()
waste = []

stock_rect = pygame.Rect(50, 50, CARD_WIDTH, CARD_HEIGHT)
selected_column = None
running = True

while running:
    screen.fill(GREEN)

    for column_index, column in enumerate(tableau):
        for row_index, card in enumerate(column):
            x = 100 + column_index * 100
            y = 150 + row_index * 20
            card.draw(screen, x, y)

    if stock:
        screen.blit(card_back, stock_rect)

    if waste:
        waste[-1].draw(screen, 150, 50)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if stock and stock_rect.collidepoint(event.pos):
                waste.append(make_card(stock.pop(), card_images))
                continue

            for column_index, column in enumerate(tableau):
                if column and column[-1].rect.collidepoint(event.pos):
                    selected_column = column_index
                    break

        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            if selected_column is None:
                continue

            for target_index, column in enumerate(tableau):
                target_y = 150 + len(column) * 20
                drop_area = pygame.Rect(
                    100 + target_index * 100,
                    target_y,
                    CARD_WIDTH,
                    CARD_HEIGHT,
                )

                if drop_area.collidepoint(event.pos):
                    card = tableau[selected_column].pop()
                    tableau[target_index].append(card)
                    break

            selected_column = None

    pygame.display.flip()
    clock.tick(60)

pygame.quit()

The program handles Pygame’s event queue every frame, uses positioned Rect objects for hit testing, and limits the loop to 60 frames per second. The card-back image is loaded once rather than repeatedly inside the loop.

What the Prototype Does

With all required image files available, the code:

  1. creates a Pygame window;
  2. loads and scales the card images;
  3. shuffles a 52-card deck;
  4. deals 28 cards across seven tableau columns;
  5. keeps the remaining 24 cards in the stock;
  6. draws a stock card into the waste area when the stock is clicked; and
  7. allows the top card in a tableau column to be placed into a drop area in another column.

The placement behavior does not check whether a move is legal. It also does not animate a card under the pointer while the mouse moves, despite the archived draft describing the interaction as dragging.

What Is Still Missing

A complete Classic Solitaire implementation would still need:

  • face-up and face-down card state;
  • tableau move validation by rank and color;
  • movement of valid multi-card sequences;
  • four working foundation piles;
  • stock and waste rules, including any chosen recycle behavior;
  • correct handling of empty tableau columns;
  • synchronized visual and game state for every move;
  • a new-game flow and win-condition check; and
  • keyboard and other accessibility support beyond mouse input.

Those features and the required artwork were not present in the exported article, so they have not been invented here. Python syntax can be validated without the assets, but that does not prove functional correctness or rules-valid gameplay.