Первая попытка игры в шахматы на Python

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

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

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

Структура папок следующая (если вы хотите запустить ее самостоятельно):

+ main.py
+ board.py
/ Pieces
    + __init__.py
    + bishop.py
    + king.py
    + knight.py
    + pawn.py
    + piece.py
    + queen.py
    + rook.py

main.py

from board import Board


LETTERS = "ABCDEFGH"
NUMBERS = "12345678"

if __name__ == "__main__":
    board = Board()
    print(board.board[8:0, 7:9])

    while True:
        board.display()
        while True:
            start = input("What piece would you like to move? (eg. A2)n")
            if len(start) == 2:
                if start[0].upper() in LETTERS and start[1] in NUMBERS:
                    break
            print("Please write the coordinate in the form A2 (Letter)(Number)")
        while True:
            end = input("What square would you like to move to? (eg. A3)n")
            if len(end) == 2:
                if end[0].upper() in LETTERS and end[1] in NUMBERS:
                    break
            print("Please write the coordinate in the form A2 (Letter)(Number)")
        board.movePiece(start.upper(), end.upper())

board.py

from typing_extensions import TypedDict
import Pieces as p
import numpy as np


class Coordinate(TypedDict):
    x: int
    y: int


class Board():
    def __init__(self):
        self.board = np.full((8, 8), p.Piece("", -1, -1), dtype=p.Piece)
        self.setBoard()

    def setBoard(self):
        """"Initialise the board by creating and placing all pieces for both colours"""
        colours = ["black", "white"]
        for i in range(8):
            self.board[1][i] = p.Pawn("black", i, 1)
            self.board[6][i] = p.Pawn("white", i, 6)
        for j in (0, 1):
            pos = j * 7
            for i in (0, 7):
                self.board[pos][i] = p.Rook(colours[j], i, pos)
            for i in (1, 6):
                self.board[pos][i] = p.Knight(colours[j], i, pos)
            for i in (2, 5):
                self.board[pos][i] = p.Bishop(colours[j], i, pos)
            self.board[pos][3] = p.Queen(colours[j], 3, pos)
            self.board[pos][4] = p.King(colours[j], 4, pos)

    def display(self):
        """Print the board with borders"""
        print("-" * len(self.board) * 3)
        for i in range(len(self.board)):
            print('|' + '|'.join(map(str, self.board[i])) + '|')
            print("-" * len(self.board) * 3 + '-')

    def convertToCoords(self, position: str) -> Coordinate:
        """Convert coordinates from the Chess Syntax (ie. A2) to usable array coordinates"""
        letters = "ABCDEFGH"
        return {"y": abs(int(position[1]) - 8), "x": letters.find(position[0])}

    def convertToAlphaCoords(self, coord: Coordinate) -> str:
        """Convert coordinates from usable array coordinates to the Chess Syntax coordinates (ie. A2)"""
        letters = "ABCDEFGH"
        return letters[coord["x"]] + str(abs(coord["y"] - 8))

    def checkCollisions(self, piece: p.Piece, victim: p.Piece, start: dict, end: dict):
        """Check whether a move will collide with a piece.
           This function only applies to Rooks, Bishops and Queens."""
        minY = min(start["y"], end["y"])
        maxY = max(start["y"], end["y"])
        minX = min(start["x"], end["x"])
        maxX = max(start["x"], end["x"])
        rookMove = False
        if isinstance(piece, p.Rook) or isinstance(piece, p.Queen):
            if start["x"] == end["x"]:
                claim = self.board[minY:maxY, start["x"]]
                rookMove = True
            elif start["y"] == end["y"]:
                claim = self.board[start["y"], minX:maxX]
                rookMove = True
            if rookMove:
                for i in claim:
                    if i != piece and i != victim:
                        if i.initialised:
                            block = self.convertToAlphaCoords({"x": i.x, "y": i.y})
                            raise p.InvalidMove(f"This move is blocked by a piece at {block}")

        if isinstance(piece, p.Bishop) or isinstance(piece, p.Queen):
            claim = []
            for i in range(minX, maxX):
                for j in range(minY, maxY):
                    if abs(i - start["x"]) == abs(j - start["y"]):
                        claim.append(self.board[j][i])

            for i in claim:
                if i != piece and i != victim:
                    if i.initialised:
                        block = self.convertToAlphaCoords({"x": i.x, "y": i.y})
                        raise p.InvalidMove(f"This move is blocked by a piece at {block}")

    def movePiece(self, start: str, end: str):
        """Move a piece. It takes a starting position and an ending position,
           checks if the move is valid and then moves the piece"""
        start = self.convertToCoords(start)
        end = self.convertToCoords(end)
        piece = self.board[start["y"]][start["x"]]
        victim = self.board[end["y"]][end["x"]]
        try:
            piece.checkMove(end["x"], end["y"], victim)
            self.checkCollisions(piece, victim, start, end)
            piece.move(end)
            if isinstance(piece, p.Pawn):
                if end["y"] == 7 or end["y"] == 0:
                    piece = piece.promote()
            self.board[end["y"]][end["x"]] = piece
            self.board[start["y"]][start["x"]] = p.Piece("", -1, -1)
        except p.InvalidMove as e:
            print(e.message)

