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

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

Кто знает, сколько продлится эта серия - хотя бы месяцев! Пойдем на прогулку!

Постепенное обучение

Вся эта серия основана на идее, что лучший способ учиться - постепенно.

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

Я написал целый пост, объясняющий мою философию начинать с малого.

Таким образом, эта серия не является учебным пособием. Вместо этого относитесь к этому как к совместному упражнению, строящемуся и обучающемуся вместе. Я уже какое-то время занимаюсь программированием на Python и все еще изучаю новые вещи, работая над этим простым проектом.

Где мы остановились

Мы начали с рендеринга доски для игры в крестики-нолики для пользователя.

Затем мы создали функцию, которая принимает вводимые пользователем данные и обрабатывает ошибки на них.

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

Напоминаем, что вот код до этого момента.

Сегодняшняя цель

Сегодня я хочу решить три основные задачи:

  1. Как только мы получим пользовательский ввод, добавьте на доску «X» или «O» в правильном положении.
  2. Повторно запрашивать у пользователя следующий ход, заполняя доску
  3. Чередуйте «X» и «O» на каждом ходу.

По сути, к концу сегодняшнего дня вы сможете играть в элементарные крестики-нолики!

Как всегда, мы будем работать по мини-шагам, чтобы разбить задачи. Я надеюсь, что вы будете писать код вместе со мной, а не просто копировать мои решения. Так мы будем учиться вместе!

Мини-шаг №1: сопоставьте 1–9 с i, j

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

Вспомните, как мы определили доску для пользователя:

Но! Также помните, как мы определили доску за кулисами как список списков:

board = [
    ['_', '_', '_'],
    ['_', '_', '_'],
    ['_', '_', '_']
]

Мы индексируем списки, используя скобки, так что данное место на доске будет существовать в board[i][j], где i - строка (0–2), а j - столбец (0–2).

Другими словами, наша доска выглядит так, с точки зрения i и j:

board = [
    [0|0, 0|1, 0|2],
    [1|0, 1|1, 1|2],
    [2|0, 2|1, 2|2]
]

Мы собираемся получить от пользователя число 1–9, и нам нужно преобразовать его в формат i / j, который мы можем использовать.

Примеры:

  • 1 => 0, 0
  • 4 => 1, 0
  • 8 => 2, 1

Это кажется сложной задачей. Думаешь, ты готов? Попробуйте!

Определите функцию convert_selection(selection), которая возвращает правильную пару i, j в виде кортежа.

(Нужна подсказка? Думайте деление этажей + по модулю и помните, что в Python индексы начинаются с 0.)

Мое решение:

def convert_selection(selection):
    selection -= 1
    return (selection // 3, selection % 3)

Я вычитаю единицу, и теперь мой диапазон составляет 0–8. Затем я могу использовать деление по этажам и модуль для добавления строк и столбцов.

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

Мини-шаг № 2: Обновите доску

Теперь, когда мы преобразовали пользовательский ввод, мы можем использовать его для обновления доски!

А пока давайте будем просто. Давайте поместим только «X» в то место, которое выберет пользователь.

Итак, нам нужна функция place_piece(selection, board), которая принимает в качестве аргументов кортеж выбора и плату и вносит в нее правильные изменения.

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

Попробуйте!

def place_piece(selection, board):
    board[selection[0]][selection[1]] = "X"

Мы индексируем кортеж selection, чтобы получить правильное значение для индексации в board. Woahhhhh, мета!

Мини-шаг № 3: Добавьте эти новые функции в игру

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

Давай исправим это.

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

Вот что у меня есть:

# ttt.py

def convert_selection(selection):
    selection -= 1
    return (selection // 3, selection % 3)


def place_piece(selection, board):
    board[selection[0]][selection[1]] = "X"


def print_board(board):
    for row in board:
        print(row)


def select_square():
    selection = int(input("Select a square: "))
    if not 1 <= selection <= 9:
        raise ValueError
    return selection


board = [["_" for _ in range(3)] for _ in range(3)]
print_board(board)
try:
    selection = convert_selection(select_square())
    place_piece(selection, board)
except ValueError:
    print("Sorry, please select a number 1-9")
print_board(board)

Заметьте, я вложил select_square внутрь convert_selection. Таким образом, возвращаемое значение передается автоматически, и мне не нужно сохранять его как переменную.

В этом отношении мы могли бы продолжить вложение до следующего уровня:

board = [["_" for _ in range(3)] for _ in range(3)]
print_board(board)
try:
    place_piece(convert_selection(select_square()), board)
except ValueError:
    print("Sorry, please select a number 1-9")
print_board(board)

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

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

И когда мы его запускаем:

$ python ttt.py
['_', '_', '_']
['_', '_', '_']
['_', '_', '_']
Select a square: 6
['_', '_', '_']
['_', '_', 'X']
['_', '_', '_']

Он помещает изделие в правильное место! Потрясающий.

Мини-шаг №4: повторно запросить пользователя

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

Как сделать так, чтобы пользователь мог делать несколько поворотов подряд?

Я дам вам подумать над этим немного на время и постараюсь придумать собственное решение.

(Подсказка: если бы только существовал какой-то способ заставить код цикл вернуться к самому себе…;))

Мое решение - просто поменять одну строчку!

board = [["_" for _ in range(3)] for _ in range(3)]
while True:
    print_board(board)
    try:
        selection = convert_selection(select_square())
        place_piece(selection, board)
    except ValueError:
        print("Sorry, please select a number 1-9")

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

А пока вы должны знать, что ctrl-c остановит нашу программу.

Давай попробуем!

python ttt.py 
['_', '_', '_']
['_', '_', '_']
['_', '_', '_']
Select a square: 4
['_', '_', '_']
['X', '_', '_']
['_', '_', '_']
Select a square: 2
['_', 'X', '_']
['X', '_', '_']
['_', '_', '_']
Select a square: 6
['_', 'X', '_']
['X', '_', 'X']
['_', '_', '_']
Select a square:

Бонус: рефакторинг в main ()

Теперь тело нашей программы начинает содержать реальную логику. Но возможно, что не каждый, кто получает доступ к ttt.py, захочет играть в крестики-нолики.

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

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

Узнать больше об основном методе.

Для этого мы используем специфичный для Python синтаксис в конце файла:

if __name__ == "__main__":
    main()

Затем мы перемещаем всю игровую логику в основную функцию, которую мы можем переместить в начало файла:

# ttt.py
def main():
    board = [["_" for _ in range(3)] for _ in range(3)]
    while True:
        print_board(board)
        try:
            selection = convert_selection(select_square())
            place_piece(selection, board)
        except ValueError:
            print("Sorry, please select a number 1-9")
def convert_selection(selection):
    selection -= 1
    return (selection // 3, selection % 3)
def place_piece(selection, board):
    board[selection[0]][selection[1]] = "X"
def print_board(board):
    for row in board:
        print(row)
def select_square():
    selection = int(input("Select a square: "))
    if not 1 <= selection <= 9:
        raise ValueError
    return selection
if __name__ == "__main__":
    main()

Вау, это похоже на настоящее приложение!

Мини-шаг № 5: смена игроков

Мы так близки к игре в крестики-нолики! Но вы заметите одно большое упущение.

Сейчас есть только один игрок. Только «X» может двигаться!

Нам нужно чередовать «X» и «O» на каждой итерации цикла while.

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

Не забудьте обновить функцию place_piece.

Вот мое решение:

def main():
    board = [["_" for _ in range(3)] for _ in range(3)]
    is_x = True
    while True:
        player = "X" if is_x else "O"
        print_board(board)
        try:
            selection = convert_selection(select_square())
            place_piece(selection, player, board)
        except ValueError:
            print("Sorry, please select a number 1-9")
        is_x = not is_x
...
def place_piece(selection, player, board):
    board[selection[0]][selection[1]] = player

Я создал две новые переменные is_x и player. is_x - это логическое значение, которое переворачивается каждый ход (т. Е. - is_x = not is_x). Тогда player будет либо X, либо O в зависимости от значения is_x.

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

Избыточность = время для рефакторинга!

def main():
    board = [["_" for _ in range(3)] for _ in range(3)]
    is_x = True
    while True:
        print_board(board)
        try:
            selection = convert_selection(select_square())
            place_piece(selection, is_x, board)
        except ValueError:
            print("Sorry, please select a number 1-9")
        is_x = not is_x
...
def place_piece(selection, is_x, board):
    board[selection[0]][selection[1]] = "X" if is_x else "O"

Попробуйте!

$ python ttt.py
Select a square: 5
['_', '_', '_']
['_', 'X', '_']
['_', '_', '_']
Select a square: 1
['O', '_', '_']
['_', 'X', '_']
['_', '_', '_']
Select a square: 9
['O', '_', '_']
['_', 'X', '_']
['_', '_', 'X']
Select a square: 2
['O', 'O', '_']
['_', 'X', '_']
['_', '_', 'X']
Select a square: 3
['O', 'O', 'X']
['_', 'X', '_']
['_', '_', 'X']
Select a square: 6
['O', 'O', 'X']
['_', 'X', 'O']
['_', '_', 'X']
Select a square: 7
['O', 'O', 'X']
['_', 'X', 'O']
['X', '_', 'X']
Select a square:

Оно работает! Мы в деле!

Подведение итогов

Сегодня мы добились многого. Теперь мы можем успешно играть в крестики-нолики в нашем приложении.

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

Кроме того, если вы играете с текущей версией, X может перезаписать ход O и наоборот. Нам нужен способ сделать ходы постоянными.

Но сейчас мы на верном пути, и это здорово!

Вот сегодняшний кодекс.

Увидимся в следующий раз!

О Беннетте

Я веб-разработчик, создающий вещи с помощью Python и JavaScript.

Хотите, чтобы мой лучший контент по веб-разработке и стал лучшим программистом?

Я делюсь своим любимым советом со своим списком рассылки - никакого спама, ничего продажного, только полезный контент.

Присоединяйтесь к 500 другим разработчикам в моей серии писем.

Ознакомьтесь с полным списком всех постов из этой серии про крестики-нолики.