ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 399
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
294
Глава 14.Проекты для тренировки
FUNCTIONS
displayDisk(width)
Выводит диск заданной ширины.
--snip--
При необходимости в doc-строку модуля можно добавить больше слов или даже абзацев. Я ограничился минимумом текста, потому что программа очень проста.
После doc-строки модуля следуют команды import
:
import copy import sys
Black форматирует их как отдельные команды вместо одной команды import copy
, sys
. При таком подходе будет проще отслеживать добавление или удаление им- портированных модулей в системах контроля версий (таких как Git), которые отслеживают изменения, вносимые программистом.
Затем определяются константы, требуемые программе:
TOTAL_DISKS = 5 # Чем больше дисков, тем сложнее головоломка.
# Изначально все диски находятся на стержне A:
SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1))
Мы определяем константы в начале файла, чтобы сгруппировать их, и делаем их глобальными. Имена записываем в верхнем регистре согласно «змеиной» схеме, чтобы пометить их как константы.
Константа
TOTAL_DISKS
определяет, сколько дисков используется в головоломке.
Переменная
SOLVED_TOWER
содержит пример списка, содержащего решение голово- ломки: в списке указаны все диски, самый большой расположен внизу, а самый ма- ленький — наверху. Это значение генерируется на основании значения
TOTAL_DISKS
и для пяти дисков список имеет вид
[5,
4,
3,
2,
1]
Обратите внимание: в файле отсутствуют аннотации типов. Дело в том, что типы всех переменных, параметров и возвращаемых значений автоматически определя- ются на основании кода. Например, константе
TOTAL_DISKS
присваивается целое значение 5. По нему системы проверки типов (такие как Mypy) определяют, что
TOTAL_DISKS
будет содержать только целые числа.
Мы определяем функцию main()
, которая вызывается программой в конце файла:
def main():
"""Проводит одну игру Ханойская башня."""
print(
"""THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com
Головоломка «Ханойская башня»
295
Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk.
More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi
"""
)
Функции также могут содержать doc-строки. Обратите внимание на doc-строку main()
под командой def
. Чтобы просмотреть эту doc-строку, выполните команды import towerofhanoi и help(towerofhanoi.main)
из интерактивной оболочки.
Далее следует комментарий, который описывает структуру данных, используемую для представления башни, потому что она занимает центральное место в работе программы:
"""Словарь towers содержит ключи "A", "B" и "C" и значения - списки,
представляющие стопку дисков. Список содержит целые числа, представляющие диски разных размеров, а начало списка представляет низ башни. Для игры с 5 дисками список [5, 4, 3, 2, 1] представляет заполненную башню. Пустой список list [] представляет башню без дисков. В списке [1, 3] больший диск находится на меньшем диске, такая конфигурация недопустима. Список [3, 1]
допустим, так как меньшие диски могут размещаться на больших."""
towers = {"A": copy.copy(SOLVED_TOWER), "B": [], "C": []}
Список
SOLVED_TOWER
используется как стек — одна из простейших структур данных, используемых при разработке. Стек представляет собой упорядоченный список значений, а его состояние может изменяться только добавлением (занесе- нием в стек) или удалением (извлечением из стека) значений на вершине (начале) стека. Эта структура данных идеально подходит для представления башен в нашей программе. Список Python можно преобразовать в стек; для этого нужно исполь- зовать метод append()
для включения элементов и метод pop()
для их извлечения и избегать изменения списка любыми другими способами. Конец списка будет рассматриваться как вершина стека.
Каждое целое число в списке towers представляет один диск определенного разме- ра. Например, в игре с пятью дисками список
[5,
4,
3,
2,
1]
представляет полную стопку дисков от самого большего (
5
) внизу до самого маленького (
1
) наверху.
Также обратите внимание на то, что в комментарии приведены примеры допусти- мого и недопустимого списка.
Внутри функции main()
находится бесконечный цикл, в котором выполняется один ход нашей головоломки:
while True: # Один ход для каждой итерации цикла.
# Вывести башни и диски:
displayTowers(towers)
296
Глава 14.Проекты для тренировки
# Запросить ход у пользователя:
fromTower, toTower = getPlayerMove(towers)
# Переместить верхний диск с fromTower на toTower:
disk = towers[fromTower].pop()
towers[toTower].append(disk)
За один ход игрок смотрит на текущее состояние башен и вводит свой ход. Затем программа обновляет структуру данных towers
. Подробности выполнения этих операций скрыты в функциях displayTowers()
и getPlayerMove()
. Благодаря со- держательным именам функция main()
дает общее представление о том, что делает программа.
Следующие строки проверяют, решил ли игрок головоломку, для чего решение из
SOLVED_TOWER
сравнивается с towers["B"]
и towers["C"]
:
# Проверить, решена ли головоломка:
if SOLVED_TOWER in (towers["B"], towers["C"]):
displayTowers(towers) # Вывести башни в последний раз.
print("You have solved the puzzle! Well done!")
sys.exit()
Сравнивать с towers["A"]
не нужно, потому что в начале игры стержень уже со- держит завершенную башню; чтобы решить головоломку, игрок должен постро- ить башню на стержне B или C. Обратите внимание:
SOLVED_TOWER
используется повторно для генерирования начальных башен и проверки того, решил ли игрок головоломку. Так как
SOLVED_TOWER
является константой, можно быть уверенным в том, что
SOLVED_TOWER
всегда будет иметь значение, присвоенное в начале кода.
Используемое условие эквивалентно
SOLVED_TOWER
==
towers["B"]
or
SOLVED_TOWER
==
towers["C"]
, но записывается короче — эту идиому Python я рассматривал в главе 6. Если условие истинно, значит, игрок решил головоломку и программа завершается. В противном случае цикл продолжается следующим ходом.
Функция getPlayerMove()
запрашивает у игрока ход и проверяет его по игровым правилам:
def getPlayerMove(towers):
"""Запрашивает ход у пользователя. Возвращает (fromTower, toTower)."""
while True: # Пока пользователь не введет допустимый ход.
print('Enter the letters of "from" and "to" towers, or QUIT.')
print("(e.g., AB to moves a disk from tower A to tower B.)")
print()
response = input("> ").upper().strip()
Мы начинаем бесконечный цикл, который продолжается до выполнения одного из двух условий: либо цикл прерывается командой return
(с выходом из функ- ции), либо программа будет завершена вызовом sys.exit()
. Первая часть цикла
Головоломка «Ханойская башня»
297
предлагает игроку ввести ход в виде двух букв — для башни, с которой перемещается диск и на которую он перемещается.
Обратите внимание на инструкцию input(">
").upper().strip()
, которая получает ввод с клавиатуры от игрока. Вызов input(">
")
запрашивает текст у игрока с вы- водом приглашения
>
. Символ показывает, что игрок должен что-то ввести. Если программа не выведет приглашение, то игрок может подумать, что она зависла.
Строка, полученная от input()
, преобразуется к верхнему регистру вызовом ме- тода upper()
. Это позволяет игроку вводить обозначения башен как в верхнем, так и в нижнем регистре — например,
'a'
или 'A'
для башни A. Затем для строки в верхнем регистре вызывается метод strip()
для удаления пробельных символов в начале и конце строки на случай, если пользователь случайно добавил пробел при вводе хода. Такие удобства несколько упрощают работу с программой для пользователя.
Все еще внутри функции getPlayerMove()
мы проверяем данные, введенные поль- зователем:
if response == "QUIT":
print("Thanks for playing!")
sys.exit()
# Убедиться в том, что пользователь ввел допустимые обозначения башен:
if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
print("Enter one of AB, AC, BA, BC, CA, or CB.")
continue # Снова запросить ход.
Если пользователь вводит 'QUIT'
(в произвольном регистре и даже с пробелами в начале или конце строки благодаря вызовам upper()
и strip()
), программа за- вершается. Также функция getPlayerMove()
могла бы вернуть 'QUIT'
, чтобы указать вызывающей стороне на необходимость вызвать sys.exit()
, вместо того чтобы вы- зывать sys.exit()
в getPlayerMove()
. Но это усложнило бы возвращаемое значение getPlayerMove()
: функция должна возвращать либо кортеж с двумя строками (для хода игрока), либо одну строку 'QUIT'
. Функция, которая возвращает значения одного типа данных, более понятна, чем та, которая может возвращать значения многих возможных типов. Эта тема обсуждалась в разделе «Возвращаемые значения всегда должны иметь один тип данных», с. 212.
Из трех башен можно составить только шесть комбинаций «с какой башни — на какую башню». Несмотря на тот факт, что мы жестко зафиксировали все шесть значений в условии, проверяющем ход, такой код читается намного проще, чем что-нибудь вроде конструкции вида len(response)
!=
2
or response[0]
not in
'ABC'
or response[1]
not in
'ABC'
or response[0]
==
response[1]
. С учетом этого факта подход с жестко фиксированными вариантами оказывается наиболее пря- молинейным.
298
Глава 14.Проекты для тренировки
Как правило, использование «магических» значений вроде "AB"
,
"AC"
и т. д. считается нежелательным, потому что они работают, только пока в программе используются три стержня. Но хотя количество дисков можно отрегулировать изменением константы
TOTAL_DISKS
, крайне маловероятно, что в игре увеличится количество стержней.
В данном случае запись всех возможных перемещений в программе допустима.
Две новые переменные fromTower и toTower создаются как содержательные имена для данных. Они не имеют функционального назначения, но лучше читаются, чем response[0
] и response[1]
:
# Более содержательные имена переменных:
fromTower, toTower = response[0], response[1]
Затем мы проверяем, есть ли для созданных пользователем башен допустимый ход:
if len(towers[fromTower]) == 0:
# Башня fromTower не может быть пустой:
print("You selected a tower with no disks.")
continue # Снова запросить ход.
elif len(towers[toTower]) == 0:
# На пустую башню можно переместить любой диск:
return fromTower, toTower elif towers[toTower][-1] < towers[fromTower][-1]:
print("Can't put larger disks on top of smaller ones.")
continue # Снова запросить ход.
Если ход недопустим, команда continue возвращает управление в начало цикла, где игроку снова предлагается ввести ход. Затем мы проверяем, не пуста ли башня toTower
. Если она пуста, возвращается fromTower,
toTower
, чтобы подчеркнуть, что ход допустим, потому что диск всегда можно поместить на пустой стержень. Первые два условия проверяют, что к моменту проверки третьего условия towers[toTower]
и towers[fromTower]
не будут пустыми и не вызовут ошибки
IndexError
. Условия были упорядочены так, чтобы их порядок предотвращал
IndexError и дополни- тельные проверки.
Важно, чтобы ваши программы обрабатывали любой недопустимый ввод от поль- зователя или потенциальные ошибочные ситуации. Пользователи могут не знать, что нужно ввести, или могут допустить опечатку при вводе. Также файлы могут быть неожиданно удалены или в базе данных может произойти сбой. Ваши про- граммы должны обладать достаточной устойчивостью к аномальным ситуациям; в противном случае возможны аварийные завершения или позднее могут возник- нуть коварные ошибки. Если ни одно из условий не равно
True
, getPlayerMove()
возвращает fromTower,
toTower
:
else:
# Допустимый ход, вернуть выбранные башни:
return fromTower, toTower
Головоломка «Ханойская башня»
299
В Python команды return всегда возвращают одно значение. Хотя может показаться, что эта команда return возвращает два значения, Python в действительности воз- вращает один кортеж с двумя значениями, что эквивалентно return
(fromTower,
toTower)
. Программисты на языке Python часто опускают круглые скобки в этом контексте. Круглые скобки определяют кортеж в меньшей степени, чем запятые.
Обратите внимание: программа вызывает функцию getPlayerMove()
только один раз из функции main()
. Функция не избавляет от дублирования кода — самой рас- пространенной причины для использования функций. Нет никаких причин, по которым весь код getPlayerMove()
нельзя было бы разместить в функции main()
Но функции также могут использоваться как механизм структурирования кода по отдельным блокам, для чего в данном случае и задействована getPlayerMove()
С этой функцией main()
не будет слишком длинной и громоздкой.
Функция displayTowers()
выводит диски на башнях A, B и C в аргументе towers
:
def displayTowers(towers):
"""Выводит три башни с дисками."""
# Вывести три башни:
for level in range(TOTAL_DISKS, -1, -1):
for tower in (towers["A"], towers["B"], towers["C"]):
if level >= len(tower):
displayDisk(0) # Вывести пустой стержень без диска.
else:
displayDisk(tower[level]) # Вывести диск.
print()
Для вывода каждого диска в башне функция зависит от функции displayDisk()
, которая будет рассмотрена следующей. Цикл for level проверяет каждый воз- можный диск, а цикл for tower проверяет башни A, B и C.
Функция displayTowers()
вызывает displayDisk()
для вывода каждого диска с за- данной шириной, или при передаче 0 выводится только стержень без диска:
# Вывести обозначения башен A, B и C:
emptySpace = " " * (TOTAL_DISKS)
print("{0} A{0}{0} B{0}{0} C\n".format(emptySpace))
На экране выводятся метки A, B и C. Эта информация помогает игроку различать баш- ни, а также подчеркивает, что башни обозначаются буквами A, B и C, а не 1, 2 и 3, или
«Левая», «Средняя» и «Правая». Я не стал использовать цифры 1, 2 и 3 для пометки башен, чтобы игроки не путали эти числа с числовыми обозначениями размера дисков.
Переменной emptySpace присваивается количество пробелов между метками, которое в свою очередь вычисляется на основании
TOTAL_DISKS
, потому что чем больше дисков в игре, тем больше разделяющее их расстояние. Вместо f-строк, как в print(f'{emptySpace}
A{emptySpace}{emptySpace}
B{emptySpace}{emptySpace}
300
Глава 14.Проекты для тренировки
C\n')
, используется метод строк format()
. Это позволяет применить один и тот же аргумент emptySpace всюду, где в соответствующей строке встречается
{0}
, в резуль- тате чего код получается более коротким и лучше читается, чем в версии с f-строкой.
Функция displayDisk()
выводит один диск заданной ширины. Если диск отсут- ствует, выводится только стержень:
def displayDisk(width):
"""Выводит диск заданной ширины. Ширина 0 означает отсутствие диска."""
emptySpace = " " * (TOTAL_DISKS - width)
if width == 0:
# Вывести сегмент стержня без диска:
print(f"{emptySpace}||{emptySpace}", end="")
else:
# Вывести диск:
disk = "@" * width numLabel = str(width).rjust(2, "_")
print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end="")
Изображение диска состоит из начального пробела, символов
@
в количестве, равном ширине диска, двух символов ширины (с начальным символом подчеркивания, если ширина задается одной цифрой), еще одной серии символов
@
и завершающего пробела. Чтобы вывести только пустой стержень, достаточно вывести начальный пробел, две вертикальные черты и завершающий пробел. В результате для вывода следующей башни потребуются шесть вызовов displayDisk()
с шестью разными аргументами ширины:
||
@_1@
@@_2@@
@@@_3@@@
@@@@_4@@@@
@@@@@_5@@@@@
Обратите внимание на то, как функции displayTowers()
и displayDisk()
разделяют обязанности по выводу башен. Хотя функция displayTowers()
решает, как интер- претировать структуры данных, представляющие каждую башню, она зависит от displayDisk()
для отображения каждого диска на башне. Разбиение программы на меньшие функции упрощает тестирование каждой части. Если программа не- правильно выводит диски, то, скорее всего, проблема в displayDisk()
. Если диски следуют в неправильном порядке, то, вероятно, проблема в displayTowers()
В любом случае объем кода, который необходимо отладить, будет намного меньше.
Для вызова функции main()
используется стандартная идиома Python:
# Если программа была запущена (а не импортирована), начать игру:
if __name__ == "__main__":
main()
Игра «Четыре в ряд»
301
Python автоматически присваивает переменной
__name__
значение '__main__'
, если игрок запускает программу towerofhanoi.py напрямую. Но если кто-то импортирует программу как модуль командой import towerofhanoi
, то
__name__
будет присво- ено значение 'towerofhanoi'
. Строка if
__name__
==
'__main__':
будет вызывать функцию main()
, если кто-то запускает нашу программу, тем самым начиная игру.
Но если вы хотите просто импортировать программу как модуль, чтобы, допустим, вызывать ее отдельные функции для модульного тестирования, то это условие дает результат
False
, и функция main()
не вызывается.
1 ... 27 28 29 30 31 32 33 34 ... 40
Игра «Четыре в ряд»
Это игра для двух игроков: каждый пытается выстроить ряд из четырех своих фишек по горизонтали, по вертикали или по диагонали, помещая свои фишки на самое нижнее свободное место в столбце. В этой игре используется вертикальная доска 7
× 6. В нашей реализации игры два игрока-человека (обозначаемые X и O) играют друг с другом (режим игры с компьютером не поддерживается).
Вывод результатов
При запуске программы «Четыре в ряд» вывод будет выглядеть примерно так:
Four-in-a-Row, by Al Sweigart al@inventwithpython.com
Two players take turns dropping tiles into one of seven columns, trying to make four in a row horizontally, vertically, or diagonally.
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|.......|
+-------+
Player X, enter 1 to 7 or QUIT:
> 1 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|X......|
+-------+
Player O, enter 1 to 7 or QUIT:
--snip--
302
Глава 14.Проекты для тренировки
Player O, enter 1 to 7 or QUIT:
> 4
1234567
+-------+
|.......|
|.......|
|...O...|
|X.OO...|
|X.XO...|
|XOXO..X|
+-------+
Player O has won!
Игрок должен найти такую стратегию, которая позволит ему выстроить четыре фишки в ряд, одновременно не позволяя сделать то же самое противнику.
Исходный код
Откройте в редакторе или IDE новый файл и введите приведенный ниже код. Со- храните файл под именем fourinarow.py
"""Four-in-a-Row, by Al Sweigart al@inventwithpython.com
Игра на выстраивание четырех фишек в ряд."""
import sys
# Константы, используемые для вывода игрового поля:
EMPTY_SPACE = "." # Точки проще подсчитать, чем пробелы.
PLAYER_X = "X"
PLAYER_O = "O"
# Примечание: если BOARD_WIDTH изменится, обновите BOARD_TEMPLATE и COLUMN_LABELS.
BOARD_WIDTH = 7
BOARD_HEIGHT = 6
COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7")
assert len(COLUMN_LABELS) == BOARD_WIDTH
# Шаблонная строка для вывода игрового поля:
BOARD_TEMPLATE = """
1234567
+-------+
|{}{}{}{}{}{}{}|
|{}{}{}{}{}{}{}|
|{}{}{}{}{}{}{}|
|{}{}{}{}{}{}{}|
|{}{}{}{}{}{}{}|
|{}{}{}{}{}{}{}|
+-------+"""
def main():
"""Проводит одну игру Четыре в ряд."""
Игра «Четыре в ряд»
303
print(
"""Four-in-a-Row, by Al Sweigart al@inventwithpython.com
Два игрока по очереди опускают фишки в один из семи столбцов,
стараясь выстроить четыре фишки по вертикали, горизонтали или диагонали.
"""
)
# Подготовка новой игры:
gameBoard = getNewBoard()
playerTurn = PLAYER_X
while True: # Обрабатывает ход игрока.
# Вывод игрового поля и получение хода игрока:
displayBoard(gameBoard)
playerMove = getPlayerMove(playerTurn, gameBoard)
gameBoard[playerMove] = playerTurn
# Проверка победы или ничьей:
if isWinner(playerTurn, gameBoard):
displayBoard(gameBoard) # В последний раз вывести поле.
print("Player {} has won!".format(playerTurn))
sys.exit()
elif isFull(gameBoard):
displayBoard(gameBoard) # В последний раз вывести поле.
print("There is a tie!")
sys.exit()
# Ход передается другому игроку:
if playerTurn == PLAYER_X:
playerTurn = PLAYER_O
elif playerTurn == PLAYER_O:
playerTurn = PLAYER_X
def getNewBoard():
"""Возвращает словарь, представляющий игровое поле.
Ключи - кортежи (columnIndex, rowIndex) с двумя целыми числами,
а значения - одна из строк "X", "O" or "." (пробел)."""
board = {}
for rowIndex in range(BOARD_HEIGHT):
for columnIndex in range(BOARD_WIDTH):
board[(columnIndex, rowIndex)] = EMPTY_SPACE
return board def displayBoard(board):
"""Выводит на экран игровое поле и фишки."""
# Подготовить список, передаваемый строковому методу format() для
# шаблона игрового поля. Список содержит все фишки игрового поля
# и пустые ячейки, перечисляемые слева направо, сверху вниз:
tileChars = []
for rowIndex in range(BOARD_HEIGHT):
for columnIndex in range(BOARD_WIDTH):
tileChars.append(board[(columnIndex, rowIndex)])