__init__.py

"""Taken from https://stackoverflow.com/a/49776782/12115915"""
import os
import sys

dir_path = os.path.dirname(os.path.abspath(__file__))
files_in_dir = [f[:-3] for f in os.listdir(dir_path)
                if f.endswith('.py') and f != '__init__.py']
for f in files_in_dir:
    mod = __import__('.'.join([__name__, f]), fromlist=[f])
    to_import = [getattr(mod, x) for x in dir(mod) if isinstance(getattr(mod, x), type)]  # if you need classes only

    for i in to_import:
        try:
            setattr(sys.modules[__name__], i.__name__, i)
        except AttributeError:
            pass

кусок.py

class Piece():
    def __init__(self, team: str, x: int, y: int, initisalised: bool = False):
        self.team = team
        self.x = x
        self.y = y
        self.initialised = initisalised
        self.moved = False

    def __str__(self) -> str:
        return "  "
    
    def checkMove(self, x: int, y: int, other):
        raise InvalidMove("You cannot move an empty square")

    def move(self, coord: dict):
        self.x = coord["x"]
        self.y = coord["y"]
        self.moved = True


class InvalidMove(Exception):
    def __init__(self, message):
        self.message = message
        super().__init__(message)

    def __str__(self) -> str:
        return self.message

bishop.py

from .piece import Piece
from .piece import InvalidMove


