В последние несколько дней я начал серию статей о постепенном изучении методов разработки программного обеспечения на Python.
Сериал посвящен созданию игры в крестики-нолики из командной строки. Пока что у нас есть доска и способ ввода данных пользователем. В будущем мы добавим в нашу игру всевозможные функции.
Кто знает, сколько продлится эта серия - хотя бы месяцев! Пойдем на прогулку!
Постепенное обучение
Вся эта серия основана на идее, что лучший способ учиться - постепенно.
Чтобы по-настоящему хорошо разбираться в коде, вам нужно перестать полагаться на учебные пособия и вырезать и вставить. Лучший способ практиковать свои навыки независимого программирования - это крошечные эксперименты.
Я написал целый пост, объясняющий мою философию начинать с малого.
Таким образом, эта серия не является учебным пособием. Вместо этого относитесь к этому как к совместному упражнению, строящемуся и обучающемуся вместе. Я уже какое-то время занимаюсь программированием на Python и все еще изучаю новые вещи, работая над этим простым проектом.
Где мы остановились
Мы начали с рендеринга доски для игры в крестики-нолики для пользователя.
Затем мы создали функцию, которая принимает вводимые пользователем данные и обрабатывает ошибки на них.
Сегодня мы обновим доску, используя только что собранные проверенные данные пользователя.
Напоминаем, что вот код до этого момента.
Сегодняшняя цель
Сегодня я хочу решить три основные задачи:
- Как только мы получим пользовательский ввод, добавьте на доску «X» или «O» в правильном положении.
- Повторно запрашивать у пользователя следующий ход, заполняя доску
- Чередуйте «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 другим разработчикам в моей серии писем.
Ознакомьтесь с полным списком всех постов из этой серии про крестики-нолики.