Питон Крестики-Нолики


Это консоль на основе игры "Крестики-нолики" я запрограммирован на практике мой питон. Он является объектно-ориентированным с несколько автономных функций для получения пользовательского ввода. Также в игре есть базовые ИИ, реализованный в рекурсивной функции. Буду признателен за предложения о том, что на работу или улучшений в игре функций.

main.py

from TicTacToe import TicTacToe


def main():
    game = TicTacToe()
    game.run()


if __name__ == "__main__":
    main()

TicTacToe.py

from Board import *
import Players


class TicTacToe:
    def __init__(self):
        print("Welcome to Tic Tac Toe!")
        self.board = None
        self.player1 = None
        self.player2 = None
        self.players = [None, None]

        self.new_board()
        self.new_players()

    def new_board(self) -> None:
        self.board = Board()

    def new_players(self) -> None:
        player1type, player2type = get_player_types()
        self.player1 = player1type(None)
        self.player2 = player2type(get_enemy(self.player1.mark))
        self.players = [self.player1, self.player2]

    def is_game_complete(self) -> bool:
        state = self.board.winner()
        if state is None:
            return False
        else:
            self.board.print()
            if state == Board.TIE:
                print("Tie!")
            else:
                for player in self.players:
                    if player.mark == state:
                        print(player.name + " has won!")
            return True

    def run(self) -> None:
        game_running = True
        while game_running:
            for player in self.players:
                self.board.print()
                print("It is " + player.name + "'s turn.")
                move = player.get_move(self.board)
                self.board.make_move(move, player.mark)
                print("" + player.name + " has chosen tile " + str(move + 1) + ". ")

                if self.is_game_complete():
                    if prompt_play_again():
                        self.new_board()
                    else:
                        game_running = False
                    break  # Breaks from for loop


def get_player_types() -> (object, object):
    players = get_player_number()
    if players == 0:
        return Players.Computer, Players.Computer
    if players == 1:
        return Players.Human, Players.Computer
    if players == 2:
        return Players.Human, Players.Human


def get_player_number() -> int:
    while True:
        print("Please enter number of Human Players (0-2).")
        try:
            players = int(input(">>> "))
            assert players in (0, 1, 2)
            return players
        except ValueError:
            print("\tThat is not a valid number. Try again.")
        except AssertionError:
            print("\tPlease enter a number 0 through 2.")


def prompt_play_again() -> bool:
    while True:
        print("Would you like to play again? (Y/N)")
        response = input(">>> ").upper()
        if response == "Y":
            return True
        elif response == "N":
            return False
        else:
            print("Invalid input. Please enter 'Y' or 'N'.")

Board.py

class Board:
    X_MARK = "X"
    O_MARK = "O"
    PLAYERS = (X_MARK, O_MARK)
    TIE = "T"
    BLANK = None

    winning_combos = (
        [0, 1, 2], [3, 4, 5], [6, 7, 8],
        [0, 3, 6], [1, 4, 7], [2, 5, 8],
        [0, 4, 8], [2, 4, 6])

    # unicode characters
    vrt_line = chr(9475)
    hrz_line = chr(9473)
    cross = chr(9547)

    def __init__(self):
        self.board = [Board.BLANK] * 9

    def __str__(self):
        s = u"\n"
        s += " 1 | 2 | 3 \n"
        s += "---+---+---\n"
        s += " 4 | 5 | 6 \n"
        s += "---+---+---\n"
        s += " 7 | 8 | 9 \n"

        s = s.replace("|", Board.vrt_line)
        s = s.replace('-', Board.hrz_line)
        s = s.replace('+', Board.cross)

        for tile in range(9):
            if self.get_tile(tile) is not None:
                s = s.replace(str(tile + 1), self.board[tile])
        return s

    def print(self):
        print(str(self))

    def get_tile(self, key):
        return self.board[key]

    def make_move(self, key, player):
        if self.board[key] is None:
            self.board[key] = player
            return True
        return False

    def clear_tile(self, key):
        self.board[key] = Board.BLANK

    def available_moves(self) -> list:
        return [key for key, value in enumerate(self.board) if value is None]

    def get_tiles(self, player) -> list:
        return [key for key, value in enumerate(self.board) if value == player]

    def winner(self):
        for player in Board.PLAYERS:
            positions = self.get_tiles(player)
            for combo in Board.winning_combos:
                win = True
                for pos in combo:
                    if pos not in positions:
                        win = False
                if win:
                    return player

        if len(self.available_moves()) == 0:
            return Board.TIE
        return None


def get_enemy(player):
    if player == Board.X_MARK:
        return Board.O_MARK
    elif player == Board.O_MARK:
        return Board.X_MARK
    else:
        return None

Players.py

from Board import *
from Exceptions import *
import random


class Player:

    def __init__(self, mark=None):
        if mark is None:
            self.mark = random.choice(Board.PLAYERS)
        else:
            self.mark = mark

    def get_move(self, board):
        pass


class Human(Player):

    count = 0

    def __init__(self, mark=None):
        Human.count += 1
        self.id = "Player " + str(Human.count)

        self.name = self.get_name()
        if mark is None:
            mark = self.get_mark()

        super(Human, self).__init__(mark)

    def get_move(self, board):
        available_moves = board.available_moves()
        while True:
            try:
                print("\nWhere would you like to place an '" + self.mark + "'")
                move = int(input(">>> ")) - 1

                if move < 0 or move >= 9:
                    raise InvalidMove
                if move not in available_moves:
                    raise UnavailableMove
                return move

            except InvalidMove:
                print("That is not a valid square.",
                      "Please choose another.")

            except UnavailableMove:
                print("That square has already been taken.",
                      "Please choose another.")

            except ValueError:
                print("Error converting input to a number.",
                      "Please enter the number (1-9) of the square you wish to take.")

    def get_mark(self):
        print("Hello " + self.name + "! Would you like to be 'X' or 'O'?")
        while True:
            mark = input(">>> ").upper()
            if mark in (Board.X_MARK, Board.O_MARK):
                return mark
            else:
                print("Input unrecognized. Please enter 'X' or 'O'.")

    def get_name(self):
        print(self.id + ", what is your name? ")
        return input(">>> ")


class Computer(Player):

    count = 0

    def __init__(self, mark=None):
        Computer.count += 1
        self.id = "Computer " + str(Computer.count)
        self.name = self.id

        super(Computer, self).__init__(mark)

    def __del__(self):
        Computer.count -= 1

    def get_move(self, board):
        best_score = -2
        best_moves = []
        available_moves = board.available_moves()

        if len(available_moves) == 9:
            return 4
        for move in available_moves:
            board.make_move(move, self.mark)
            move_score = self.min_max(board, get_enemy(self.mark))
            board.clear_tile(move)

            if move_score > best_score:
                best_score = move_score
                best_moves = [move]
            elif move_score == best_score:
                best_moves.append(move)
        move = random.choice(best_moves)
        return move

    def min_max(self, board, mark):
        winner = board.winner()
        if winner == self.mark:
            return 1
        elif winner == Board.TIE:
            return 0
        elif winner == get_enemy(self.mark):
            return -1
        available_moves = board.available_moves()

        best_score = None
        for move in available_moves:
            board.make_move(move, mark)
            move_score = self.min_max(board, get_enemy(mark))
            board.clear_tile(move)

            if best_score is None:
                best_score = move_score

            if mark == self.mark:
                if move_score > best_score:
                    best_score = move_score
            else:
                if move_score < best_score:
                    best_score = move_score
        return best_score

Exceptions.py

class InvalidMove(ValueError):
    def __init__(self, *args):
        super(InvalidMove, self).__init__(*args)


class UnavailableMove(ValueError):
    def __init__(self, *args):
        super(UnavailableMove, self).__init__(*args)


219
5
задан 14 апреля 2018 в 09:04 Источник Поделиться
Комментарии
2 ответа

Вот мой список мыслей (в случайном порядке). Поскольку вы не укажете какой-либо конкретной цели я рассматриваю в основном для "относительной красоты" код:


  • у вас есть self.player1, self.player2 и self.players а вы только ссылку self.players. Вы можете выполнить рефакторинг, чтобы удалить self.player1 и self.player2.

  • лично я бы поставил код в ваш main.py на дно TicTacToe.py и избавиться от main(). Можно еще импортировать из крестики-нолики, потому что thats, что if __name__[...] охранники против. Это делает больше смысла для меня, чтобы позвонить python tictactoe.py чтобы запустить его или, если вы хотите, создать __init__.py и переместить код из main.py есть. Таким образом, вы можете назвать папку для воспроизведения или импортировать папку (сделав несколько изменений) в качестве библиотеки

  • Я бы не стал играть снова в TicTacToe класс. Вместо этого разобрать весь класс и построить новый. А ты выкинь их все, построить заново и будьте уверены, вы не пропустите каких-либо переменных еще плавают

  • Я бы двигаться new_board() и new_players() в __init__(). Вы, кажется, только использовать их один раз, так что нет необходимости для них, чтобы быть функции.

  • ты можешь избавиться от всего TicTacToe.py и вместо того, чтобы сохранить 3 объекта (2 игрока и доски) в main.py. Вы можете добавить еще один класс, Score или scoreboardдля отслеживания и отчетности баллы между раундами.

  • Я не скрываю игровой цикл внутри метода класса. Это не просто сделать какое-нибудь маленькое дело с классом. Он изменяет много вещей в разных местах. Также в центральной части кода. Я бы переместить, что в main.py.

Board.py


  • title это не статический член класса; вы изменить его динамически. Сделать это свойство объекта.

  • вам не нужна функция печати. Если вы хотите печатать в stdout использовать билдинг печати, где это уместно print(board_object). Это позволяет абоненту выбрать где печатать его, например my_fancy_custom_string_stream.write(board_object)

  • вы можете реализовать геттеры и сеттеры более красиво, используя @property

  • правление не должно волновать, что игрок выиграл. Он не должен даже знать об игроках. Что должно быть tictactoe.py или какой-либо метод оценки вы решите применить на нынешних Государственного совета

  • же для применения правил о движений; хотя и можно спорить здесь. Это то, что мы заставляем доски не подходят несколько штук в одном месте (в этом случае комиссия не интересует) или это несколько штук просто не подходит (в этом случае мы не должны быть в состоянии, чтобы попытаться этот шаг, чтобы начать с).

players.py


  • игроку кажется, в порядке. Есть несколько незначительных моментов, но этот пост уже в ТЛ;ДР территории.

  • Я все еще не могу найти код, который принадлежит get_enemyя чувствую, что я слепой :Д

  • Ваш компьютер, кажется, изменить правление в ходе планирования и передается в фактическое правление

  • реализация минимаксного фактически не ориентирован на многопотоковое исполнение и много чего можно оптимизировать, но так как крестики-нолики, только очень маленький игры это не важно.

5
ответ дан 15 апреля 2018 в 06:04 Источник Поделиться


  1. Пользовательских исключений не требуется явное __init__(). Простой

    class MyCustomError(ValueError):
    pass

    будем делать. Python будет предоставить неявного конструктора для вызова класса baseclass.


  2. Аннотации типа могут ссылаться на пользовательские типы. (object, object) практически бесполезны, так как все другие типы являются производными от него.

  3. Использовать строку форматирования вместо конкатенации строк. Это яснее для чтения и скорее всего быстрее.

  4. Путем прямого литья input() для intбез проверки, если ответ численный, если пользователь (acidentally) вводит нечисловые строки, программа разбивает и печатает довольно бесполезным отладочные сообщения. Вы могли бы попробовать поймать ValueError в try: / except: блок. Если вы окажетесь повторяя тот же рисунок много раз, может быть, написать get_integer() функция, которая многократно запрашивает ввода до численного ответа не дано.

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

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

  7. Я бы посадила print() звонок от TicTacToe.__init__() для TicTacToe.run(). На мой взгляд, конструкторы не должны быть обеспокоены с начала игры (и делать ИО).

4
ответ дан 15 апреля 2018 в 06:04 Источник Поделиться