class Bishop(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wB"
        return "bB"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Bishops cannot take their own pieces")

        if abs(self.x - x) != abs(self.y - y):
            raise InvalidMove("Bishops can only move diagonally")

king.py

from .piece import Piece
from .piece import InvalidMove


class King(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wK"
        return "bK"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Kings cannot take their own pieces")

        if abs(x - self.x) > 1 or abs(y - self.y) > 1:
            raise InvalidMove("Kings can only move one space in any direction")

knight.py

from .piece import Piece
from .piece import InvalidMove


class Knight(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wN"
        return "bN"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Knights cannot take their own pieces")

        minMove = min(abs(x - self.x), abs(y - self.y))
        maxMove = max(abs(x - self.x), abs(y - self.y))
        if minMove != 1 or maxMove != 2:
            raise InvalidMove("Knights can only move in an L shape (2 in one direction, 1 in another)")

pawn.py

from .piece import Piece
from .piece import InvalidMove
from .rook import Rook
from .bishop import Bishop
from .knight import Knight
from .queen import Queen


class Pawn(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wP"
        return "bP"

    def promote(self) -> Piece:
        self.promoted = True
        promotions = "RNBQ"
        while True:
            choice = input("What would you like to promote the Pawn to? (R, N, B, Q) ").upper()
            if choice in promotions:
                if choice == "R":
                    return Rook(self.team, self.x, self.y)
                elif choice == "N":
                    return Knight(self.team, self.x, self.y)
                elif choice == "B":
                    return Bishop(self.team, self.x, self.y)
                elif choice == "Q":
                    return Queen(self.team, self.x, self.y)

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Pawns cannot take their own pieces")
        
        moveDist = abs(y - self.y)
        if self.x != x:
            if moveDist != 1:
                raise InvalidMove("Pawns cannot move sideways unless taking another piece diagonally one space away")
            if not other.initialised:
                raise InvalidMove("Pawns cannot move diagonally into an empty space")
        else:
            if other.initialised:
                raise InvalidMove("Pawns cannot take pieces in front of them")
        
        if moveDist > 2:
            raise InvalidMove("Pawns can only move 1 space (or two if it is their first move)")

        if moveDist == 2:
            if self.moved:
                raise InvalidMove("Pawns can only move two spaces on their first move")
        
        if self.team == "white":
            if y > self.y:
                raise InvalidMove("White Pawns cannot move down")
        else:
            if y < self.y:
                raise InvalidMove("Black Pawns cannot move up")

queen.py

from .piece import Piece
from .piece import InvalidMove


class Queen(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wQ"
        return "bQ"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Queens cannot take their own pieces")

        if not ((self.x == x or self.y == y) or (abs(self.x - x) == abs(self.y - y))):
            raise InvalidMove("Queens can only move diagonally, horizontally or vertically any amount")

rook.py

from .piece import Piece
from .piece import InvalidMove


class Rook(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wR"
        return "bR"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Rooks cannot take their own pieces")

        if self.x != x and self.y != y:
            raise InvalidMove("Rooks can only move in one direction, not diagonally")

Любая обратная связь будет принята с благодарностью!

4 ответа
4

Я думаю, это выглядит неплохо. Несколько вещей, которые я бы порекомендовал:

  1. части вашей логики checkCollision должны быть перемещены в ваши подклассы фигур. Обычно, когда ваша логика включает проверки isinstance, это хороший знак, что он не в том месте, а не объектно-ориентированный объект.
  2. В проверке checkMove есть некоторое дублирование. Ни одна фигура не должна двигаться сама по себе или выводить свою команду, поэтому общие проверки должны проводиться в базовом классе.
  3. У королевы есть возможность использовать множественное наследование или раскрыть часть логики слона / ладьи в качестве миксина, чтобы ее мог повторно использовать ферзь.

  • 1

    Спасибо за это. Что касается вашего первого замечания, я рассматривал возможность включения логики checkCollision в каждый класс, но я не думал, что передавать доску каждой части было хорошей идеей, но это имеет больше смысла. С проверкой checkMove я не мог понять, как это сделать. У вас есть ссылка на то место, где я могу прочитать об этом, но при этом сохраняю исключение по умолчанию, если оно вызывается из самого класса базовой части? Наконец, я даже не думал о множественном наследовании, но это относится к тому же, что и checkMove о непонимании того, как это сделать. Спасибо еще раз!

    — Борода

Обычно у вас не было бы метода checkMove в шахматной программе, вместо этого у вас было бы getValidMoves для каждой фигуры, а затем основывать checkMove на getValidMoves. GetValidMoves вернет список возможных ходов, которые можно использовать в простом компьютерном шахматном движке. Также в базовом классе Piece отсутствует логика, позволяющая подклассам описывать фактическое движение по направлениям или аналогичной простой логике, рассмотрите возможность добавления класса перемещения.

    • Отредактируйте имя переменной, чтобы оно соответствовало обычному стилю Python: checkMove -> check_move (и аналогично для всех методов и переменных)
    • Имена пакетов должны быть в нижнем регистре. Pieces -> pieces
    • Кроме того, имена переменных должны быть значимыми: import Pieces as p -> import Pieces
    • main.py повторяет много логики, которая может быть в функциях.
    
        def ask_square():
            while True:
                square = input("What piece would you like to move? (eg. A2)n")
                if len(start) == 2:
                    if square[0].upper() in LETTERS and square[1] in NUMBERS:
                        return square
                print("Please write the coordinate in the form A2 (Letter)(Number)")
                
                
        if __name__ == "__main__":
            board = Board()
            print(board.board[8:0, 7:9])
        
            while True:
                board.display()
                start = ask_square()
                end = ask_square()
                board.movePiece(start.upper(), end.upper())
    
    
    • Рассмотрите возможность использования именованного кортежа вместо полноценного класса для Coordinate. подробнее об этом здесь
    • colours = ["black", "white"] используется как константа. Переместите его за пределы метода и как COLOURS = ('black', 'white') (правильное именование, использование неизменяемого кортежа вместо списка)
    • Использование enumerate? for j in (0, 1): -> for j, colour in enumerate(colours):
    • convertToCoords и подобные методы используются только внутри класса Board. Почему бы не сделать их приватными? Позвони им _convertToCoords (или следуя надлежащему соглашению об именах: _convert_to_coords.
    • Пропустите его через черный или аналогичный модуль форматирования кода, чтобы он очистился (black.vercel.app/).
    • Избегайте использования встроенных магических значений. Вместо этого создайте константы. Например, BLACK = 'black', WHITE = 'white', COLOURS = (BLACK, WHITE) и используйте их в таких местах, как:
    
        def __str__(self) -> str:
            if self.team == "white":
                return "wR"
            return "bR"
    
    
    • Если все части могут быть повышены, почему бы не переместить это в класс Piece, чтобы избежать дублирования кода?
    
        def __init__(self, team: str, x: int, y: int):
            Piece.__init__(self, team, x, y, True)
            self.promoted = False  # move this to super and remove in all pieces
    
    
    • Опять же, избегайте использования волшебных, непостоянных, встроенных значений.
    
        def promote(self) -> Piece:
            self.promoted = True
            promotions = "RNBQ"
            while True:
                choice = input("What would you like to promote the Pawn to? (R, N, B, Q) ").upper()
                if choice in promotions:
                    if choice == "R":
                        return Rook(self.team, self.x, self.y)
                    elif choice == "N":
                        return Knight(self.team, self.x, self.y)
                    elif choice == "B":
                        return Bishop(self.team, self.x, self.y)
                    elif choice == "Q":
                        return Queen(self.team, self.x, self.y)
    
    
    • В более общем плане, что касается предыдущего фрагмента кода, вы смешиваете пользовательский элемент управления с моделью частей. Эти «должны» быть двумя отдельными уровнями для облегчения разделения и, например, простой замены на использование интерфейса командной строки, веб-интерфейса или какого-либо пользовательского интерфейса. Кроме того, это делает его более тестируемым (то, как разработан ваш код, смешивает пользовательское управление с моделью и логикой, что затрудняет модульное тестирование). Другими словами, у вас должно быть
        def promote(self, piece: type(Piece)) -> Piece
    

    например

        queen = pawn.promote(Queen)
    

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

    • Конечно, есть еще, но на один CR хватит! Опубликуйте снова очищенный код, когда захотите.

    У меня есть для вас один большой совет: научись любить super().

    При написании классов на Python super() функция позволяет дочернему классу делегировать часть (или все) вызова метода родительскому классу *. Например, если у меня есть занятия Base а также Derived, вот так:

    class Base:
        def greet_user(self):
            print('Hello! Nice to meet you.')
    
    class Derived(Base):
        def greet_user(self):
            super().greet_user()
            print("Isn't the weather nice today?")
    

    Затем, если я создам экземпляр Derived и позвони greet_user, вы увидите следующий результат:

    >>> d = Derived()
    >>> d.greet_user()
    Hello! Nice to meet you.
    Isn't the weather nice today?
    

    Derived.greet_user() успешно делегировал часть вызова метода Base.greet_user().

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

    class Piece:
        def __init__(self, team: str, x: int, y: int, initisalised: bool = False) -> None:
            self.team = team
            self.x = x
            self.y = y
            self.initialised = initisalised
            self.moved = False
    
        def __str__(self) -> str:
            return "  "
        
        def checkMove(self, x: int, y: int, other):
            if self.x == x and self.y == y:
                raise InvalidMove("You cannot move to the same spot")
    
            if other.team == self.team:
                raise InvalidMove(
                   f"{self.__class__.__name__}s cannot take their own pieces"
                )
    
        def move(self, coord: dict):
            self.x = coord["x"]
            self.y = coord["y"]
            self.moved = True
    

    Теперь твой King класс можно сделать намного короче:

    class King(Piece):
        def __init__(self, team: str, x: int, y: int):
            Piece.__init__(self, team, x, y, True)
            self.promoted = False
    
        def __str__(self) -> str:
            if self.team == "white":
                return "wK"
            return "bK"
    
        def checkMove(self, x: int, y: int, other):
            super().checkMove(x, y, other)
    
            if abs(x - self.x) > 1 or abs(y - self.y) > 1:
                raise InvalidMove("Kings can only move one space in any direction")
    

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


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

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *