Как написать шахматы на python


 

·

23 min read
· Updated
nov 2022

· GUI Programming

The chess game is a pretty cool project idea for intermediate Python programmers. It’s good practice for making apps with GUIs while getting good at using classes. In this tutorial, you learn about:

  • Using the basics of pygame.
  • Learn how to code a chess game with Python classes.

Installation and Setting Up

Before we start coding, let’s first install the pygame module in the terminal:

$ pip install pygame

Once we installed the pygame, let’s move into setting up our environment by making the py files and folder we’re using in this order:

> python-chess
    > data
        > classes
            > pieces
                /* Bishop.py
                /* King.py
                /* Knight.py
                /* Pawn.py
                /* Queen.py
                /* Rook.py
            /* Board.py
            /* Piece.py
            /* Square.py
        > imgs
    /* main.py

Move the images of the chess icons you’ll use in the python/data/imgs/ directory. Make sure your image files are named [color's 1st letter]_[piece name].png just like this:

If you don’t have chess icons, you can use mine here.

Coding the Game

And now we’re done setting up; we can start coding now. Our chess game has two main code parts; creating the board and creating the pieces. The board will mainly focus on square positions and game rules, while the pieces focus on the piece they represent and the moves it has.

Making the Board

Let’s start by making the Square class. The Square class creates, colors, position, and draw each chess tile inside our game window:

# /* Square.py
import pygame

# Tile creator
class Square:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.abs_x = x * width
        self.abs_y = y * height
        self.abs_pos = (self.abs_x, self.abs_y)
        self.pos = (x, y)
        self.color = 'light' if (x + y) % 2 == 0 else 'dark'
        self.draw_color = (220, 208, 194) if self.color == 'light' else (53, 53, 53)
        self.highlight_color = (100, 249, 83) if self.color == 'light' else (0, 228, 10)
        self.occupying_piece = None
        self.coord = self.get_coord()
        self.highlight = False
        self.rect = pygame.Rect(
            self.abs_x,
            self.abs_y,
            self.width,
            self.height
        )

    # get the formal notation of the tile
    def get_coord(self):
        columns = 'abcdefgh'
        return columns[self.x] + str(self.y + 1)

    def draw(self, display):
        # configures if tile should be light or dark or highlighted tile
        if self.highlight:
            pygame.draw.rect(display, self.highlight_color, self.rect)
        else:
            pygame.draw.rect(display, self.draw_color, self.rect)
        # adds the chess piece icons
        if self.occupying_piece != None:
            centering_rect = self.occupying_piece.img.get_rect()
            centering_rect.center = self.rect.center
            display.blit(self.occupying_piece.img, centering_rect.topleft)

The first thing we’re gonna do is to make a class for making chess Square. Let’s start by adding the __init__() function to get the square’s width, height, x for a row, and y for the column.

With this basic information, we can fulfill other variables using them. As you see above, we have self.x and self.y while we also have self.abs_x and self.abs_y. self.abs_x and self.abs_y dictates where the chess tile is assigned to be drawn inside the window, and we compile them both in self.abs_pos.

The self.color tells the square tile should be light colored if it is divisible by 2 or instead dark if not, while the self.draw_color tells the color configuration for light and dark. We also have self.highlight_color which we use to highlight the tiles with the possible movement of a piece if it was selected. The self.rect configures the width, height, and location (using self.abs_x and self.abs_y) of a square or tile.

The get_coord() returns the name of the tile depending on its x and y based on the real board. Letters symbolize rows, and the number symbolizes columns. Like «a1», it is the bottom leftmost tile in a chess board.

The draw(), executes the configurations we did by drawing the tile in the canvas, in the color it was assigned. The second if statement tells that if the square has a piece in this position, you should access its icon and place it inside the tile.

Now we have a class for making a square. Let’s make another class for handling tiles and the whole board.

# /* Board.py

import pygame
from data.classes.Square import Square
from data.classes.pieces.Rook import Rook
from data.classes.pieces.Bishop import Bishop
from data.classes.pieces.Knight import Knight
from data.classes.pieces.Queen import Queen
from data.classes.pieces.King import King
from data.classes.pieces.Pawn import Pawn

# Game state checker
class Board:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.tile_width = width // 8
        self.tile_height = height // 8
        self.selected_piece = None
        self.turn = 'white'
        self.config = [
            ['bR', 'bN', 'bB', 'bQ', 'bK', 'bB', 'bN', 'bR'],
            ['bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP'],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP'],
            ['wR', 'wN', 'wB', 'wQ', 'wK', 'wB', 'wN', 'wR'],
        ]
        self.squares = self.generate_squares()
        self.setup_board()

    def generate_squares(self):
        output = []
        for y in range(8):
            for x in range(8):
                output.append(
                    Square(x,  y, self.tile_width, self.tile_height)
                )
        return output

    def get_square_from_pos(self, pos):
        for square in self.squares:
            if (square.x, square.y) == (pos[0], pos[1]):
                return square

    def get_piece_from_pos(self, pos):
        return self.get_square_from_pos(pos).occupying_piece

In making the whole chess board, it’s important to know first what is the width and height of the game window so we can divide it into 8 rows with 8 columns to identify our tiles’ exact size.

The self.config represents the chessboard configuration with a 2D list having our pieces with their default position. Below it, we configure the self.squares with a value calling our self.generate_squares() for making chess tiles and putting them all in a list.

Now let’s create the other parts of the Board, including the self.setup_board() we called above.

    def setup_board(self):
        for y, row in enumerate(self.config):
            for x, piece in enumerate(row):
                if piece != '':
                    square = self.get_square_from_pos((x, y))
                    # looking inside contents, what piece does it have
                    if piece[1] == 'R':
                        square.occupying_piece = Rook(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    # as you notice above, we put `self` as argument, or means our class Board
                    elif piece[1] == 'N':
                        square.occupying_piece = Knight(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'B':
                        square.occupying_piece = Bishop(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'Q':
                        square.occupying_piece = Queen(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'K':
                        square.occupying_piece = King(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'P':
                        square.occupying_piece = Pawn(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )

The setup_board() creates each piece, and puts them in their respective place by mapping the self.config with the whole board. If the current value of piece in self.config is an empty string or '', then the tile must be empty, and if not, it will access its respective tile position through the current value of x and y. Each piece from self.config will be identified according to the capital letter it has, except the Knight.

If we got 'N', then it’s a Knight if 'P', then it’s a Pawn. If 'R', then it’s a Rook, ‘B’ for Bishop, and so on. After we configure the letters, we’ll overwrite the current square.occupying_piece with a value of their piece class with the color depending on the first value of the piece string. As you noticed here and in the other statement:

                    if piece[1] == 'R':
                        square.occupying_piece = Rook(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self

We put a self as an argument for the Rook class. That means we put our current class, Board, as an argument.

We need a function that detects each click in our game. So let’s make handle_click() in our Board class:

    def handle_click(self, mx, my):
        x = mx // self.tile_width
        y = my // self.tile_height
        clicked_square = self.get_square_from_pos((x, y))
        if self.selected_piece is None:
            if clicked_square.occupying_piece is not None:
                if clicked_square.occupying_piece.color == self.turn:
                    self.selected_piece = clicked_square.occupying_piece
        elif self.selected_piece.move(self, clicked_square):
            self.turn = 'white' if self.turn == 'black' else 'black'
        elif clicked_square.occupying_piece is not None:
            if clicked_square.occupying_piece.color == self.turn:
                self.selected_piece = clicked_square.occupying_piece

The handle_click() accepts the x (mx) and y (my) coordinates of where you click inside the game window as an argument. The x and y variables inside this function compute what row and column you clicked, then we pass its outcomes to clicked_square to get the square or the tile.

This configuration can now receive our every click inside the game window. The following if/else statements process our click if we’re making a move or just clicking around.

It all works once you clicked somewhere inside the game window so let’s assume that you’re playing the game using a white piece and you’ve already clicked. If we haven’t selected any piece yet, it will look as if the tile you clicked has a piece, and if it’s your colors turn, and if yes, it will be your self.selected_piece.

With the help of other classes, your piece’s possible move will be highlighted in the game. After selecting a piece’s move, it will convert the self.turn into the next player’s piece color.

Now that we have a selected piece and chosen a move, it will make the move. I’ll explain the other moving later as we make the classes for that piece.

Let’s add another feature for the Board class; we are adding functions that check if a player is in check or checkmate.

    # check state checker
    def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2, y2)]
        output = False
        king_pos = None
        changing_piece = None
        old_square = None
        new_square = None
        new_square_old_piece = None
        if board_change is not None:
            for square in self.squares:
                if square.pos == board_change[0]:
                    changing_piece = square.occupying_piece
                    old_square = square
                    old_square.occupying_piece = None
            for square in self.squares:
                if square.pos == board_change[1]:
                    new_square = square
                    new_square_old_piece = new_square.occupying_piece
                    new_square.occupying_piece = changing_piece
        pieces = [
            i.occupying_piece for i in self.squares if i.occupying_piece is not None
        ]
        if changing_piece is not None:
            if changing_piece.notation == 'K':
                king_pos = new_square.pos
        if king_pos == None:
            for piece in pieces:
                if piece.notation == 'K' and piece.color == color:
                        king_pos = piece.pos
        for piece in pieces:
            if piece.color != color:
                for square in piece.attacking_squares(self):
                    if square.pos == king_pos:
                        output = True
        if board_change is not None:
            old_square.occupying_piece = changing_piece
            new_square.occupying_piece = new_square_old_piece
        return output

For every move we make, the is_in_check() function will be called, whenever the board_change is not empty.

In the first iteration, it locates the position of the old tile, passes its current piece in changing_piece, and empty that tile while in the second iteration, it catches the new tile position and passes its current piece to new_square_old_piece and give it a new piece from changing_piece.

Once our changing_piece is not empty, it’ll try to identify if it’s a King by getting its self.notation. If so, it’ll override the king_pos and give it the value of new_square.pos.

Note: The self.notation is a variable from the pieces’ class, that serves as an identification containing their letter symbols.

The next thing we’ll try to identify is what the enemy piece does to do the check to our player’s King, where we check starting by if piece.color != color.

        for piece in pieces:
            if piece.color != color:
                for square in piece.attacking_squares(self):
                    if square.pos == king_pos:
                        output = True

With the following code above, we can iterate through enemy pieces and check their attacking_squares, which gets all the possible moves of a piece. If a piece has a position in attacking_squares the same value as king_pos, which means one of the players is checked, so we set the output to True. The output tells if a King is in check or not, so we have to return it.

Now let’s make the is_in_checkmate() function for identifying if we have a winner yet:

    # checkmate state checker
    def is_in_checkmate(self, color):
        output = False
        for piece in [i.occupying_piece for i in self.squares]:
            if piece != None:
                if piece.notation == 'K' and piece.color == color:
                    king = piece
        if king.get_valid_moves(self) == []:
            if self.is_in_check(color):
                output = True
        return output

Once we get the King the same color as the argument we passed, it’ll try to see if it has any moves left. If none, then it’ll check if the player is in check. If that’s the case, then it will return the value of the output which is True, which means the side of the color we passed is checkmate.

Now we have all the board configurations; it’s time to add the final function for the Board class which is the draw() function:

    def draw(self, display):
        if self.selected_piece is not None:
            self.get_square_from_pos(self.selected_piece.pos).highlight = True
            for square in self.selected_piece.get_valid_moves(self):
                square.highlight = True
        for square in self.squares:
            square.draw(display)

This function highlights all the possible moves of a piece once selected while it is its color’s turn.

Making the Pieces

Now we’re finished with Board class, let’s make another class for pieces in Piece.py.

Let’s start by adding a function that gets all the available moves and a checker if the next player got checked by the previous player:

# /* Piece.py

import pygame

class Piece:
    def __init__(self, pos, color, board):
        self.pos = pos
        self.x = pos[0]
        self.y = pos[1]
        self.color = color
        self.has_moved = False

    def get_moves(self, board):
        output = []
        for direction in self.get_possible_moves(board):
            for square in direction:
                if square.occupying_piece is not None:
                    if square.occupying_piece.color == self.color:
                        break
                    else:
                        output.append(square)
                        break
                else:
                    output.append(square)
        return output

The get_moves() gets all the available moves of the current player, including attacking the enemy piece. If an opponent piece is in range of a piece’s move, the piece can capture it where its range will limit on these opponent’s piece tile position through output.append(square) then break unless the piece is Knight which can move in an ‘L-shape’.

    def get_valid_moves(self, board):
        output = []
        for square in self.get_moves(board):
            if not board.is_in_check(self.color, board_change=[self.pos, square.pos]):
                output.append(square)
        return output

Before proceeding for our current player in making a move, the get_valid_moves() checks first if the last player does a move that checked our current player. And if not, then it will return the available moves.

To make the pieces work, we’re adding a move() function that handles every move we make on the board:

    def move(self, board, square, force=False):
        for i in board.squares:
            i.highlight = False
        if square in self.get_valid_moves(board) or force:
            prev_square = board.get_square_from_pos(self.pos)
            self.pos, self.x, self.y = square.pos, square.x, square.y
            prev_square.occupying_piece = None
            square.occupying_piece = self
            board.selected_piece = None
            self.has_moved = True
            # Pawn promotion
            if self.notation == ' ':
                if self.y == 0 or self.y == 7:
                    from data.classes.pieces.Queen import Queen
                    square.occupying_piece = Queen(
                        (self.x, self.y),
                        self.color,
                        board
                    )
            # Move rook if king castles
            if self.notation == 'K':
                if prev_square.x - self.x == 2:
                    rook = board.get_piece_from_pos((0, self.y))
                    rook.move(board, board.get_square_from_pos((3, self.y)), force=True)
                elif prev_square.x - self.x == -2:
                    rook = board.get_piece_from_pos((7, self.y))
                    rook.move(board, board.get_square_from_pos((5, self.y)), force=True)
            return True
        else:
            board.selected_piece = None
            return False

    # True for all pieces except pawn
    def attacking_squares(self, board):
        return self.get_moves(board)

It takes board and square as arguments. If the tile we select to move our chosen piece is in self.get_valid_moves(), the move is valid to execute. To make it happen, the move() function will get the current square using board.get_square_from_pos(self.pos) and save it in prev_square and get its positions square.pos, square.x, square.y and save it in self.pos, self.x, and self.y for further use.

Then the function will empty the prev_square, and the piece (self — the current chess piece class) will be moved to the square.occupying_piece.

Chess has cool features; some of them are castling and pawn promotion, and that’s what we do next.

If the notation of the piece we’ve just moved in is ' ', which is a pawn, and it reaches row 0 (for white pawns) or row 7 (for black pawns), the pawn will be replaced by another queen of the same color.

And if the piece’s notation is 'K' and then moved 2 tiles to the left or right, it means the player’s move is casting.

Making a Class for Each Piece

Now that we’ve finished the Square, Board, and Piece classes, it’s time to create different classes for every piece type. Each piece will have the main Piece class as its parent class:

# /* Pawn.py

import pygame

from data.classes.Piece import Piece

class Pawn(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_pawn.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 35, board.tile_height - 35))
        self.notation = ' '

    def get_possible_moves(self, board):
        output = []
        moves = []
        # move forward
        if self.color == 'white':
            moves.append((0, -1))
            if not self.has_moved:
                moves.append((0, -2))
        elif self.color == 'black':
            moves.append((0, 1))
            if not self.has_moved:
                moves.append((0, 2))
        for move in moves:
            new_pos = (self.x, self.y + move[1])
            if new_pos[1] < 8 and new_pos[1] >= 0:
                output.append(
                    board.get_square_from_pos(new_pos)
                )
        return output

    def get_moves(self, board):
        output = []
        for square in self.get_possible_moves(board):
            if square.occupying_piece != None:
                break
            else:
                output.append(square)
        if self.color == 'white':
            if self.x + 1 < 8 and self.y - 1 >= 0:
                square = board.get_square_from_pos(
                    (self.x + 1, self.y - 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
            if self.x - 1 >= 0 and self.y - 1 >= 0:
                square = board.get_square_from_pos(
                    (self.x - 1, self.y - 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
        elif self.color == 'black':
            if self.x + 1 < 8 and self.y + 1 < 8:
                square = board.get_square_from_pos(
                    (self.x + 1, self.y + 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
            if self.x - 1 >= 0 and self.y + 1 < 8:
                square = board.get_square_from_pos(
                    (self.x - 1, self.y + 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
        return output

    def attacking_squares(self, board):
        moves = self.get_moves(board)
        # return the diagonal moves 
        return [i for i in moves if i.x != self.x]

Here is our code for the Pawn pieces, whether it is black or white. As you notice, we have get_moves() and attacking_square() functions here in the Pawn class, just like the functions we have in the Piece class but given with a different script. It is because pawn pieces are basically allowed to move 1 step at a time away from their team position. A pawn also has 3 possible moves; the pawn can move up to 2 tiles from its starting position only, can move 1 step forward at a time, and can capture a piece 1 diagonally step at a time.

As we noticed, we have another function which is the get_possible_moves(). As of its name, it gets all the possible moves of a piece base on the current state of the board.

Now let’s move to do the other codes for other pieces.

Code for Knight.py:

# /* Kinght.py

import pygame
from data.classes.Piece import Piece

class Knight(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_knight.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'N'

    def get_possible_moves(self, board):
        output = []
        moves = [
            (1, -2),
            (2, -1),
            (2, 1),
            (1, 2),
            (-1, 2),
            (-2, 1),
            (-2, -1),
            (-1, -2)
        ]
        for move in moves:
            new_pos = (self.x + move[0], self.y + move[1])
            if (
                new_pos[0] < 8 and
                new_pos[0] >= 0 and 
                new_pos[1] < 8 and 
                new_pos[1] >= 0
            ):
                output.append([
                    board.get_square_from_pos(
                        new_pos
                    )
                ])
        return output

Code for Bishop.py:

# /* Bishop.py

import pygame
from data.classes.Piece import Piece

class Bishop(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_bishop.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'B'

    def get_possible_moves(self, board):
        output = []
        moves_ne = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y - i < 0:
                break
            moves_ne.append(board.get_square_from_pos(
                (self.x + i, self.y - i)
            ))
        output.append(moves_ne)
        moves_se = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y + i > 7:
                break
            moves_se.append(board.get_square_from_pos(
                (self.x + i, self.y + i)
            ))
        output.append(moves_se)
        moves_sw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y + i > 7:
                break
            moves_sw.append(board.get_square_from_pos(
                (self.x - i, self.y + i)
            ))
        output.append(moves_sw)
        moves_nw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y - i < 0:
                break
            moves_nw.append(board.get_square_from_pos(
                (self.x - i, self.y - i)
            ))
        output.append(moves_nw)
        return output

Code for Rook.py:

# /* Rook.py

import pygame

from data.classes.Piece import Piece

class Rook(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_rook.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'R'

    def get_possible_moves(self, board):
        output = []
        moves_north = []
        for y in range(self.y)[::-1]:
            moves_north.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_north)
        moves_east = []
        for x in range(self.x + 1, 8):
            moves_east.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_east)
        moves_south = []
        for y in range(self.y + 1, 8):
            moves_south.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_south)
        moves_west = []
        for x in range(self.x)[::-1]:
            moves_west.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_west)
        return output

Code for Queen.py:

# /* Queen.py

import pygame
from data.classes.Piece import Piece

class Queen(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_queen.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'Q'

    def get_possible_moves(self, board):
        output = []
        moves_north = []
        for y in range(self.y)[::-1]:
            moves_north.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_north)
        moves_ne = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y - i < 0:
                break
            moves_ne.append(board.get_square_from_pos(
                (self.x + i, self.y - i)
            ))
        output.append(moves_ne)
        moves_east = []
        for x in range(self.x + 1, 8):
            moves_east.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_east)
        moves_se = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y + i > 7:
                break
            moves_se.append(board.get_square_from_pos(
                (self.x + i, self.y + i)
            ))
        output.append(moves_se)
        moves_south = []
        for y in range(self.y + 1, 8):
            moves_south.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_south)
        moves_sw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y + i > 7:
                break
            moves_sw.append(board.get_square_from_pos(
                (self.x - i, self.y + i)
            ))
        output.append(moves_sw)
        moves_west = []
        for x in range(self.x)[::-1]:
            moves_west.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_west)
        moves_nw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y - i < 0:
                break
            moves_nw.append(board.get_square_from_pos(
                (self.x - i, self.y - i)
            ))
        output.append(moves_nw)
        return output

Code for King.py:

# /* King.py

import pygame
from data.classes.Piece import Piece

class King(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_king.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'K'

    def get_possible_moves(self, board):
        output = []
        moves = [
            (0,-1), # north
            (1, -1), # ne
            (1, 0), # east
            (1, 1), # se
            (0, 1), # south
            (-1, 1), # sw
            (-1, 0), # west
            (-1, -1), # nw
        ]
        for move in moves:
            new_pos = (self.x + move[0], self.y + move[1])
            if (
                new_pos[0] < 8 and
                new_pos[0] >= 0 and 
                new_pos[1] < 8 and 
                new_pos[1] >= 0
            ):
                output.append([
                    board.get_square_from_pos(
                        new_pos
                    )
                ])
        return output

    def can_castle(self, board):
        if not self.has_moved:
            if self.color == 'white':
                queenside_rook = board.get_piece_from_pos((0, 7))
                kingside_rook = board.get_piece_from_pos((7, 7))
                if queenside_rook != None:
                    if not queenside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 7)) for i in range(1, 4)
                        ] == [None, None, None]:
                            return 'queenside'
                if kingside_rook != None:
                    if not kingside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 7)) for i in range(5, 7)
                        ] == [None, None]:
                            return 'kingside'
            elif self.color == 'black':
                queenside_rook = board.get_piece_from_pos((0, 0))
                kingside_rook = board.get_piece_from_pos((7, 0))
                if queenside_rook != None:
                    if not queenside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 0)) for i in range(1, 4)
                        ] == [None, None, None]:
                            return 'queenside'
                if kingside_rook != None:
                    if not kingside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 0)) for i in range(5, 7)
                        ] == [None, None]:
                            return 'kingside'

    def get_valid_moves(self, board):
        output = []
        for square in self.get_moves(board):
            if not board.is_in_check(self.color, board_change=[self.pos, square.pos]):
                output.append(square)
        if self.can_castle(board) == 'queenside':
            output.append(
                board.get_square_from_pos((self.x - 2, self.y))
            )
        if self.can_castle(board) == 'kingside':
            output.append(
                board.get_square_from_pos((self.x + 2, self.y))
            )
        return output

Let’s finish the game by adding code in main.py that runs our whole game:

import pygame

from data.classes.Board import Board

pygame.init()

WINDOW_SIZE = (600, 600)
screen = pygame.display.set_mode(WINDOW_SIZE)

board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1])

def draw(display):
	display.fill('white')
	board.draw(display)
	pygame.display.update()


if __name__ == '__main__':
	running = True
	while running:
		mx, my = pygame.mouse.get_pos()
		for event in pygame.event.get():
			# Quit the game if the user presses the close button
			if event.type == pygame.QUIT:
				running = False
			elif event.type == pygame.MOUSEBUTTONDOWN: 
       			# If the mouse is clicked
				if event.button == 1:
					board.handle_click(mx, my)
		if board.is_in_checkmate('black'): # If black is in checkmate
			print('White wins!')
			running = False
		elif board.is_in_checkmate('white'): # If white is in checkmate
			print('Black wins!')
			running = False
		# Draw the board
		draw(screen)

As you see above, we had screen and board variable, which has pretty similar arguments but not really.

The screen handles the rendering of the chess board on the screen so we can see what’s happening in the board. The code pygame.display.set_mode(WINDOW_SIZE) creates the game window.

While we use the board for making and handling tiles, tile positions, and what piece a chess square has. As you remember, in the Board class code, we give it two arguments: the game window’s length and width.

To keep the game running, we give it a while loop that runs as long as the value of running is True.

The mx, my = pygame.mouse.get_pos() locates the current position of your mouse as long as it’s inside the game window. If you add print(mx, my) below this code, you’ll see the current mouse position, and its value changes every time you hover it inside the window.

The event.type == pygame.MOUSEBUTTONDOWN catches every click you make. To identify if a player is making a move, every time it catches a player doing a click, the current position of the mouse we get from pygame.mouse.get_pos() will be sent in Board.handle_click(), and process your click back there.

Ok, now let’s try this game. If it’s working so in your terminal, move to the directory where our Main.py file was saved, then run the Main.py. Once you run the file, the game will start immediately:

Start clicking on the pieces that can move, and you’ll see available moves:

Conclusion

To simplify it, always remember that the chess game has two main parts, the board, and the pieces.

The board is in charge of every tile’s name & position and the rules of the game, while the piece classes take care of moves and attacks for every piece.

To make the board, you should have a Square class that creates the handling chess tiles that also mind the piece it contains, and another class called Board, which contains the game rules. We also need to do class for every chess piece, from the Pawn to the King. And that’s how you make a chess game with Python using only classes and pygame!

You can check the complete code here.

Learn also: How to Build a GUI Voice Recorder App in Python

Happy coding ♥

View Full Code

Read Also

How to Make a Drawing Program in Python

How to Make a Planet Simulator with PyGame in Python

How to Make Buttons in PyGame

Comment panel

В предыдущей статье я разработал программу игры в шахматы за один день, используя Python.

Теперь я решил поэкспериментировать, чтобы значительно сократить это время, используя chatGPT для генерации кода. ChatGPT — это чат-бот, основанный на GPT3 от OpenAI, о котором сейчас говорят все.

Цель этого эксперимента — проверить способности написания кода chatGPT.

Давайте посмотрим на диалог, который у меня был с chatGPT

Вопрос: Давай вместе напишем программу шахмат на Python

Ответ: Конечно, это звучит очень интересно! Прежде чем мы начнём, я должен упомянуть, что написание полнофункциональной шахматной программы является нетривиальной задачей и, вероятно, потребует значительного количества времени и усилий. Однако мы, безусловно, можем начать с обсуждения некоторых основных концепций и идей, которые были бы задействованы в таком проекте.

Для начала нам нужно было бы представить состояние шахматной доски в нашей программе. Это можно было бы сделать с помощью двумерного массива, где каждый элемент массива представляет собой один квадрат на шахматной доске. Нам нужно было бы сохранить тип фигуры, которая занимает каждый квадрат, а также любую другую соответствующую информацию, такую как переместилась ли фигура, угрожают ли ей какие-либо другие фигуры и т.д.

Как только у нас появится способ представить состояние шахматной доски, нам нужно будет внедрить правила шахмат. Это включало бы такие вещи, как обеспечение того, чтобы фигуры двигались только теми способами, которые им разрешены (например, слон может двигаться только по диагонали), предотвращение совершения игроками незаконных ходов (таких как перемещение фигуры на квадрат, который занят одной из их собственных фигур), и обработка специальных ходов, таких как рокировка и взятие на проходе.

Далее нам нужно было бы реализовать логику игры, которая включала бы отслеживание того, чья очередь, проверку окончания игры (например, когда игрок ставит мат) и обработку других аспектов игры, таких как продвижение пешки и захват фигур.

Наконец, нам нужно было бы реализовать какой-то пользовательский интерфейс, такой как интерфейс командной строки или графический пользовательский интерфейс, чтобы позволить игрокам взаимодействовать с программой и делать свои ходы.

В целом, при написании шахматной программы необходимо учитывать множество различных аспектов, и для её правильного выполнения потребуется хорошее понимание правил шахмат и тщательное планирование. Тем не менее, я думаю, что это был бы сложный и полезный проект для работы, и я с нетерпением жду возможности услышать больше о ваших идеях о том, как подойти к нему!

Вопрос: Начни с генерации кода шахматной доски

Ответ: Конечно, мы можем начать с внедрения шахматной доски. Как я упоминал ранее, одним из способов сделать это было бы использование двумерного массива, где каждый элемент массива представляет собой один квадрат на шахматной доске. Вот один из возможных способов сделать это:

# Define a constant for the size of the chess board
BOARD_SIZE = 8

# Create an empty array to represent the chess board
board = [[None for i in range(BOARD_SIZE)] for j in range(BOARD_SIZE)]

# Populate the array with the initial positions of the pieces
board[0][0] = «Rook»
board[0][1] = «Knight»
board[0][2] = «Bishop»
board[0][3] = «Queen»
board[0][4] = «King»
board[0][5] = «Bishop»
board[0][6] = «Knight»
board[0][7] = «Rook»

for i in range(BOARD_SIZE):
board[1][i] = «Pawn»
board[6][i] = «Pawn»

board[7][0] = «Rook»
board[7][1] = «Knight»
board[7][2] = «Bishop»
board[7][3] = «Queen»
board[7][4] = «King»
board[7][5] = «Bishop»
board[7][6] = «Knight»
board[7][7] = «Rook»

# Print out the resulting board
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
print(board[i][j], end=» «)
print()

Этот код создаёт массив 8×8 под названием board и заполняет его начальными позициями фигур. Затем он выводит получившуюся доску, которая должна выглядеть примерно так:

Rook Knight Bishop Queen King Bishop Knight Rook
Pawn Pawn Pawn Pawn Pawn Pawn Pawn Pawn
None None None None None None None None
None None None None None None None None
None None None None None None None None
None None None None None None None None
Pawn Pawn Pawn Pawn Pawn Pawn Pawn Pawn
Rook Knight Bishop Queen King Bishop Knight Rook

Конечно, это всего лишь один из возможных способов представления шахматной доски, и есть много других способов, которыми мы могли бы это сделать. Например, вместо того, чтобы использовать строки для представления типов фрагментов, мы могли бы использовать целые числа или другие типы данных для представления различных типов фрагментов. Кроме того, мы могли бы хранить дополнительную информацию о каждой детали, такую как её цвет, перемещалась ли она и так далее.

Как только у нас будет способ представить шахматную доску, мы сможем перейти к реализации шахматных правил. Это будет включать в себя написание кода для обработки таких вещей, как перемещение фигур, проверка на наличие незаконных ходов и обработка специальных ходов, таких как рокировка и взятие на проходе.

Вопрос: напиши функцию, которая генерирует все возможные для передвижения позиции

Ответ: Конечно, мы можем написать функцию, которая генерирует все возможные ходы для данной позиции на шахматной доске. Чтобы сделать это, нам нужно будет рассмотреть правила шахмат, которые диктуют, как может двигаться фигура каждого типа. Вот один из возможных способов реализации этой функции:

def get_possible_moves(board, x, y):
# This function will return a list of all possible moves that can be made
# from the given position (x, y) on the given board.

# First, we need to check what type of piece is located at the given position
piece = board[x][y]
if piece is None:
# If there is no piece at the given position, there are no possible moves
return []

# Next, we need to handle the different types of pieces and generate the
# list of possible moves for each type
if piece == «Pawn»:
# Pawns can move forward one square, unless there is another piece
# blocking their path. They can also capture pieces that are located
# on the squares diagonally in front of them.
moves = []
if board[x+1][y] is None:
moves.append((x+1, y))
if y > 0 and board[x+1][y-1] is not None:
moves.append((x+1, y-1))
if y < BOARD_SIZE-1 and board[x+1][y+1] is not None:
moves.append((x+1, y+1))

elif piece == «Rook»:
# Rooks can move any number of squares horizontally or vertically,
# but they cannot move through other pieces.
moves = []
for i in range(x+1, BOARD_SIZE):
if board[i][y] is None:
moves.append((i, y))
else:
break
for i in range(x-1, -1, -1):
if board[i][y] is None:
moves.append((i, y))
else:
break
for j in range(y+1, BOARD_SIZE):
if board[x][j] is None:
moves.append((x, j))
else:
break
for j in range(y-1, -1, -1):
if board[x][j] is None:
moves.append((x, j))
else:
break

elif piece == «Knight»:
# Knights can move in an «L» shape, either two squares horizontally
# and one square vertically, or two squares vertically and one
# square horizontally. They can move over other pieces.
moves = []
if x < BOARD_SIZE-2 and y > 0:
moves.append((x+2, y-1))
if x < BOARD_SIZE-2 and y < BOARD_SIZE-1:
moves.append((x+2, y+1))
if x > 1 and y > 0:
moves.append((x-2, y-1))
if x > 1 and y < BOARD_SIZE-1:
moves.append((x-2, y+1))
if x < BOARD_SIZE-1

Вопрос: что с остальными фигурами?

Ответ: В коде, который я предоставил ранее, я включил только правила перемещения пешек, ладей и коней. Правила для других фигур (слона, ферзя и короля) аналогичны, но немного отличаются. Вот как может выглядеть остальная часть кода:

elif piece == «Bishop»:
# Bishops can move any number of squares diagonally, but they cannot
# move through other pieces.
moves = []
for i in range(1, BOARD_SIZE):
if x+i < BOARD_SIZE and y+i < BOARD_SIZE and board[x+i][y+i] is None:
moves.append((x+i, y+i))
else:
break
for i in range(1, BOARD_SIZE):
if x-i >= 0 and y+i < BOARD_SIZE and board[x-i][y+i] is None:
moves.append((x-i, y+i))
else:
break
for i in range(1, BOARD_SIZE):
if x+i < BOARD_SIZE and y-i >= 0 and board[x+i][y-i] is None:
moves.append((x+i, y-i))
else:
break
for i in range(1, BOARD_SIZE):
if x-i >= 0 and y-i >= 0 and board[x-i][y-i] is None:
moves.append((x-i, y-i))
else:
break

elif piece == «Queen»:
# Queens can move any number of squares horizontally, vertically,
# or diagonally, but they cannot move through other pieces.
moves = []
for i in range(1, BOARD_SIZE):
if x+i < BOARD_SIZE and board[x+i][y] is None:
moves.append((x+i, y))
else:
break
for i in range(1, BOARD_SIZE):
if x-i >= 0 and board[x-i][y] is None:
moves.append((x-i, y))
else:
break
for j in range(1, BOARD_SIZE):
if y+j < BOARD_SIZE and board[x][y+j] is None:
moves.append((x, y+j))
else:
break
for j in range(1, BOARD_SIZE):
if y-j >= 0 and board[x][y-j] is None:
moves.append((x, y-j))
else:
break
for i in range(1, BOARD_SIZE):
if x+i < BOARD_SIZE and y+i < BOARD_SIZE and board[x+i][y+i] is None:
moves.append((x+i, y+i))
else:
break
for i in range(1, BOARD_SIZE):
if x-i >= 0 and y+i < BOARD_SIZE and board[x-i][y+i] is None:
moves.append((x-i, y+i))
else:
break
for i in

Вопрос: пожалуйста, продолжай

Ответ: Конечно! Вот остальная часть кода для функции get_possible_moves():

range(1, BOARD_SIZE):
if x+i < BOARD_SIZE and y-i >= 0 and board[x+i][y-i] is None:
moves.append((x+i, y-i))
else:
break
for i in range(1, BOARD_SIZE):
if x-i >= 0 and y-i >= 0 and board[x-i][y-i] is None:
moves.append((x-i, y-i))
else:
break

elif piece == «King»:
# Kings can move one square in any direction (horizontally, vertically,
# or diagonally), but they cannot move into check (i.e. they cannot
# move into a square that is threatened by an enemy piece).
moves = []
if x > 0:
moves.append((x-1, y))
if x < BOARD_SIZE-1:
moves.append((x+1, y))
if y > 0:
moves.append((x, y-1))
if y < BOARD_SIZE-1:
moves.append((x, y+1))
if x > 0 and y > 0:
moves.append((x-1, y-1))
if x > 0 and y < BOARD_SIZE-1:
moves.append((x-1, y+1))
if x < BOARD_SIZE-1 and y > 0:
moves.append((x+1, y-1))
if x < BOARD_SIZE-1 and y < BOARD_SIZE-1:
moves.append((x+1, y+1))

# Once we have generated the list of possible moves, we can return it
return moves

Этот код обрабатывает правила перемещения фигур каждого типа и генерирует список возможных ходов для данной позиции. Например, если мы вызовем get_possible_moves(board, 0, 1), функция вернёт список возможных ходов для коня, который изначально расположен на квадрате (0,1) на шахматной доске. Она должно включать все квадраты, на которые конь может переместиться за один ход, такие как (2,0), (2,2), (1,3), и так далее.

Конечно, это всего лишь один из способов реализации функции get_possible_moves(), и есть много других способов, которыми мы могли бы это сделать. Например, мы могли бы использовать различные структуры данных для представления шахматной доски и возможных ходов, или мы могли бы включить дополнительную информацию в список возможных ходов. Однако я надеюсь, что это даёт вам общее представление о том, как мы могли бы подойти к этой проблеме. Дайте мне знать, если у вас возникнут какие-либо другие вопросы!

Вопрос: можешь ли ты написать функцию, которая генерирует следующий наилучший ход?

Ответ: Можно написать функцию, которая генерирует следующий “лучший” ход для данной позиции на шахматной доске, но важно отметить, что это очень сложная задача для решения, и универсального варианта не существует. На самом деле, даже самые продвинутые шахматные программы в мире не способны последовательно находить абсолютный “лучший” ход в каждой ситуации.

Одним из возможных способов решения этой проблемы было бы использование метода, называемого “minimax” поиском, который является распространенным методом, используемым в игровых программах искусственного интеллекта. Основная его идея заключается в том, чтобы рассмотреть все возможные ходы, которые могут быть сделаны из текущей позиции, а затем для каждого из этих ходов рассмотреть все возможные контрдвижения, которые мог бы сделать противник. Затем этот алгоритм “минимизирует” значение лучшего хода противника, одновременно “максимизируя” значение хода исходного игрока, чтобы найти ход, который с наибольшей вероятностью приведёт к победе (или, по крайней мере, не к проигрышу).

Чтобы реализовать это, нам нужно было бы определить функцию “value”, которая присваивает числовой счёт каждой позиции на шахматной доске. Эта функция учитывала бы такие факторы, как относительная сила фигур каждого игрока, мобильность фигур каждого игрока и так далее. Затем алгоритм minimax будет использовать эту функцию значений для оценки каждого возможного хода и контрдвижения и выбора хода, который приводит к наибольшему общему баллу.

Однако, как я упоминал ранее, это очень сложная для решения проблема, и даже самые продвинутые шахматные программы не всегда способны найти абсолютный “лучший” ход. Кроме того, производительность алгоритма minimax может быть очень низкой, поскольку он должен учитывать каждый возможный ход и контрдвижение, чтобы найти наилучший ход. По этим причинам часто необходимо использовать различные методы оптимизации, такие как альфа-бета-обрезка и таблицы транспозиции, чтобы сделать этот алгоритм более эффективным.

В целом, хотя возможно написать функцию, которая генерирует следующий “лучший” ход для данной позиции на шахматной доске, это сложная задача, которая потребует хорошего понимания игровых методов искусственного интеллекта и тщательного планирования и реализации. Я надеюсь, что это даёт вам общее представление о том, как можно было бы подойти к этой проблеме, но дайте мне знать, если у вас возникнут какие-либо другие вопросы!

Вопрос: можешь ли ты сгенерировать код на python?

Ответ: Конечно, вот пример того, как алгоритм minimax может быть реализован на Python. Имейте в виду, что это всего лишь один из возможных способов реализации алгоритма, и есть много других способов, которыми мы могли бы это сделать. Кроме того, этот код не включает в себя все оптимизации, которые обычно используются в реальных шахматных программах, поэтому он будет не очень эффективен для больших шахматных позиций.

def minimax(board, depth, maximizingPlayer):
# This function will use the minimax algorithm to find the best move for
# the current player. The «board» parameter is the current state of the
# chess board, the «depth» parameter is the depth of the search tree (i.e.
# how many moves to look ahead), and the «maximizingPlayer» parameter
# indicates whether the current player is the maximizing player (True) or
# the minimizing player (False).

# If the search tree has reached the maximum depth, we can evaluate the
# current position using the value function and return that value.
if depth == 0:
return evaluate(board)

# Generate a list of all possible moves that can be made from the current
# position.
moves = get_possible_moves(board)

# If there are no possible moves, the game is over and we can evaluate
# the final position using the value function.
if len(moves) == 0:
return evaluate(board)

# If the current player is the maximizing player, we want to find the
# move that leads to the highest score, so we initialize the bestValue
# variable to a very low value.
if maximizingPlayer:
bestValue = -float(«inf»)

# Loop through all of the possible moves and compute the score for
# each one.
for move in moves:
# Make the move on a copy of the board.
newBoard = make_move(board, move)

# Recursively call the minimax function to find the best move
# for the opponent.
value = minimax(newBoard, depth-1, False)

# Update the bestValue variable if the current move leads to a
# higher score than the previous best move.
bestValue = max(bestValue, value)

# Once we have considered all of the possible moves, we can return
# the best score.
return bestValue

# If the current player is the minimizing player, we want to find the
# move that leads to the lowest score, so we initialize the bestValue
# variable to a very high value.
else:
bestValue = float(«inf»)

# Loop through all of the possible moves and compute the score for
# each one.
for move in moves:
# Make the move on a copy of the board.
newBoard = make_move(board, move)

# Recursively call the minimax function to find the best move
# for the opponent.
value = minimax(newBoard, depth-1, True)

# Update the bestValue variable if the current move leads to a
# lower score than the previous best move.
bestValue = min(bestValue

Вопрос: пожалуйста, продолжай

Ответ: Конечно, вот остальная часть функции minimax():

value)

# Once we have considered all of the possible moves, we can return
# the best score.
return bestValue

# If the current player is the minimizing player, we want to find the
# move that leads to the lowest score, so we initialize the bestValue
# variable to a very high value.
else:
bestValue = float(«inf»)

# Loop through all of the possible moves and compute the score for
# each one.
for move in moves:
# Make the move on a copy of the board.
newBoard = make_move(board, move)

# Recursively call the minimax function to find the best move
# for the opponent.
value = minimax(newBoard, depth-1, True)

# Update the bestValue variable if the current move leads to a
# lower score than the previous best move.
bestValue = min(bestValue, value)

# Once we have considered all of the possible moves, we can return
# the best score.
return bestValue

Этот код использует алгоритм minimax для поиска в пространстве возможных ходов и контр-движений, чтобы найти ход, который с наибольшей вероятностью приведёт к победе (или, по крайней мере, не к проигрышу). Функция evaluate() используется для присвоения числового значения каждой позиции на доске, а функции get_possible_moves() и make_move() используются для генерации и применения возможных ходов.

Опять же, имейте в виду, что это всего лишь один из возможных способов реализации минимаксного алгоритма, и есть много других способов, которыми мы могли бы это сделать. Кроме того, этот код не включает в себя все оптимизации, которые обычно используются в реальных шахматных программах, поэтому он будет не очень эффективен для больших шахматных позиций. Я надеюсь, что это даст вам общее представление о том, как работает алгоритм minimax и как он может быть реализован на Python, но дайте мне знать, если у вас возникнут какие-либо другие вопросы!

Вопрос: можете ли вы сгенерировать для меня функцию оценки?

Ответ: Конечно, вот пример того, как мы могли бы реализовать функцию оценки для шахматной программы на Python:

def evaluate(board):
# This function will evaluate the current position on the chess board and
# return a numerical score. The higher the score, the better the position
# is for the player who is currently moving.

# First, we will compute the material advantage for each player. This
# will give us a basic measure of the relative strength of each player’s
# pieces.
whiteMaterial = 0
blackMaterial = 0
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
piece = board[i][j]
if piece is not None:
if piece.isupper():
whiteMaterial += PIECE_VALUES[piece]
else:
blackMaterial += PIECE_VALUES[piece]

# Next, we will compute the mobility for each player. This will give us
# a measure of how many squares each player’s pieces can move to.
whiteMobility = 0
blackMobility = 0
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
piece = board[i][j]
if piece is not None:
if piece.isupper():
whiteMobility += len(get_possible_moves(board, i, j))
else:
blackMobility += len(get_possible_moves(board, i, j))

# Finally, we will combine the material advantage and mobility scores
# to compute the overall score for the position.
score = whiteMaterial + whiteMobility — blackMaterial — blackMobility

# Return the score for the position.
return score

Этот код использует два фактора для оценки текущего положения на шахматной доске: материальное преимущество каждого игрока (т.е. относительная сила их фигур) и мобильность фигур каждого игрока (т.е. на сколько квадратов они могут переместиться). Эти два фактора объединяются для вычисления общего балла за позицию, который возвращается функцией evaluate().

Конечно, это всего лишь один из возможных способов реализации функции оценки для шахматной программы, и есть много других факторов, которые мы могли бы включить в оценку (таких как структура пешки, безопасность короля и так далее). Кроме того, точные значения, которые мы присваиваем каждому фактору (например, значения для каждого типа шахматной фигуры), могут оказать большое влияние на производительность шахматной программы, поэтому важно тщательно учитывать эти значения и корректировать их по мере необходимости. Я надеюсь, что это даёт вам общее представление о том, как может работать функция оценки, но дайте мне знать, если у вас возникнут какие-либо другие вопросы!

Вопрос: теперь мне нужна функция make_move()

Ответ: Конечно, вот пример того, как мы могли бы реализовать функцию make_move() для шахматной программы на Python:

def make_move(board, move):
# This function will take a chess board and a move, and return a new
# chess board that reflects the move that has been made.

# First, we will make a copy of the original board, so that we can
# modify the copy without affecting the original.
newBoard = copy.deepcopy(board)

# Next, we will extract the source and destination coordinates for the
# move that is being made.
(x1, y1) = move[0]
(x2, y2) = move[1]

# We will then move the piece from the source square to the destination
# square on the new board.
newBoard[x2][y2] = newBoard[x1][y1]
newBoard[x1][y1] = None

# Finally, we will return the new board.
return newBoard

Этот код использует функцию copy.deepcopy() для создания глубокой копии оригинальной шахматной доски, чтобы мы могли изменять копию, не затрагивая оригинал. Затем он использует исходные и конечные координаты выполняемого хода для обновления доски, перемещая фигуру из исходного квадрата в конечный квадрат.

Заключение

Я нахожу результаты впечатляющими. Непростая задача была не только решена с помощью выходных данных кода, но и объяснена соответствующими и корректными текстами. Также были указаны ограничения и возможные расширения.

Что не сработало:

  • словарь PIECE_VALUES должен был быть добавлен
  • необходимо было добавить импортную копию.
  • Сгенерированная функция для создания всех ходов не соответствовала функции minimax , потому что она ожидала все ходы позиции, но созданная функция генерирует только все ходы фигуры позиции.

Однако все эти недостатки также могли быть вызваны тем, что я не задавал достаточно точных вопросов в диалоге. Я думаю, что для эффективного использования этого инструмента нам всем нужно больше практики!

Статья была взята из этого источника:

Коркунова Наталья Ивановна

Python очень обширый язык программирования, на котором можно создать бесконечное множество разнообразных проектов. Свой первый проект я решил посвятить шахматам – одной из древнейших настольных игр с достаточно простыми правилами но в то же время заставляющей человека думать и планировать.

Актуальность выбранной темы проекта связана с актуальностью профессионального изучения информатики и программирования в современном мире.

Новизна проекта заключается в сочетании теории и практики, использовании различных ресурсов для написания кода.

Цель проекта – Создать графический интерфейс на основе PyQt для игры в шахматы для 2 игроков

  1. Реализовать классы фигур, механики передвижения.
  2. Создать поле для игры, механику атаки
  3. Реализовать таймер, обработку событий мыши.
  4. С помощью PyQt создаем графический интерфейс

Все фигуры имеют некоторые одинаковые механики, поэтому для начала создадим базовый класс, от которого будем наследовать другие.

Если представить доску в виде матрицы, то можно определять местоположение фигур с помощью координат х и у.

Пешка может двигаться на 1 клетку по вертикали (вверх если белая и вниз если черная), а также на 2 клетки, если её ещё ни разу не двигали. Исходя из этих данных создаем класс:

def can_move ( self , board , row , col , row1 , col1):
# Пешка может ходить только по вертикали
# «взятие на проходе» не реализовано
if col != col1:
return False

if not self .is_path_clear(board , row , col , row1 , col1):
return False

# Пешка может сделать из начального положения ход на 2 клетки
# вперёд, поэтому поместим индекс начального ряда в start_row.
if self .color == WHITE:
direction = 1
start_row = 1
else :
direction = — 1
start_row = 6

# ход на 1 клетку
if row + direction == row1:
return True

# ход на 2 клетки из начального положения
if row == start_row and row + 2 * direction == row1:
return True

def can_attack ( self , board , row , col , row1 , col1):
direction = 1 if ( self .color == WHITE) else — 1
return (row + direction == row1
and (col + 1 == col1 or col — 1 == col1))

Эта фигура может двигаться только по вертикали и горизонтали, т.е. должны совпадать координаты до передвижения и после лтбо по вертикали либо по горизонтали. Зная это, реализуем класс:

def can_move ( self , board , row , col , row1 , col1):
# Невозможно сделать ход в клетку, которая не лежит в том же ряду
# или столбце клеток.
if row != row1 and col != col1:
return False
if not self .is_path_clear(board , row , col , row1 , col1):
return False

Конь двигается буквой «Г», т.е. на 2 по вертикали и 1 по горизонтали и наоборот. При этом направление не важно, значит нужно использовать ф-цию abs(), т.е. модуль.

class Knight(Piece):
ch = ‘N’

def can_move ( self , board , row , col , row1 , col1):
# Конь двигается буквой Г (2 вертикально, 1 горизонтально)
if abs (row — row1) == 2 and abs (col — col1) == 1 :
return True
# Конь двигается буквой Г (1 вертикально, 2 горизонтально)
if abs (row — row1) == 1 and abs (col — col1) == 2 :
return True

Слон двигается по диагонали т.е. его смещение по вертикали должно быть равно смещению по горизонтали:

class Bishop(Piece):
ch = ‘B’

def can_move ( self , board , row , col , row1 , col1):

if not self .is_path_clear(board , row , col , row1 , col1):
return False

# Слон двигается по диагонали
# Смещение по строка должно равняться смещению по столбцам
if abs (row — row1) == abs (col — col1):
return True

Королева двигается по всем сторонам (по горизонтали, вертикали, диагонали). Копируем эти механики из слона и ладьи и получаем:

class Queen(Piece):
ch = ‘Q’

def can_move ( self , board , row , col , row1 , col1):

if not self .is_path_clear(board , row , col , row1 , col1):
return False

# Ферзь двигается вертикально
if col == col1:
return True

# Ферзь двигается горизонтально
if row == row1:
return True

# Ферзь двигается по диагонали
# Смещение по строка должно равняться смещению по столбцам
if abs (row — row1) == abs (col — col1):
return True

Король двигается в любую сторону на расстояние 1:

def can_move ( self , board , row , col , row1 , col1):

if not self .is_path_clear(board , row , col , row1 , col1):
return False

# Король двигается в любую клетку с рассотоянием равным 1
if max ( abs (row — row1) , abs (col — col1)) == 1 :
return True

А теперь необходимо реализовать некоторые особые механики игры, например «Превращение пешки» и «Рокировка».

Когда пешка доходит до противоположного конца доски она может стать Ферзем, Ладьей, Слоном или Конем. Для этого мы можем просто изменить класс фигуры, в том месте, где стоит пешка.

Для рокировки нам нужно знать: двигались ли король и ладья, есть ли между ними фигуры, приведет ли рокировка к шаху короля. От этого будет зависеть успешность рокировки или её невозможность.

Представим доску в виде матрицы на координатной оси. Тогда расположение фигур на доске будет записано в двухмерном списке. Записыаем стартовые значения матрицы:

self .field = [[ None ] * 8 for _ in range ( 8 )]
self .field[ 0 ] = [
Rook(WHITE) , Knight(WHITE) , Bishop(WHITE) , Queen(WHITE) ,
King(WHITE) , Bishop(WHITE) , Knight(WHITE) , Rook(WHITE)
]
self .field[ 1 ] = [
Pawn(WHITE) , Pawn(WHITE) , Pawn(WHITE) , Pawn(WHITE) ,
Pawn(WHITE) , Pawn(WHITE) , Pawn(WHITE) , Pawn(WHITE)
]
self .field[ 6 ] = [
Pawn(BLACK) , Pawn(BLACK) , Pawn(BLACK) , Pawn(BLACK) ,
Pawn(BLACK) , Pawn(BLACK) , Pawn(BLACK) , Pawn(BLACK)
]
self .field[ 7 ] = [
Rook(BLACK) , Knight(BLACK) , Bishop(BLACK) , Queen(BLACK) ,
King(BLACK) , Bishop(BLACK) , Knight(BLACK) , Rook(BLACK)
]

Цвет фигуры определяют константы WHITE и BLACK.

Каждая клетка имеет свое значение, так белая пешка обозначена как «wP». В дальнейшем это обозначение определит графическую модель фигурки на доске.

В этом классе создаем ф-цию проверки возможности движения:

def check_move ( self , row: int , col: int , row1: int , col1: int ):
«»»Проверка перемещения фигуру из точки (row, col) в точку (row1, col1).
Если перемещение возможно, метод выполнит его и вернет True.
Если нет — вернет False»»»
check = self .check() # Есть ли шах к перед ходом
if not correct_coords(row , col) or not correct_coords(row1 , col1):
return False
if row == row1 and col == col1:
return False # нельзя пойти в ту же клетку
piece = self .field[row][col]
target = self .field[row1][col1]
if piece is None :
return False
if piece.get_color() != self .color:
return False

if isinstance (piece , King) and self .is_under_attack(row1 , col1 , opponent( self .current_player_color())):
return False

if not (piece.can_move( self .field , row , col , row1 , col1) and target is None ) and
not (piece.can_attack( self .field , row , col , row1 , col1) and
(target is not None and not isinstance (target , King)
and target.get_color() == opponent( self .current_player_color()))):
return False

old_field = [x.copy() for x in self .field] # Сохранить поле
self .field[row][col] = None # Снять фигуру.
self .field[row1][col1] = piece # Поставить на новое место.
if check and self .check(): # В результате хода избежать шаха не удалось
# Возвращаемся в исходное состояние
self .field = old_field
return False
# Возвращаемся в исходное состояние
self .field = old_field
return True

Ф-ция проверяет сразу все факторы: выход за пределы доски, походила ли фигурка на свое же место, приведет ли ход короля к его шаху или мату и т.д.

Реализуем ф-цию движения в которой используется ф-ция проверки:

def move_piece ( self , row , col , row1 , col1):
«»»Переместить фигуру из точки (row, col) в точку (row1, col1).
Если перемещение возможно, метод выполнит его и вернет True.
Если нет — вернет False»»»
if self .check_move(row , col , row1 , col1):
piece = self .field[row][col]
self .field[row][col] = None # Снять фигуру.
piece.moved()
self .field[row1][col1] = piece # Поставить на новое место.
if self .check_promotion(piece , row1):
self .promotion = True
self .color = opponent( self .color)
self .change = True
return True
return False

Также здесь реализуем механику атаки:

def is_under_attack ( self , row , col , color):
for row1 in range ( 8 ):
for col1 in range ( 8 ):
if self .field[row1][col1] is not None :
if self .field[row1][col1].get_color() == color and
self .field[row1][col1].can_attack( self .field , row1 , col1 , row , col):
return True
return False

Для короля мы должны реализовать «Шах» и «Мат». Это довольно таки сложно из-за того, что у короля пропадает много возможностей, например «Рокировка». Поэтому я все ещё не полностью смог воссоздать эту механику из шахмат. Но все же в случае шаха игроку будут недоступны ходы, которые приведут к мату.

Таймер и обработка событий мыши

С помощью класса QtTimer от PyQt реализуем таймер для каждого игрока с механикой смены после окончания хода. Также с помощью механизма слотов и сигналов PyQt мы сможем обрабатывать события мыши для удобного взаимодействия с графическим интерфейсом.

Реализация графического интерфейса, пожалуй, одна из самых сложных задач проекта, так как PyQt я знаю ещё очень поверхностно. Но что есть – то есть. Создаем модели фигурок, а также модели иконок и изображение поля. Настраиваем механику обработки мыши под границы моделей клеток и иконок.

Дополняем последние детали в виде кнопок «Новая игра» и «Выйти».

Проект завершен, создан рабочий вариант шахмат.

Во время разработки проекта я узнал о многих механиках программирования, получил знания о классах объектов, познакомился с QT. Этот проект ждет ещё множество новых механик и нововведений: добавление ИИ, реализация более сложных механик, которыми пользуются проффесиональные игроки(прим.: «Взятие на проходе»). В дальнейшем я не собираюсь останавливаться на этом проекте, так как меня ждет ещё множество нереализованных идей, ждущих своего часа.

Machine Learning and Chess

In late 2020, Netflix released the Queen’s Gambit, a TV show taking place in the 1950’s where we follow the journey of a young women thriving to become the best chess player.

However, unlike the protagonist Beth Harmon, chess isn’t really my strongest suit. So I did the next best thing by building my own python chess engine to compensate my poor chess skills.

Episode I: Strategies

When it comes to building any board game engine, there are multiple strategies:

  • Reinforcement Learning
  • Minimax
  • ML model trained on human knowledge

I opted for an hybrid of the second and third strategies: As a Data scientist, I am fond of working with large datasets and machine learning models. Also, despite being a little slower, Minimax is a quite reliable algorithm and does a good job at capturing enemy pieces. Reinforcement learning is simply too complex.

My strategy is simple:

  • First of all, using the machine learning model, the engine will dismiss 50% of the possible moves given a board. It does so by finding the probability of a move being a ‘good move’. All the moves are sorted according to this probability and only the 50th percentile remains.
  • Secondly, the engine will perform the Minimax algorithm on the remaining moves. Depending on the complexity of the board, the search tree will go to a certain depth.

Episode II: Data

In order to make accurate predictions, we need a lot of data. For this project, I will be using a dataset that I found on Kaggle. This dataset is a collection of thousands of games played by chess grand masters in the .pgn format (portable game notation).

The next step is to extract all the moves from all the games and label them as good or bad.

In my opinion, a ‘good move’ is a move that the winner played at some point during the game. A ‘bad move’ is a legal move that the winner chose not to play.

This python script got the job done leaving me with a brand new .csv dataset.

Создаем несложный шахматный ИИ: 5 простых этапов

Перевели для вас статью Лори Хартикка (Lauri Hartikka) о создании простейшего ИИ для шахмат. Она написана еще в 2017 году, но базовые принципы остались теми же. Все файлы, которые использовал Лори, тоже доступны.

Простой искусственный интеллект, который умеет играть в шахматы, можно создать на базе четырех концепций:

  1. 1. Перемещение;
  2. 2. Оценка доски; ; . На каждом этапе работы с алгоритмом будет использоваться одна из них, это позволит постепенно совершенствовать игровые способности ИИ.

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Готовый исходный код можно найти на GitHub.

I’m having trouble beating a chess program I wrote.

Not sure if I’m a bad player or the algorithm is decent.

— Lauri Hartikka (@lhartikk) March 28, 2017

Этап 1. Визуализация шахматной доски с генерацией ходов

На этом этапе мы будем использовать библиотеки chess.js для генерации ходов и chessboard.js для визуализации доски. Библиотека, которая отвечает за генерацию ходов, позволяет применять все шахматные правила, так что мы можем рассчитывать каждое действие для конкретного расположения фигур.


При клике на картинке она откроется в полном разрешении.

Работа с этими библиотеками позволяет сконцентрироваться на главной задаче — поиске и создании алгоритма, который позволяет найти оптимальный ход. Работу начинаем с написания функции, которая возвращает случайный ход из списка всех возможных.

Несмотря на то, что алгоритм не является идеальным игроком в шахматы, для большинства игроков его уровня этого будет вполне достаточно.

Этап 2. Оценка позиции

Теперь давайте разберемся, у какой стороны преимущество в том или ином положении. Самый простой путь — подсчитать относительную силу фигур на доске, это можно сделать при помощи таблицы.

Используя функцию оценки, мы получаем возможность создать алгоритм, который выбирает ход с максимальной оценкой.

В принципе, уровень прежний, но алгоритм уже может взять чужую фигуру, когда такая возможность есть.

Черные получили возможность брать белые фигуры. (Исходники и игра здесь).

Этап 3. Дерево поиска с минимакс

После этого мы создаем дерево поиска. Теперь программа может выбрать из него лучший ход. Это делается при помощи минимакс-алгоритма.

Здесь рекурсивное дерево с отображением всех возможных ходов анализируется до заданной глубины. Позиция же оценивается по листьям нашего дерева.

Далее мы возвращаем минимальное или максимальное значение потомка в родительский узел. Все зависит от того, ход какой стороны сейчас просчитывается. Другими словами, результат максимизируется или минимизируется на каждом из уровней.


Здесь лучшим ходом для белых является b2-c3, поскольку он гарантирует, что игрок доберется до позиции с оценкой -50.

С минимакс-алгоритмом наш ИИ уже стал понимать базовую тактику шахмат.

Минимакс с глубиной 2 (Исходники и игра здесь)

Стоит отметить, что эффективность минимакс-алгоритма увеличивается с глубиной поиска. За это отвечает следующий этап.

Этап 4. Альфа-бета-отсечения

Это метод оптимизации минимакс-алгоритма, дающий возможность игнорировать некоторые ветви в дереве поиска. А это позволяет увеличить глубину поиска, затрачивая прежний объем ресурсов.

Альфа-бета-отсечение основано на ситуации, когда мы можем остановить оценку определенной ветви, если обнаруживается, что новый ход приведет к худшей ситуации, чем та, которую мы видели при оценке предыдущего.

На результат минимакса оптимизация не влияет, но все начинает работать быстрее.

Этот алгоритм гораздо более эффективен в том случае, если сначала проверить пути, ведущие к хорошим ходам.


Изображение демонстрирует ходы, которые становятся ненужными в процессе использования альфа-бета-отсечения.

Как видите, с альфа-бета-отсечением минимакс оптимизируется, и весьма значительно.


Количество позиций, которые требуется оценить в случае поиска с глубиной 4 и начальной позицией, которая изображена выше. (исходники и игра доступны здесь)

Этап 5. Улучшенная функция оценки

Изначальная функция оценки достаточно простая, поскольку она просто считает очки фигур, находящихся на доске. Для ее оптимизации можно учитывать положение фигур. К примеру, если разместить коня в центре доски, то он становится дороже — спектр доступных ходов для этой фигуры расширится.

На этом этапе мы будем работать с несколько видоизмененной версией квадратных таблиц, изначально описанной в вики Chess Programming.

И теперь наш алгоритм играет уже весьма неплохо, конечно, по сравнению со средним игроком.

Исходники и игра доступны здесь

Заключение

Достоинством предложенного алгоритма является то, что он не делает совсем уж глупых ошибок. Конечно, стратегию здесь сложно назвать совершенной, но тем не менее.

Cover image for Creating a chess game with Python, pygame and chess (Pt. 1)

Prince

Prince

Posted on Nov 21, 2022

• Updated on Nov 23, 2022



 



 



 



 



 

 

Hey, I’m Prince and I’m going to be walking you through my process of creating a chess game with Python (this is my first project with pygame).
DISCLAIMER: This article is not for beginners but I will make an effort to make it accessible to those with just a little bit of Python knowledge. Some concepts involved here include; OOP and data structures.

The objectives of this program are to create a chess game that can be played with 2 players or against an AI.

Here’s a link to the project on Github, feel free to play around with it or contribute.

I relied heavily on the techniques covered in this article

So firstly create a new folder (for the purposes of this article we will call it chess-game) where you want to store the code and in that folder create a virtual environment (if you are not familiar with virtual environments take a look at this ), activate the virtual environment and install the following packages:

  1. chess
  2. pygame

We need the chess module to handle the chess rules and validations and pygame to make the actual game.

Ok, we are going to split this walkthrough into 3 sections:

  • The pieces, squares and the boards
  • Displaying the board and pieces on the pygame window and
  • Creating an AI player

The pieces, squares and the board

We will create a new package in our code, gui_components. To create a package just create a new folder (in this case gui_components) and in that new folder create a new file __init__.py)
We will also create a new folder in our project directory (chess-game) called skins. This is where we will store the images for our pieces. Feel free to copy the skins directory from the repository

The project should have the following structure:
chess-game/
—|gui_components/
—|skins/

  1. The pieces
    We will create a pieces.py file in our gui_components folder. In this file we will create a Piece class. For now the objects of this class will simply be used to display the image and get the value of the piece based on its notation (in chess the different pieces have notations k for King, q for Queen, r for Rook, b for bishop, n for Knight and p for Pawn) and whether or not it has been captured.
import os
import pygame

class Piece:
    colors_notations_and_values = {
        "w": {
            "p": 1,
            "n": 3,
            "b": 3,
            "r": 5,
            "q": 9,
            "k": 90
        }, 
        "b": {
            "p": -1,
            "n": -3,
            "b": -3,
            "r": -5,
            "q": -9,
            "k": -90
        }
    }

    def __init__(self, name, notation, color, skin_directory="skins/default", is_captured=False) -> None:
        self.name = name
        self.__notation = notation
        self.color = color
        self.skin_directory = skin_directory
        self.set_is_captured(is_captured)

        self.value = self.get_piece_value()

    def get_piece_value(self):
        return Piece.colors_notations_and_values[self.color][self.__notation.lower()]

    def get_piece_color_based_on_notation(notation) -> str:
        """
        The chess module displays black pieces' notations in lowercase and white in uppercase, so we can get the color based on this
        """
        return "w" if notation.isupper() else "b"

    def get_value_from_notation(notation: str, color: str) -> int:
        """
        A class method that gets the corresponding value for a particular notation and color
        """
        return Piece.colors_notations_and_values[color][notation.lower()]

    def set_is_captured(self, is_captured: bool):
        self.__is_captured = bool(is_captured)

    def get_image_path(self):
        """
        Gets the path to the image of the piece based on its notation and 
        whether or not it has been captured
        """
        if not self.__is_captured:
            path = os.path.join(self.skin_directory, self.color, f"{self.__notation.lower()}.png")
        else:
            path = os.path.join(self.skin_directory, self.color, "captured", f"{self.__notation.lower()}.png")

        return path

    def get_image(self):
        """
        Returns a pygame image object from the piece's corresponding image path
        """
        image_path = self.get_image_path()

        if os.path.exists(image_path):
            return pygame.image.load(image_path)
        else:
            raise FileNotFoundError(f"The image was not found in the {image_path}")

    def __str__(self):
        return f"{self.__notation} {self.color}"

    def get_notation(self) -> str:
        """
        Returns the notation of the piece, (pawns' notations are empty strings)
        """
        if self.__notation != 'p':
            return self.__notation.upper()

        return ''

    def __set_notation(self, notation):
        self.__notation = notation

    def promote(self, notation: str):
        """
        Promotes this piece to a piece with the notation notation.
        It is important to note that promotion does not increase the piece's value, 
        just its capabilities
        """
        if self.__notation.lower() != "p":
            raise ValueError("Cannot promote a piece other than a pawn")

        if notation not in ["q", "r", "n", "b"]:
            raise ValueError("Can only promote to queen, rook, bishop or knight pieces")
        self.__set_notation(notation)

Enter fullscreen mode

Exit fullscreen mode

  1. The squares and board
    When creating this game I thought about being able to have a checkers game with it, so the classes in this section kind of reflect that vision. First and foremost, create a new file boards.py. In this file create a Square class (a generic class for squares checkers or chess)
import chess

import pygame

from gui_components.pieces import Piece

class Square(pygame.Rect):
    def __init__(self, left: float, top: float, width: float, height: float, background_color: str, border_color: str, piece: Piece = None) -> None:
        super().__init__(left, top, width, height)
        self.background_color = background_color
        self.border_color = border_color
        self.piece = piece
        self.is_possible_move = False

    def toggle_is_possible_move(self):
        self.is_possible_move = not self.is_possible_move
        return self

    def empty(self):
        self.piece = None

        return self

    def set_is_possible_move(self, value: bool):
        self.is_possible_move = bool(value)
        return self

Enter fullscreen mode

Exit fullscreen mode

Now a square for chess pieces

class ChessSquare(Square):
    def __init__(self, left: float, top: float, width: float, height: float, background_color: str, border_color: str, file_number, rank_number, piece: Piece = None) -> None:
        super().__init__(left, top, width, height, background_color, border_color, piece)
        self.file_number = file_number
        self.rank_number = rank_number
        self.ranks = list( str(i) for i in range(1, 9) )
        self.files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

    def get_chess_square(self) -> chess.Square:
        """
        Returns a chess.Square object that corresponds to this one
        """
        return chess.square(self.file_number, self.rank_number)

    def is_identical_to_chess_square(self, square: chess.Square) -> bool:
        """
        Checks if this object corresponds to a chess.Square object
        """
        return (
            self.file_number == chess.square_file(square) and 
            self.rank_number == chess.square_rank(square)
        )

    def get_rank(self) -> str:
        """
        Gets the rank of the object. Ranks are the rows of the board and they range from 1 to 8
        """
        return self.ranks[ self.rank_number ]

    def get_file(self) -> str:
        """
        Gets the file of the object. Files are the columns of the board and range from A to H
        """
        return self.files[ self.file_number ]

    def get_notation(self) -> str:
        """
        Gets the notation of the square object. A squares notation is simply its file and rank
        """
        return f'{self.get_file()}{self.get_rank()}'

Enter fullscreen mode

Exit fullscreen mode

Now for the board. Same as the square we will create 2 board classes although the parent board class doesn’t do much for now. This class will help us keep track of the pieces on our squares, highlight a move made, display the possible moves, get a square that corresponds to particular coordinates and make a move.

class Board(pygame.sprite.Sprite):
    RANKS = [ i+1 for i in range(0, 8) ]
    FILES = [ chr(i) for i in range(65, 65+9) ]

    def __init__(self, number_of_rows, number_of_columns, left, top, width, height, horizontal_padding, vertical_padding, **kwargs) -> None:
        self.left = left
        self.top = top
        self.number_of_rows = number_of_rows
        self.number_of_columns = number_of_columns
        self.width = width
        self.height = height
        self.horizontal_padding = horizontal_padding
        self.vertical_padding = vertical_padding
        self.squares = []

    def create_squares(self):
        pass

class ChessBoard(Board):
    def __init__(
        self, left, top, width, height, 
        horizontal_padding=None, vertical_padding=None, 
        light_square_color: str=(245, 245, 245), dark_square_color: str=(100, 100, 100), 
        previous_square_highlight_color=(186, 202, 43),
        current_square_highlight_color=(246, 246, 105),
        board: chess.Board=None, move_hints=True, **kwargs
    ) -> None:
        super().__init__(
            8, 8, left, top, width, height, 
            horizontal_padding, vertical_padding, **kwargs
        )
        self.__set_square_size()
        self.light_square_color = light_square_color
        self.dark_square_color = dark_square_color
        self.board = board
        self.move_hints = move_hints
        print('The current board is')
        print(self.board)
        self.rect = pygame.Rect(left, top, width, height)

        self.create_squares()

        self.captured_pieces = {
            "w": [],
            "b": []
        }

        # the square the piece that made the latest move came from
        self.previous_move_square = None 
        self.current_move_square = None 

        self.previous_square_highlight_color = previous_square_highlight_color
        self.current_square_highlight_color = current_square_highlight_color

        self.is_flipped = bool(kwargs["flipped"]) if "flipped" in kwargs else False

        # set to True if a pawn has the right to promote and has to choose which piece it wants to promote to
        self.awaiting_promotion = False

        # self.flip()

    def __set_square_size(self):
        self.__square_size = self.height // 8

    @property
    def square_size(self) -> int:
        return self.__square_size


    def get_piece_from_notation(self, notation):
        """
        Returns a piece object based on a particular notation
        """
        if notation != '.':
            piece_color = "b" if notation.islower() else "w"
            notation = notation.lower()
            piece = Piece(name=notation, notation=notation, color=piece_color)

            return piece

        return None

    def get_square_from_chess_square(self, square: chess.Square) -> ChessSquare:
        """
        Returns a Square object that corresponds to a particular chess.Square object
        """
        square_file = chess.square_file(square)
        square_rank = chess.square_rank(square)

        rank = self.squares[ 7 - square_rank ]

        return rank[ square_file ]

    def create_squares(self):
        """
        Creates the squares oon the board and places pieces on them based on the state of the chess.Board object
        """
        string = self.board.__str__()
        ranks_inverted = string.split('n')#[::-1]

        for i in range(self.number_of_rows):
            self.squares.append( [] )

            rank = ranks_inverted[i].split(' ')

            for j in range(self.number_of_columns):
                square = rank[j]

                piece = self.get_piece_from_notation(square)

                color = self.light_square_color if (i+j) % 2 == 0 else self.dark_square_color

                board_square = ChessSquare( 
                    self.left + (j*self.square_size), self.top + (i*self.square_size), self.square_size, 
                    self.square_size, color, self.dark_square_color, j, 7 - i, piece=piece
                )

                self.squares[i].append( board_square )

    def flip(self):
        """
        Changes the coordinates of the squares in essence flipping them
        """
        board_rect = pygame.Rect(self.left, self.top, self.width, self.height)

        for (i, rank) in enumerate(self.squares):
            print(f"Flipping the squares on rank: {8 - i}")
            for (j, square) in enumerate(rank):
                square: ChessSquare = square
                _old = square.__repr__()

                square.x += (7 - j) * self.square_size
                square.y += (7 - i) * self.square_size

                if not square.colliderect(board_rect):
                    print("Square is out of bounds of the board")
                    print(f"The board rectangle is: {board_rect}. The square rectangle is: {square}")

                else:
                    print(f"Square was flipped successfully. Old coordinates: {_old}, new: {square}")

        self.is_flipped = not self.is_flipped

    def place_pieces(self):
        """
        places pieces on the board based on the progress of the board attribute 
        different from create_squares in that it doesn't create squares it instead 
        clears all the squares of existing pieces and positions the pieces on the board
        """
        string = self.board.__str__()
        ranks_inverted = string.split('n')#[::-1]

        for i in range( self.number_of_rows ):
            rank = ranks_inverted[i].split(' ')

            for j in range( self.number_of_columns ):
                self.squares[i][j].empty()
                board_square = rank[j]

                piece = self.get_piece_from_notation(board_square)

                self.squares[i][j].piece = piece

    def get_possible_moves(self, source_coordinates, remove_hints=False):
        """
        Gets the possible moves from some coordinates and marks the squares as possible moves if move_hints are enabled
        """
        # source_square = [ square.get_chess_square() for square in self.iter_squares() if square.collidepoint(source_coordinates) ]
        source_square = self.get_square_from_coordinates(source_coordinates)

        if source_square:
            destination_chess_squares = [ move.to_square for move in self.board.legal_moves if move.from_square == source_square ]
            destination_squares = [ square.set_is_possible_move(not remove_hints) for square in self.iter_squares() if square.get_chess_square() in destination_chess_squares ]

            return destination_squares

        return []

    def get_possible_moves_without_hint(self, source_coordinates):
        """
        Gets the possible moves from some coordinates
        """
        source_square = self.get_square_from_coordinates(source_coordinates)

        if source_square:
            destination_chess_squares = [ move.to_square for move in self.board.legal_moves if move.from_square == source_square ]
            destination_squares = [ square for square in self.iter_squares() if square.get_chess_square() in destination_chess_squares ]

            return destination_squares

        return []

    def hide_hints(self):
        """
        Hides the hints on the squares
        """
        [square.set_is_possible_move(False) for square in self.iter_squares()]

    def get_square_from_coordinates(self, coordinates, return_chess_square=True) -> ChessSquare:
        """
        Returns a square that corresponds to the coordinates passed
        """
        square = [ (square.get_chess_square() if return_chess_square else square) for square in self.iter_squares() if square.collidepoint(coordinates) ]

        if len(square) > 0:
            square = square[0]

            return square
        print(f"There is no square at the {coordinates} coordinates")
        return None

    def get_move_notation(self, source_square: ChessSquare, destination_square: ChessSquare):
        """
        Gets the notation for a particular move made from source_square to destination_square
        """
        move = ''

        if source_square.piece:
            other_pieces_of_the_same_type_that_can_make_move = self.get_pieces_that_can_make_move( [source_square.piece.get_notation()], source_square.piece.color, destination_square, [source_square] )
            same_rank = False
            same_file = False

            if source_square.piece.get_notation() != '':
                for square in other_pieces_of_the_same_type_that_can_make_move:
                    if square.rank_number == source_square.rank_number:
                        same_rank = True
                    if square.file_number == source_square.file_number:
                        same_file = True

                move = move + source_square.piece.get_notation()

                if same_file or same_rank:
                    if not same_file:
                        move = move + f"{source_square.get_file()}"
                    elif same_file and not same_rank:
                        move = move + f"{source_square.get_rank()}"
                    else:
                        move = move + f"{source_square.get_notation()}"

        if destination_square.piece:
            move = move + 'x'

            if source_square.piece and source_square.piece.get_notation() == '':
                move = source_square.get_file() + move


        move = move + f'{destination_square.get_notation()}'

        if source_square.piece.get_notation() == 'K' and source_square.get_file() == 'e' and destination_square.get_file() in [ 'c', 'g' ]:
            # castling
            if destination_square.get_file() == 'c':
                return '0-0-0'
            else:
                return '0-0'

        move = chess.Move(
            from_square=source_square.get_chess_square(), to_square=destination_square.get_chess_square()
        )

        return move

    def get_pieces_that_can_make_move(self, piece_notations: list, color, square: ChessSquare, squares_to_exclude: list):
        """
        Returns the pieces with notations in <piece_notations> list and of color <color> that can make a move the <square> square 
        while excluding the pieces on the <squares_to_exclude> list
        """
        squares_with_pieces_of_specified_types = [ _square for _square in self.iter_squares() if _square.piece and _square.piece.get_notation() in piece_notations and _square.piece.color == color and _square not in squares_to_exclude ]
        squares_that_can_make_move = [ _square for _square in squares_with_pieces_of_specified_types if square in self.get_possible_moves_without_hint(_square.center) ]

        return squares_that_can_make_move

    def play(self, source_coordinates, destination_coordinates):
        """
        Makes a move from source_coordinates to destination_coordinates
        """
        source_square = self.get_square_from_coordinates(source_coordinates, return_chess_square=False)
        destination_square = self.get_square_from_coordinates(destination_coordinates, return_chess_square=False)

        self._play(source_square, destination_square)

    def _play(self, source_square: ChessSquare=None, destination_square: ChessSquare=None, 
        source_chess_square: chess.Square=None, destination_chess_square: chess.Square=None,
        move: chess.Move=None
    ):
        """
        Makes a move based on the arguments.
        """
        if move:
            self.make_move(move)
            self.previous_move_square = self.get_square_from_chess_square(move.from_square)
            self.current_move_square = self.get_square_from_chess_square(move.to_square)

        elif source_square and destination_square:
            move = self.get_move_notation(source_square, destination_square)
            self.make_move(move)
            self.previous_move_square = source_square
            self.current_move_square = destination_square

        elif source_chess_square and destination_chess_square:
            move = chess.Move(from_square=source_chess_square, to_square=destination_chess_square)
            self.make_move(move)
            self.previous_move_square = self.get_square_from_chess_square(source_chess_square)
            self.current_move_square = self.get_square_from_chess_square(destination_chess_square)

        else:
            print("None of the conditions were fulfilled. No move is currently being made")

        self.place_pieces()

        print('The current board is')
        print(self.board)

    def make_move(self, move):
        """
        Makes a move either with an str object or a chess.Move object
        """
        if isinstance(move, str):
            self.board.push_san(move)
        elif isinstance(move, chess.Move):

            if self.board.is_capture(move):
                destination_square: ChessSquare = self.get_square_from_chess_square(move.to_square)
                piece: Piece = destination_square.piece

                print("The move was a capture")

                if piece is not None:
                    piece.set_is_captured(True)
                    color = piece.color

                    self.captured_pieces[color].append(piece)

            self.board.push(move)

    def iter_squares(self):
        """
        A generator that returns the different squares on the board
        """
        for rank in self.squares:
            for square in rank:
                yield square

Enter fullscreen mode

Exit fullscreen mode

Displaying the board in a pygame window

Before we move forward with this, let’s firstly create some classes we will use in this file. In our gui_components folder we will create a new file components.py. Put this code inside that file

import pygame

class BorderedRectangle():
    """
    An object that contains 2 pygame.Rect object, one put inside the other
    """
    def __init__(
        self, left: float, top: float, width: float, height: float, 
        background_color: str, border_color: str, border_width: int,
        outer_rectangle_border_width=2, inner_rectangle_border_width=2
    ) -> None:
        self.background_color = background_color
        self.border_color = border_color
        self.is_possible_move = False
        self.outer_rectangle_border_width = outer_rectangle_border_width
        self.inner_rectangle_border_width = inner_rectangle_border_width

        self.outer_rectangle = pygame.Rect(left, top, width, height)

        self.inner_rectangle = pygame.Rect(
            left+(border_width / 2), top+(border_width/2), 
            width - border_width, height - border_width
        )

Enter fullscreen mode

Exit fullscreen mode

Now in our root directory (chess-game), create a new file main.py. In this file we will write the code to display our board in a pygame window and even to play the game without AI and board flips.

import chess

import pygame

from pygame import mixer

mixer.init()

from gui_components.board import ChessBoard
from gui_components.components import BorderedRectangle

from ai import players as ai_players

pygame.init()

screen = pygame.display.set_mode([500, 500])

board = chess.Board()

# A dictionary of the different players in the game. True corresponds to white and 
# False to black
players = { 
    True: "user",
    False: "user"
}

turns_taken = {
    True: False, # set to True if white has already started playing 
    False: False # set to True if black has already started playing
}

# the different sounds for the moves
move_sound = mixer.Sound("sound_effects/piece_move.mp3")
check_sound = mixer.Sound("sound_effects/check.mp3")
checkmate_sound = mixer.Sound("sound_effects/checkmate.mp3")

SOURCE_POSITION = None
DESTINATION_POSITION = None
PREVIOUSLY_CLICKED_POSITION = None
POSSIBLE_MOVES = []
TURN = True
IS_FIRST_MOVE = True

running = True

LIGHT_COLOR = (245, 245, 245) # color of the light squares
DARK_COLOR = ( 100, 100, 100 ) # color of the dark squares
WHITE_COLOR = (255, 255, 255) # white
BLACK_COLOR = (0, 0, 0) # black

chess_board = ChessBoard(  # creating a new ChessBoard object
    50, 50, 400, 400, 0, 0, board=board
)

def draw_bordered_rectangle(rectangle: BorderedRectangle, screen):
    pygame.draw.rect( screen, rectangle.border_color, rectangle.outer_rectangle, width=rectangle.outer_rectangle_border_width )
    pygame.draw.rect( screen, rectangle.background_color, rectangle.inner_rectangle, width=rectangle.inner_rectangle_border_width )

def draw_chessboard(board: ChessBoard):
    """
    Draw the chess board on the pygame window
    """
    ranks = board.squares # get the rows of the board

    # a rectangle enclosing the board and the files and ranks labels
    board_bordered_rectangle = BorderedRectangle(25, 25, 450, 450, WHITE_COLOR, DARK_COLOR, 48)
    draw_bordered_rectangle(board_bordered_rectangle, screen)

    # draw the inner rectangle of the bordered rectangle with the same color 
    # as that of the dark squares
    pygame.draw.rect( 
        screen, board_bordered_rectangle.border_color, board_bordered_rectangle.inner_rectangle, 
        width=1
    )

    board_top_left = board.rect.topleft
    board_top_right = board.rect.topright
    board_bottom_left = board.rect.bottomleft

    for i, rank in enumerate(ranks):
        rank_number = ChessBoard.RANKS[ 7 - i ]
        file_letter = ChessBoard.RANKS[i]

        font_size = 15 # font size for the ranks and files

        # add the text rectangle on the left and right of the board
        font = pygame.font.SysFont('helvetica', font_size)

        # render the ranks (1-8)
        for _i in range(1):
            if _i == 0:
                _rect = pygame.Rect(
                    board_top_left[0] - font_size, board_top_left[1] + (i*board.square_size), 
                    font_size, board.square_size
                )
            else:
                _rect = pygame.Rect(
                    board_top_right[0], board_top_right[1] + (i*board.square_size),
                    font_size, board.square_size
                )

            text = font.render(f"{rank_number}", True, DARK_COLOR)
            text_rect = text.get_rect()
            text_rect.center = _rect.center

            screen.blit(text, text_rect)

        # render the files A-H
        for _i in range(1):
            if _i == 0:
                _rect = pygame.Rect(
                    board_top_left[0] + (i*board.square_size), board_top_left[1] - font_size, 
                    board.square_size, font_size
                )
            else:
                _rect = pygame.Rect(
                    board_top_left[0] + (i*board.square_size), board_bottom_left[1], 
                    board.square_size, font_size
                )

            text = font.render(f"{file_letter}", True, DARK_COLOR)
            text_rect = text.get_rect()
            text_rect.center = _rect.center

            screen.blit(text, text_rect)

        for j, square in enumerate(rank):
            if square is board.previous_move_square:
                # highlight source square of the latest move
                pygame.draw.rect( screen, board.previous_square_highlight_color, square )
            elif square is board.current_move_square:
                # highlight the destination square of the latest move
                pygame.draw.rect( screen, board.current_square_highlight_color, square )
            else:
                pygame.draw.rect( screen, square.background_color, square )

            if square.piece:
                # draw the piece on the square
                try:
                    image = square.piece.get_image()
                    image_rect = image.get_rect()
                    image_rect.center = square.center

                    screen.blit( image, image_rect )
                except TypeError as e:
                    raise e
                except FileNotFoundError as e:
                    print(f"Error on the square on the {i}th rank and the {j}th rank")
                    raise e

            if square.is_possible_move and board.move_hints:
                # draw a circle in the center of the square to highlight is as a possible move
                pygame.draw.circle( 
                    screen, (50, 50, 50), 
                    square.center,
                    board.square_size*0.25
                )

def play_sound(board):
    """
    Play sound after move based on move type
    """
    if board.is_checkmate():
        mixer.Sound.play(checkmate_sound)

    elif board.is_check():
        mixer.Sound.play(check_sound)

    elif board.is_stalemate():
        pass

    else:
        mixer.Sound.play(move_sound)

def play(source_coordinates: tuple=None, destination_coordinates: tuple=None):
    """
    Make a move on the board based on the source and destination coordinates if a user is playing
    """
    global board, TURN, IS_FIRST_MOVE, chess_board

    turn = board.turn

    player = players[turn]
    turns_taken[turn] = not turns_taken[turn]
    print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}")

    if not isinstance(player, str):
        # AI model to play
        player.make_move(chess_board)
        play_sound(board)

        TURN = not TURN

        if isinstance(players[TURN], ai_players.AIPlayer):
            # if the next player is an AI, automatically play
            print("Next player is AI, making a move for them automaically")
            # sleep(5)
    else:
        if source_coordinates and destination_coordinates:
            # user to play
            print("User is making move")
            chess_board.play(source_coordinates, destination_coordinates)
            play_sound(board)
            TURN = not TURN

    if IS_FIRST_MOVE:
        IS_FIRST_MOVE = False

    turns_taken[turn] = not turns_taken[turn]
    print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}")


def click_handler(position):
    """
    Handle the click events of the game
    """
    global SOURCE_POSITION, POSSIBLE_MOVES, TURN

    if chess_board.rect.collidepoint(position): # if position is in the board
        current_player = players[TURN]

        if isinstance(current_player, str):
            if SOURCE_POSITION is None:
                POSSIBLE_MOVES = chess_board.get_possible_moves(position)
                SOURCE_POSITION = position if POSSIBLE_MOVES else None
            else:
                # getting the squares in the possible destinations that correspond to the clicked point
                destination_square = [ square for square in POSSIBLE_MOVES if square.collidepoint(position) ]

                if not destination_square:
                    chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True)
                    SOURCE_POSITION = None
                else:
                    destination_square = destination_square[0]
                    print(f"In main.py, about to play, the source and destination are {SOURCE_POSITION} and {position} respectively")
                    chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True)

                    # chess_board.play( SOURCE_POSITION, position )
                    play(SOURCE_POSITION, position)
                    SOURCE_POSITION = None

                    current_player = players[TURN]

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

        if event.type == pygame.MOUSEBUTTONDOWN:
            MOUSE_CLICKED_POSITION = pygame.mouse.get_pos()
            click_handler(MOUSE_CLICKED_POSITION)

    screen.fill( (255, 255, 255) )

    draw_chessboard(chess_board, True)

    pygame.display.flip()

pygame.quit()

Enter fullscreen mode

Exit fullscreen mode

Now if you activate your virtual environment and run the main.py file python main.py a GUI chess game should be displayed:

A new game GUI

Here’s a gif of a game between two users

User v User

In the next article, we are going to look at the creation of an AI player and how to integrate that with our existing code.

If you are having difficulties with the other code, I improved it with some minor fixes. If you want to know how to move your pieces, type a2-a4 as an example. Make sure to copy all of the code after this text:

«»»CONVENTIONS:
positions are done row-column from the bottom left and are both numbers.
This corresponds to the alpha-number system in traditional chess while being computationally useful.
They are specified as tuples
«»»
import itertools
import copy
import inspect
WHITE = «white»
BLACK = «black»
gameboard = None

def canSeeKing(kingpos,piecelist, overridegameboard = None):
global gameboard
overridegameboard = overridegameboard or gameboard
for piece,position in piecelist:
if piece.isValid(position,kingpos,piece.Color,overridegameboard):
return True

def isCheck(overridegameboard = None):
global gameboard
#ascertain where the kings are, check all pieces of opposing color against those kings,
# then if either get hit, check if its checkmate
overridegameboard = overridegameboard or gameboard
king = King
kingDict = {}
pieceDict = {BLACK : [], WHITE : []}
for position,piece in overridegameboard.items():
if type(piece) == king:
kingDict[piece.Color] = position
print(piece)
pieceDict[piece.Color].append((piece,position))
#white
if canSeeKing(kingDict[WHITE],pieceDict[BLACK], overridegameboard):
return WHITE
if canSeeKing(kingDict[BLACK],pieceDict[WHITE], overridegameboard):
return BLACK
return False

class Game:
def init(self):
global gameboard
self.playersturn = WHITE
self.message = «Input Your Move»
gameboard = self.gameboard = {}
self.placePieces()
print(«Chess program. Enter moves in algebraic notation separated by space. Example: a2-a4»)
self.main()

def placePieces(self):

    for i in range(0,8):
        self.gameboard[(i,1)] = Pawn(WHITE,uniDict[WHITE][Pawn],1)
        self.gameboard[(i,6)] = Pawn(BLACK,uniDict[BLACK][Pawn],-1)
        
    placers = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
    
    for i in range(0,8):
        self.gameboard[(i,0)] = placers[i](WHITE,uniDict[WHITE][placers[i]])
        self.gameboard[((7-i),7)] = placers[i](BLACK,uniDict[BLACK][placers[i]])
    placers.reverse()

    
def main(self):
    
    while True:
        self.printBoard()
        print(self.message)
        self.message = ""
        startpos,endpos = self.parseInput()
        try:
            target = self.gameboard[startpos]
        except:
            self.message = "could not find piece; index probably out of range"
            target = None
            
        if target:
            print("found "+str(target))
            if target.Color != self.playersturn:
                self.message = "This is Not Your Turn"
                continue
            if target.isValid(startpos,endpos,target.Color,self.gameboard):
                hasLegalMoves = False
                for position in self.gameboard:
                    piece = self.gameboard[position]
                    if (piece.Color == self.playersturn):
                        for move in piece.availableMoves(position[0], position[1], self.gameboard):
                            overridegameboard = copy.deepcopy(self.gameboard)
                            overridegameboard[move] = self.gameboard[position]
                            del overridegameboard[position]
                            if (isCheck(overridegameboard) != self.playersturn):
                                hasLegalMoves = True
                                break
                if (not hasLegalMoves):
                    if (isCheck() == self.playersturn) : print("You are in checkmate. " + ({WHITE: BLACK, BLACK: WHITE})[self.playersturn] + " wins!")
                    else : print("Stalemate. Nobody wins!")
                    return
                overridegameboard = copy.deepcopy(self.gameboard)
                overridegameboard[endpos] = self.gameboard[startpos]
                del overridegameboard[startpos]
                if (isCheck(overridegameboard) == self.playersturn) : self.message = "You are not allowed to put yourself in check!"
                else:
                    self.message = "That Move is Allowed"
                    self.gameboard[endpos] = self.gameboard[startpos]
                    del self.gameboard[startpos]
                    Check = isCheck()
                    if (Check):
                        self.message = "Player is in check"
                    if self.playersturn == BLACK:
                        self.playersturn = WHITE
                    else : self.playersturn = BLACK
            else : 
                self.message = "invalid move" + str(target.availableMoves(startpos[0],startpos[1],self.gameboard))
                print(target.availableMoves(startpos[0],startpos[1],self.gameboard))
        else : self.message = "There is no Piece in That Space"
            
def parseInput(self):
    try:
        a,b = input().split('-')
        a = ((ord(a[0])-97), int(a[1])-1)
        b = (ord(b[0])-97, int(b[1])-1)
        print(a,b)
        return (a,b)
    except:
        print("error decoding input. please try again")
        return((-1,-1),(-1,-1))

"""def validateInput(self, *kargs):
    for arg in kargs:
        if type(arg[0]) is not type(1) or type(arg[1]) is not type(1):
            return False
    return True"""
    
def printBoard(self):
    print("  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |")
    for i in range(0,8):
        print("-"*32)
        print(chr(i+97),end="|")
        for j in range(0,8):
            item = self.gameboard.get((i,j)," ")
            print(str(item)+' |', end = " ")
        print()
    print("-"*32)
        
       
    
"""game class. contains the following members and methods:
two arrays of pieces for each player
8x8 piece array with references to these pieces
a parse function, which turns the input from the user into a list of two tuples denoting start and end points
a checkmateExists function which checks if either players are in checkmate
a checkExists function which checks if either players are in check
a main loop, which takes input, runs it through the parser, asks the piece if the move is valid, 
and moves the piece if it is.
If the move conflicts with another piece, that piece is removed. is check(mate) is run,
and if there is a checkmate, the game prints a message as to who wins.
"""

class Piece:

def __init__(self,color,name):
    self.name = name
    self.position = None
    self.Color = color
def isValid(self,startpos,endpos,Color,gameboard):
    if endpos in self.availableMoves(startpos[0],startpos[1],gameboard,Color):
        return True
    return False
def __repr__(self):
    return self.name

def __str__(self):
    return self.name

def availableMoves(self,x,y,gameboard,Color=None):
    print("ERROR: no movement for base class")
    
def AdNauseum(self,x,y,gameboard, Color, intervals):
    """Repeats the given interval until another piece is run into. 
    if that piece is not of the same color, that square is added and
     then the list is returned"""
    answers = []
    for xint,yint in intervals:
        xtemp,ytemp = x+xint,y+yint
        while self.noConflict(gameboard, Color, xtemp, ytemp):
            #print(str((xtemp,ytemp))+"is in bounds")
            
            target = gameboard.get((xtemp,ytemp),None)
            if target is None: answers.append((xtemp,ytemp))
            elif target.Color != Color: 
                answers.append((xtemp,ytemp))
                break
            else:
                break
            
            xtemp,ytemp = xtemp + xint,ytemp + yint
    return answers
            
def isInBounds(self,x,y):
    "Checks if a position is on the board"
    if x >= 0 and x < 8 and y >= 0 and y < 8:
        return True
    return False

def noConflict(self,gameboard,initialColor,x,y):
    "Checks if a single position poses no conflict to the rules of chess"
    if self.isInBounds(x,y) and (((x,y) not in gameboard) or gameboard[(x,y)].Color != initialColor) : return True
    return False

chessCardinals = [(1,0),(0,1),(-1,0),(0,-1)]
chessDiagonals = [(1,1),(-1,1),(1,-1),(-1,-1)]

def knightList(x,y,int1,int2):
«»»Sepcifically for the rook, permutes the values needed around a position for noConflict tests»»»
return [(x+int1,y+int2),(x-int1,y+int2),(x+int1,y-int2),(x-int1,y-int2),(x+int2,y+int1),(x-int2,y+int1),(x+int2,y-int1),(x-int2,y-int1)]
def kingList(x,y):
return [(x+1,y),(x+1,y+1),(x+1,y-1),(x,y+1),(x,y-1),(x-1,y),(x-1,y+1),(x-1,y-1)]

class Knight(Piece):
def availableMoves(self,x,y,gameboard, Color = None):
if Color is None : Color = self.Color
return [(xx,yy) for xx,yy in knightList(x,y,2,1) if self.noConflict(gameboard, Color, xx, yy)]

class Rook(Piece):
def availableMoves(self,x,y,gameboard ,Color = None):
if Color is None : Color = self.Color
return self.AdNauseum(x, y, gameboard, Color, chessCardinals)

class Bishop(Piece):
def availableMoves(self,x,y,gameboard, Color = None):
if Color is None : Color = self.Color
return self.AdNauseum(x, y, gameboard, Color, chessDiagonals)

class Queen(Piece):
def availableMoves(self,x,y,gameboard, Color = None):
if Color is None : Color = self.Color
return self.AdNauseum(x, y, gameboard, Color, chessCardinals+chessDiagonals)

class King(Piece):
def availableMoves(self,x,y,gameboard, Color = None):
if Color is None : Color = self.Color
return [(xx,yy) for xx,yy in kingList(x,y) if self.noConflict(gameboard, Color, xx, yy)]

class Pawn(Piece):
def init(self,color,name,direction):
self.name = name
self.Color = color
self.direction = direction
def availableMoves(self,x,y,gameboard, Color = None):
if Color is None : Color = self.Color
answers = []
if (x+1,y+self.direction) in gameboard and self.noConflict(gameboard, Color, x+1, y+self.direction) : answers.append((x+1,y+self.direction))
if (x-1,y+self.direction) in gameboard and self.noConflict(gameboard, Color, x-1, y+self.direction) : answers.append((x-1,y+self.direction))
if (x,y+self.direction) not in gameboard and Color == self.Color and self.noConflict(gameboard, Color, x, y+self.direction) : answers.append((x,y+self.direction))# the condition after the and is to make sure the non-0capturing movement is not used in the calculation of checkmate
if (x,y+self.direction2) not in gameboard and Color == self.Color and self.noConflict(gameboard, Color, x, y+self.direction2) : answers.append((x,y+self.direction*2))# the condition after the and is to make sure the non-0capturing movement is not used in the calculation of checkmate
return answers

uniDict = {WHITE : {Pawn : «♙», Rook : «♖», Knight : «♘», Bishop : «♗», King : «♔», Queen : «♕» }, BLACK : {Pawn : «♟», Rook : «♜», Knight : «♞», Bishop : «♝», King : «♚», Queen : «♛» }}

if (name == «main«):
input(«Press the Enter Key to Exit»)

Game()

The chess module is a pure Python chess library with move generation, move validation and support for common formats. We can play chess with it. It will help us to move the king queen, pawn, bishops and knights. We need to know the basics of chess to play chess with it. This module does every task in python that is possible in the real game.

Installation:

pip install chess

We just have to import the chess library and with it, we can play chess. When we will import the chess library we have to call the function named board so that we can see the status of the chess board. 

Here is the code for making calling the function board of chess library.

Python3

import chess

board=chess.Board()

print(board)

The photo on left is gui representation and picture on left is the ASCII Board

 We can find out what are the legal moves using the below code:

Python3

Output:

<LegalMoveGenerator at 0x3586100 (Nh3, Nf3, Nc3, Na3, h3, g3, f3, e3, d3, c3, b3, a3, h4, g4, f4, e4, d4, c4, b4, a4)>

If we have to move any piece, we can check with above command that which moves we can do.

Moving players:

Python3

board.push_san("e4")

print(board)

Output:

Change after the move .

To check for a checkmate:

Python3

Output:

If There is checkmate then it will be TRUE else FALSE.It will be a boolean value.

To check If it is a stalemate:

Stalemate is a situation in the game of chess where the player whose turn it is to move is not in check but has no legal move. The rules of chess provide that when stalemate occurs, the game ends as a draw.

Python3

Output:

It will return a boolean value a TRUE or FALSE.

We can detect check also with help of above function:

Python3

Output:

It will return a boolean value a TRUE or FALSE.

With the new rules from July 2014, a game ends as a draw (even without a claim) once a fivefold repetition occurs or if there are 75 moves without a pawn push or capture. Other ways of ending a game take precedence. So there methods for checking these things also=

Python3

board.is_fivefold_repetition()

board.is_seventyfive_moves()

Output:

Both of it will return a boolean value a TRUE or FALSE.

CS371: Introduction to Cognitive Science
Bryn Mawr College
Department of Computer Science
Professor Blank, Fall 2016

1.1 Game Play¶

The first thing we need to do is import the chess library:

We will use the chess library in the following manner:

  1. Create a chess.Board instance
  2. The chess.Board instance automatically generates all possible moves for the current player
  3. Current player picks a move
  4. Go to step #2 and repeat until win, lose, or draw

That’s it! Thus we have reduced the playing a valid game of chess into simply selecting a move at each turn. To play a good game of chess, you will want to pick «the best move» at each turn.

A player will be a function that takes a board instance as a argument, and returns a move encoded as a string in Universal Chess Interface format:

def player(board):
    ### "Thinking" happens here
    return move_code

We’ll explain this fully below.

1.2 The Board class¶

The Board class keeps track of whose turn it is, possible moves, and a history of all past moves. This class can undo and redo moves, and keeps track of repeating states.

First, we create a board instance:

The board.turn value is a boolean indicating whose turn it is. The values are either True for white or False for black.

As seen above, the game always begins with white’s turn. If you forget which is True, you can ask the chess module:

In [4]:

board.turn == chess.WHITE

The chess.Board class is responsible for generating all possible moves, whose turn it is, keeping track of the placement of all pieces, and making each move. The chess.Board represents a two-dimensional 8 x 8 array. However, the internal representation is optimized for speedy operations.

Here is a visual representation of a chess.Board:

Out[5]:

aabbccddeeffgghh1122334455667788

You can also get an ASCII board representation (text-only) by converting the board into a string:

r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
P P P P P P P P
R N B Q K B N R

The string representation of the board shows the character representation for each piece. Specifically:

Piece White Black
Pawn P p
Rook R r
Knight N n
Bishop B b
Queen Q q
King K k

For our uses, you don’t really need to know how each piece moves. We discuss game strategy, though, shortly.

The 2-dimensional board is laid out so that each position is indicated by a column letter and row number. However, the internal representation is sequential. Say that we wanted to see what was at location ‘c1’ we could use:

to get the internal location of the column/row, and then use:

In [9]:

str(board.piece_at(chess.C1))

Indeed, there is a white bishop at ‘c1’.

1.3 Making Moves¶

At this point, we can as the board instance to generate all of the possible, legal moves:

Out[10]:

[Move.from_uci('b1a3'),
 Move.from_uci('b1c3'),
 Move.from_uci('g1f3'),
 Move.from_uci('g1h3'),
 Move.from_uci('a2a3'),
 Move.from_uci('b2b3'),
 Move.from_uci('c2c3'),
 Move.from_uci('d2d3'),
 Move.from_uci('e2e3'),
 Move.from_uci('f2f3'),
 Move.from_uci('g2g3'),
 Move.from_uci('h2h3'),
 Move.from_uci('a2a4'),
 Move.from_uci('b2b4'),
 Move.from_uci('c2c4'),
 Move.from_uci('d2d4'),
 Move.from_uci('e2e4'),
 Move.from_uci('f2f4'),
 Move.from_uci('g2g4'),
 Move.from_uci('h2h4')]

Python Note: board.legal_moves looks like a normal list of items. But it is really a property that gets lazily generated on the fly. We force it to be a list by wrapping list() around it.

We can get the first move (index zero):

In [11]:

move = list(board.legal_moves)[0]

1.3.1 Universal Chess Interface¶

The Universal Chess Interface (or uci) is a representation for describing a move from one cell to another (and perhaps additional information as well). We explore the first move:

Thus, this is a move from b1 to a3.

What piece is this, and where is it moving on the board? Is this a good move?

The uci string is what each player function will return.

1.3.2 Standard Algebraic Notation¶

If you know something about Chess, you might know about Standard Algebraic Notation (or san). This is an alternative to uci. You can get a move’s san with:

However, we will always use uci.

1.4 Programming a Random Player¶

There is a useful function in the random module that will select from a a list of choices. This is called random.choice.

To use it in a function, we simply:

In [16]:

def random_player(board):
    move = random.choice(list(board.legal_moves))
    return move.uci()

In [18]:

for i in range(10):
    print(random_player(board))
h2h3
a2a4
g1h3
b2b3
b2b3
e2e4
g1f3
d2d4
c2c4
g1f3

1.5 Playing a Game¶

To play a game, we’ll write a new function called play_game that will take two player functions, create a board, and alternatively call the player functions until a win, lose, or draw.

First, we need some additional modules for displaying a game in the notebook:

In [19]:

import time
from IPython.display import display, HTML, clear_output

A useful function for displaying the color of a player:

In [20]:

def who(player):
    return "White" if player == chess.WHITE else "Black"

A function for displaying the board as text, or as the nice image (called SVG):

In [21]:

def display_board(board, use_svg):
    if use_svg:
        return board._repr_svg_()
    else:
        return "<pre>" + str(board) + "</pre>"
    

And finally, we can put those together to play a game:

In [22]:

def play_game(player1, player2, visual="svg", pause=0.1):
    """
    playerN1, player2: functions that takes board, return uci move
    visual: "simple" | "svg" | None
    """
    use_svg = (visual == "svg")
    board = chess.Board()
    try:
        while not board.is_game_over(claim_draw=True):
            if board.turn == chess.WHITE:
                uci = player1(board)
            else:
                uci = player2(board)
            name = who(board.turn)
            board.push_uci(uci)
            board_stop = display_board(board, use_svg)
            html = "<b>Move %s %s, Play '%s':</b><br/>%s" % (
                       len(board.move_stack), name, uci, board_stop)
            if visual is not None:
                if visual == "svg":
                    clear_output(wait=True)
                display(HTML(html))
                if visual == "svg":
                    time.sleep(pause)
    except KeyboardInterrupt:
        msg = "Game interrupted!"
        return (None, msg, board)
    result = None
    if board.is_checkmate():
        msg = "checkmate: " + who(not board.turn) + " wins!"
        result = not board.turn
    elif board.is_stalemate():
        msg = "draw: stalemate"
    elif board.is_fivefold_repetition():
        msg = "draw: 5-fold repetition"
    elif board.is_insufficient_material():
        msg = "draw: insufficient material"
    elif board.can_claim_draw():
        msg = "draw: claim"
    if visual is not None:
        print(msg)
    return (result, msg, board)

The function takes to player functions (first white, then black), and an optional argument to indicate representation style.

Let’s pit random_player vs. random_player:

In [23]:

play_game(random_player, random_player)

Move 282 Black, Play ‘g8h8’:

aabbccddeeffgghh1122334455667788

Out[23]:

(None, 'draw: claim', Board('7k/8/8/1K6/8/8/2R5/8 w - - 21 142'))

Many times, that will end in a draw.

1.6 Allowing a Human Player¶

Do you want to play a game? Here is a way to play:

In [ ]:

def human_player(board):
    display(board)
    uci = get_move("%s's move [q to quit]> " % who(board.turn))
    legal_uci_moves = [move.uci() for move in board.legal_moves]
    while uci not in legal_uci_moves:
        print("Legal moves: " + (",".join(sorted(legal_uci_moves))))
        uci = get_move("%s's move[q to quit]> " % who(board.turn))
    return uci

And a helper function to handle the input:

In [ ]:

def get_move(prompt):
    uci = input(prompt)
    if uci and uci[0] == "q":
        raise KeyboardInterrupt()
    try:
        chess.Move.from_uci(uci)
    except:
        uci = None
    return uci

Note that you must enter your move in UCI, such as «a2a4», meaning moving the piece at a2 to location a4.

Try you hand at playing chess against the random_player. It is not as easy as it sounds. Did you win? How many turns did it take?

1.7 Analysis¶

If a random_player plays a random_player many times, how many times would you expect white to win? Black to win? To end in a draw?

Let’s try it:

In [ ]:

counts = {None: 0, True: 0, False: 0}
for i in range(10):
    result, msg, board = play_game(random_player, random_player, visual=None)
    counts[result] += 1
    print(counts)
counts

1.8 Static Analysis/Board Evaluation¶

The next sample player takes each possible move, applies it to a temporary board and state, and then goes through the board, place by place, in order to compute an evaluation score for each resulting state. The moves are sorted by this score, and the best move is then returned:

In [ ]:

def player1(board):
    moves = list(board.legal_moves)
    for move in moves:
        newboard = board.copy()
        # go through board and return a score
        move.score = staticAnalysis(newboard, move, board.turn)
    moves.sort(key=lambda move: move.score, reverse=True) # sort on score
    return moves[0].uci()

The actual score is computed by the staticAnalysis function which is designed to evaluate the resulting board after each hypothesized move. To come up with a score for each static snapshot of a board, it will be necessary to know how many of each piece is left, and where they are. You can use the board.pieces() method for this:

In [24]:

board = chess.Board()

board.pieces(chess.ROOK, True)

Out[24]:

aabbccddeeffgghh1122334455667788

If you look at the output as a list, you’ll see the 1-D representation of where those pieces are on the game board:

In [25]:

list(board.pieces(chess.ROOK, True))

In [26]:

len(board.pieces(chess.ROOK, True))

There are 2 white rooks.

Now, putting that into a function, checking for each type of piece:

In [ ]:

def staticAnalysis(board, move, my_color):
    score = 0
    ## Check some things about this move:
    # score += 10 if board.is_capture(move) else 0
    # To actually make the move:
    board.push(move)
    # Now check some other things:
    for (piece, value) in [(chess.PAWN, 1), 
                           (chess.BISHOP, 4), 
                           (chess.KING, 0), 
                           (chess.QUEEN, 10), 
                           (chess.KNIGHT, 5),
                           (chess.ROOK, 3)]:
        score += len(board.pieces(piece, my_color)) * value
        score -= len(board.pieces(piece, not my_color)) * value
        # can also check things about the pieces position here
    return score

In [ ]:

play_game(player1, random_player)

NOTE: The string representation for the board is in Forsyth-Edwards Notation, or FEN for short. The last number (6th column) is the «full-move count». If the full-move count is 36, then there have been 35 * 2 full-moves, plus 1 if «b» is in second columns, for 71 moves.

That didn’t play so well! Why not?

The following is one way around the problem. What does it do differently?

In [ ]:

def staticAnalysis(board, move, my_color):
    score = random.random()
    ## Check some things about this move:
    # score += 10 if board.is_capture(move) else 0
    # To actually make the move:
    board.push(move)
    # Now check some other things:
    for (piece, value) in [(chess.PAWN, 1), 
                           (chess.BISHOP, 4), 
                           (chess.KING, 0), 
                           (chess.QUEEN, 10), 
                           (chess.KNIGHT, 5),
                           (chess.ROOK, 3)]:
        score += len(board.pieces(piece, my_color)) * value
        score -= len(board.pieces(piece, not my_color)) * value
        # can also check things about the pieces position here
    return score

In [ ]:

play_game(player1, random_player)

Better! But it still is not very aggressive. What could we add to make it attack?

In [ ]:

def staticAnalysis(board, move, my_color):
    score = random.random()
    ## Check some things about this move:
    # score += 10 if board.is_capture(move) else 0
    # To actually make the move:
    board.push(move)
    # Now check some other things:
    for (piece, value) in [(chess.PAWN, 1), 
                           (chess.BISHOP, 4), 
                           (chess.KING, 0), 
                           (chess.QUEEN, 10), 
                           (chess.KNIGHT, 5),
                           (chess.ROOK, 3)]:
        score += len(board.pieces(piece, my_color)) * value
        score -= len(board.pieces(piece, not my_color)) * value
        # can also check things about the pieces position here
    # Check global things about the board
    score += 100 if board.is_checkmate() else 0
    return score

In [ ]:

play_game(player1, random_player)

Not bad!

This staticAnalysis function makes a much better player than either of the random players, but it still has major issues. How can you improve this static evaluation function?

1.9 Suggestions¶

Pawns get promoted when they get to the back row. Encourage them to get to the back row (eg, the closer they are to the opposite side, the better).

  • It is good to threaten opponent pieces.
  • It is good that your opponent’s King has no valid moves.

Static analysis on the next move’s state can only do so much good. It would be better if you could «look ahead» further and see the results of what your opponent could do, given what your proposed move did. And then see what you could do, then what they would do, etc. This is how real chess programs work. There are many algorithms for finding the best move by looking many moves ahead, such as minimax and alpha-beta pruning. You’ll explore these ideas fully in Artificial Intelligence.

Your board evaluation function could change during the game. For example, you might use one evaluation function at the beginning, one in the middle, and another at the end. How can you tell where you are in a game?

There is a nice article on Chess Strategy at wikipedia: http://en.wikipedia.org/wiki/Chess_strategy

Понравилась статья? Поделить с друзьями:
  • Как написать шахматный движок
  • Как написать шарию личное сообщение
  • Как написать шапку характеристики
  • Как написать шапку реферата
  • Как написать шапку объяснительной