Учим Python, делая крутые игры 2018. Invent your owncomputer gameswith python
Скачать 6.56 Mb.
|
Ссылки на список В интерактивной оболочке введите следующие команды: >>> spam = 42 >>> cheese = spam >>> spam = 100 >>> spam 100 >>> cheese 42 Переменной spam присваивается значение 42, затем значение spam пере- дается переменной cheese. Потом, когда переменной spam присваивается значение 100, это не оказывает никакого влияния на переменную cheese. Так происходит, потому что spam и cheese — разные переменные и хранят разные значения. Но списки работают не так. Когда список сохраняется в перемен- ной, в действительности в переменную передается не сам список, а ссылка на него. Ссылка — это некоторая величина, которая указывает, где хранится информация. Рассмотрим фрагмент кода, который упростит понимание это- го факта. Наберите следующее: u >>> spam = [0, 1, 2, 3, 4, 5] v >>> cheese = spam w >>> cheese[1] = 'Привет!' >>> spam [0, 'Привет!', 2, 3, 4, 5] Игра «Крестики-нолики» 173 >>> cheese [0, 'Привет!', 2, 3, 4, 5] В коде изменяется только список cheese, но, кажется, изменились оба спи- ска — cheese и spam. Это произошло потому, что переменная spam не содержит значений списка как таковых, а только ссылки на них, как показано на рис. 10.5. Списки не содержатся в переменных, а существуют совершенно самостоя- тельно. Reference spam spam = [0, 1, 2, 3, 4, 5] [0, 1, 2, 3, 4, 5] (a list value) (значение списка) Ссылка Рис. 10.5. Список spam, созданный на шаге . Переменная хранит не список, а ссылку на него Замечу, что выражение cheese = spam копирует из spam в cheese ссылку на список, а не сам список. Теперь обе переменные, spam и cheese, хранят ссылки, ко- торые указывают на одни и те же значения списка. Но существует только один список, который никуда не копируется. На рис. 10.6 показан механизм такого копирования. Reference spam cheese = spam [0, 1, 2, 3, 4, 5] (a list value) Reference cheese Ссылка Ссылка (значение списка) Рис. 10.6. Переменные spam и cheese хранят ссылки на один список 174 Глава 10 Итак, выражение cheese[1] = 'Привет!' в изменяет тот же список, на который ссылается переменная spam. Вот почему переменная spam возвращает то же самое значение, что и переменная cheese. Они обе содержат ссылки, указывающие на один список, как показано на рис. 10.7. Reference spam cheese[1] = 'Hello' [0, 'Hello', 2, 3, 4, 5] (a list value) Reference cheese Ссылка Ссылка (значение списка) 'Привет!' 'Привет!' Рис. 10.7. Изменения списка затрагивают все переменные, содержащие ссылку на этот список Если необходимо сохранить в переменных spam и cheese разные списки, то надо создать два списка, а не копировать ссылки на один и тот же. >>> spam = [0, 1, 2, 3, 4, 5] >>> cheese = [0, 1, 2, 3, 4, 5] В приведенном примере переменные spam и cheese хранят два разных спи- ска (путь даже списки идентичны по содержанию). Теперь, если модифици- ровать один список, это не произведет никакого эффекта на другой. >>> spam = [0, 1, 2, 3, 4, 5] >>> cheese = [0, 1, 2, 3, 4, 5] >>> cheese[1] = 'Привет!' >>> spam [0, 1, 2, 3, 4, 5] >>> cheese [0, 'Привет!', 2, 3, 4, 5] Рисунок 10.8 демонстрирует, как в этом примере переменные взаимодей- ствуют со значениями списков. Игра «Крестики-нолики» 175 Словари работают аналогичным образом. Переменные хранят не слова- ри, а лишь ссылки на них. Reference spam [0, 'Hello', 2, 3, 4, 5] (a list value) Reference cheese [0, 1, 2, 3, 4, 5] (a list value) Ссылка Ссылка (значение списка) (значение списка) 'Привет!' Рис. 10.8. Теперь переменные spam и cheese хранят ссылки на разные списки Использование ссылок на списки в функции makeMove() Вернемся к функции makeMove(). 36. def makeMove(board, letter, move): 37. board[move] = letter Когда список передается в переменную board, локальная переменная функ- ции в действительности копирует ссылку на список, не сам список. Поэтому любые изменения в board в коде функции, будут применены к самому списку. Несмотря на то, что board — локальная переменная, функция makeMove() мо- дифицирует сам список. А вот переменные letter и move — это копии передаваемых строкового и целочисленного параметров. Поэтому, если внутри функции makeMove() мо- дифицировать letter или move, это не приведет к изменению исходных значе- ний переменных. Проверка — не победил ли игрок Строки 42–49 в функции isWinner() — это, по сути, длинная инструкция return 39. def isWinner(bo, le): 40. # Учитывая заполнение игрового поля и буквы игрока, эта функция возвращает True, если игрок выиграл. 176 Глава 10 41. # Мы используем "bo" вместо "board" и "le" вместо "letter", поэтому нам не нужно много печатать. 42. return ((bo[7] == le and bo[8] == le and bo[9] == le) or # across the top 43. (bo[4] == le and bo[5] == le and bo[6] == le) or # через центр 44. (bo[1] == le and bo[2] == le and bo[3] == le) or # через низ 45. (bo[7] == le and bo[4] == le and bo[1] == le) or # вниз по левой стороне 46. (bo[8] == le and bo[5] == le and bo[2] == le) or # вниз по центру 47. (bo[9] == le and bo[6] == le and bo[3] == le) or # вниз по правой стороне 48. (bo[7] == le and bo[5] == le and bo[3] == le) or # по диагонали 49. (bo[9] == le and bo[5] == le and bo[1] == le)) # по диагонали Слова bo и le — короткая форма имен board и letter. Короткая форма запи си позволяет меньше печатать. Напомню, что интерпретатору Python безразлично, как вы назовете переменные. Существуют восемь вариантов победы в игре «Крестики-нолики»: можно провести линию через верхний, средний или нижний ряды; можно провести линию вдоль левого, среднего или правого столбцов; или можно провести линию по одной из диагоналей. Каждая строка условия проверяет, равны ли все три значения клеток, об- разующих линию, значению letter (в комбинации с оператором and). Между собой строки объединяются оператором or, чтобы проверить все варианты победы. Это значит, что лишь одна строка должна быть истинна, чтобы объя- вить победу игрока, чья буква содержится в переменной letter. Пусть значение переменной le равно 'O', а значение переменной bo равно [' ', 'O', 'O', 'O', ' ', 'X', ' ', 'X', ' ', ' '] . Содержимое переменной board будет выглядеть так: X| | -+-+- |X| -+-+- O|O|O Здесь приведена иллюстрация того, как будет преобразовано выражение после ключевого слова return в строке 42. Сначала Python замещает перемен- ные bo и le их значениями. return (('X' == 'O' and ' ' == 'O' and ' ' == 'O') or (' ' == 'O' and 'X' == 'O' and ' ' == 'O') or ('O' == 'O' and 'O' == 'O' and 'O' == 'O') or ('X' == 'O' and ' ' == 'O' and 'O' == 'O') or Игра «Крестики-нолики» 177 (' ' == 'O' and 'X' == 'O' and 'O' == 'O') or (' ' == 'O' and ' ' == 'O' and 'O' == 'O') or ('X' == 'O' and 'X' == 'O' and 'O' == 'O') or (' ' == 'O' and 'X' == 'O' and 'O' == 'O')) Затем Python вычисляет все инструкции сравнения == в скобках и приво- дит к логическим значениям. return ((False and False and False) or (False and False and False) or (True and True and True) or (False and False and True) or (False and False and True) or (False and False and True) or (False and False and True) or (False and False and True)) После чего интерпретатор вычисляет значения выражений в круглых скобках. return ((False) or (False) or (True) or (False) or (False) or (False) or (False) or (False)) И наконец, после того как в круглых скобках останется по одному значе- нию, от них можно избавиться: return (False or False or True or False or False or False or False or False) 178 Глава 10 Теперь Python вычисляет общее значение выражения с операторами or. return (True) И еще раз избавляется от скобок, оставляя одно значение. return True Таким образом, для выбранных значений переменных bo и le будет воз- вращено значение True. Так программа может определить, выиграл ли один из игроков. Дублирование данных игрового поля Функция getBoardCopy() позволяет создать копию 10-строкового списка, представляющего игровое поле в игре «Крестики-нолики». 51. def getBoardCopy(board): 52. # Создает копию игрового поля и возвращает его. 53. boardCopy = [] 54. for i in board: 55. boardCopy.append(i) 56. return boardCopy Когда алгоритм ИИ планирует ход от лица компьютера, бывает нужно мо- дифицировать временную копию игрового поля, не изменяя исходного. В та- ких случаях создается копия списка board посредством вызова этой функции. Новый список создается в строке 53. Во-первых, сохраняется пустой список в переменной boardCopy. Затем в цикле for производится перебор всех элемен- тов board и копии всех строк добавляются в дубликат. После того как функция getBoardCopy() завершит построение копии текущих значений списка board, она передает в переменную boardCopy ссылку на новый список, а не на исходный. Проверка — свободна ли клетка игрового поля Простая функция isSpaceFree() определяет возможность хода в соответ- ствии с ситуацией на игровом поле. 58. def isSpaceFree(board, move): 59. # Возвращает True, если сделан ход в свободную клетку. 60. return board[move] == ' ' Игра «Крестики-нолики» 179 Напомню, что свободные клетки в списке board — это строковые пере- менные, содержащие единичный пробел. Если какой-либо элемент с индек- сом пустой клетки не равен ' ', то он замещается пробелом. Разрешение игроку сделать ход Функция getPlayerMove() предлагает игроку ввести номер клетки, в кото- рую он хочет сделать ход. 62. def getPlayerMove(board): 63. # Разрешение игроку сделать ход. 64. move = ' ' 65. while move not in '1 2 3 4 5 6 7 8 9'.split() or not isSpaceFree(board, int(move)): 66. print('Ваш следующий ход? (1-9)') 67. move = input() 68. return int(move) Условие в строке 65 принимает значение True, если выражение с правой или левой стороны оператора or истинно. Цикл гарантирует, что выполнение про- граммы не будет продолжено, пока пользователь не введет целое число в диапа- зоне от 1 до 9. Заодно проверяется, что затребованная клетка свободна с учетом передаваемых в функцию параметров, определяющих состояние игрового поля. Две строки кода цикла while просто предлагают игроку ввести число от 1 до 9. Выражение в левой части проверяет, ввел ли игрок значение, равное '1', '2' , '3' и так далее до '9', создавая список этих строк (с помощью метода split() ) и проверяя, содержится ли значение move в этом списке. Выражение '1 2 3 4 5 6 7 8 9'.split() эквивалентно выражению ['1', '2', '3', '4', '5', '6', '7', '8', '9'] , просто форма более удобна для набора кода. Выражение в правой части оператора проверяет, свободна ли клетка, за- требованная пользователем, вызывая функцию isSpaceFree(). Напомню, что функция isSpaceFree() возвращает значение True, если предложенный игроком ход допустим. Необходимо отметить, что isSpaceFree() ожидает, что перемен- ная move содержит целочисленное значение, поэтому используется функция int() , которая преобразует содержимое переменной move к целому типу. Оператор not присутствует с обеих сторон, поэтому условие возвращает значение True, даже если не выполняется одно из требований. По этой при- чине цикл повторяет предложение игроку снова и снова, до тех пор, пока не будет сделан допустимый ход. В финале код в строке 68 возвращает целочисленное значение любой введенной строки. Функция input() возвращает строку, затем функция int() преобразует строку в целое. 180 Глава 10 Вычисление по короткой схеме Вы могли заметить, что при вызове функции getPlayerMove() возможны проблемы. Что произойдет, если игрок введет, например 'Z', или еще что- нибудь, вместо цифры? Выражение move not in '1 2 3 4 5 6 7 8 9'.split() в левой части оператора or вернет False, а затем Python перейдет к обработке выражения в правой части оператора or. Но вызов функции int('Z') приведет к возникновению ошибки, так как эта функция может обрабатывать только строки типа '9' или '0', но не такие строки как 'Z'. Чтобы увидеть пример такой ошибки, наберите в интерактивной оболоч- ке следующий код: >>> int('42') 42 >>> int('Z') Traceback (most recent call last): File " ", line 1, in int('Z') ValueError: invalid literal for int() with base 10: 'Z' Однако, если играя в «Крестики-нолики», вы введете 'Z' в качестве свое- го хода, этой ошибки не возникнет. Все потому, что условие цикла while вы- числяется по короткой схеме. Принцип заключается в том, что вычисляется только одна часть выраже- ния, а оставшаяся часть не влияет на полученный результат. Ниже приведен краткий, но хороший пример вычисления по короткой схеме. Наберите сле- дующий код: >>> def ReturnsTrue(): print('Была вызвана функция ReturnsTrue().') return True >>> def ReturnsFalse(): print('Была вызвана функция ReturnsFalse().') return False >>> ReturnsTrue() Была вызвана функция ReturnsTrue(). True >>> ReturnsFalse() Была вызвана функция ReturnsFalse(). False Игра «Крестики-нолики» 181 Когда вызывается функция ReturnsTrue(), на экран выводится текст 'Была вы- звана функция ReturnsTrue().' , а затем выводится значение, возвращаемое функ- цией ReturnsTrue(). То же самое происходит при вызове функции ReturnsFalse(). Теперь В интерактивной оболочке введите следующие команды : >>> ReturnsFalse() or ReturnsTrue() Была вызвана функция ReturnsFalse(). Была вызвана функция ReturnsTrue(). True >>> ReturnsTrue() or ReturnsFalse() Была вызвана функция ReturnsTrue(). True В первой части происходит следующее: выражение ReturnsFalse() or ReturnsTrue() вызывает обе функции, поэтому выводятся оба сообщения. А вот вторая часть выводит только сообщение 'Была вызвана функция ReturnsTrue().' и не выводит 'Была вызвана функция ReturnsFalse().'. Так про- исходит потому, что Python вообще не вызывает функцию ReturnsFalse(). Так как левая часть оператора or истинна, уже не имеет значения, что вернет функция ReturnsFalse() в правой части, поэтому Python даже не заботится о ее вызове. Это преобразование и есть вычисление по короткой схеме. То же самое применимо и к оператору and. Наберите следующий код: >>> ReturnsTrue() and ReturnsTrue() Была вызвана функция ReturnsTrue(). Была вызвана функция ReturnsTrue(). True >>> ReturnsFalse() and ReturnsFalse() Была вызвана функция ReturnsFalse(). False Если левая часть оператора and ложна, то и все выражение ложно. И уже не имеет значения, какое значение вернет правая часть, — True или False, так как Python просто не будет его определять. Выражения False and True и False and False ложны и представляют собой вычисления по короткой схеме в языке Python. Вернемся к строкам 65–68 игры «Крестики-нолики». 65. while move not in '1 2 3 4 5 6 7 8 9'.split() or not isSpaceFree(board, int(move)): 66. print('Ваш следующий ход? (1-9)') 67. move = input() 68. return int(move) 182 Глава 10 Так как левая часть оператора or (move not in '1 2 3 4 5 6 7 8 9'.split()) истинна, то все выражение истинно. Не важно, какое именно значение вернет правая часть, — True или False, так как достаточно, чтобы одна сторона опе- ратора or была истинной для истинности всего выражения. Таким образом, Python прекращает дальнейшую проверку и даже не вы- числяет значение выражения not isSpaceFree(board, int(move)). Это значит, что функции int() и isSpaceFree() не будут вызваны до тех пор, пока выраже- ние move not in '1 2 3 4 5 6 7 8 9'.split() будет оставаться истинным. Это хорошо применимо к нашей программе, потому что если левая часть оператора истинна, значит значение move — это не строка с одной цифрой. И значит, она стала бы причиной ошибки при вызове функции int(). Но если выражение move not in '1 2 3 4 5 6 7 8 9'.split() истинно, интерпретатор, проводя вычисление по короткой схеме, не определяет значение выражения not isSpaceFree(board, int(move)) и не вызывает int(move). Выбор хода из списка Рассмотрим функцию chooseRandomMoveFromList(), которая пригодится нам далее, в программе ИИ. 70. def chooseRandomMoveFromList(board, movesList): 71. # Возвращает допустимый ход, учитывая список сделанных ходов и список заполненных клеток. 72. # Возвращает значение None, если больше нет допустимых ходов. 73. possibleMoves = [] 74. for i in movesList: 75. if isSpaceFree(board, i): 76. possibleMoves.append(i) Напомню, что переменная board — это список строк, в которых представ- лено игровое поле игры «Крестики-нолики». Вторая переменная, movesList, — это список целых чисел, из которого выбираются свободные клетки для со- вершения хода. Например, если переменная movesList имеет значение [1, 3, 7, 9] , значит функция chooseRandomMoveFromList() должна вернуть одно из зна- чений угловых клеток. Но сначала функция chooseRandomMoveFromList() проверит, что клетки до- ступны для совершения хода. Список possibleMoves создается пустым. Затем в цикле for производится перебор значений списка movesList. Те значения, для которых функция isSpaceFree() возвращает True, добавляются в список possibleMoves с помощью метода append(). Игра «Крестики-нолики» 183 На этом этапе список possibleMoves содержит все клетки, представленные в movesList, как пустые. Затем программа проверяет, пуст ли список. 78. if len(possibleMoves) != 0: 79. return random.choice(possibleMoves) 80. else: 81. return None Если список не пуст, то есть хотя бы один возможный ход, который мож- но сделать на поле. Но этот список может быть и пустым. Например, если переменная movesList имеет значение [1, 3, 7, 9], но все угловые клетки игрового поля, представленные в переменной board, уже заняты, список possibleMoves будет равен []. В этом случае len(possibleMoves) принимает значение 0, и функция возвращает значение None. |