七十六、井字棋
井字棋是一种在3 × 3
网格上玩的经典纸笔游戏。玩家轮流放置 X 或 O 标记,试图连续获得三个。大多数井字棋都以平局告终,但如果你的对手不小心,你也有可能智胜他们。
运行示例
当您运行tictactoe.py
时,输出将如下所示:
Welcome to Tic-Tac-Toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X's move? (1-9) > 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O's move? (1-9) `--snip--` X|O|X 1 2 3 -+-+- X|O|O 4 5 6 -+-+- O|X|X 7 8 9 The game is a tie! Thanks for playing!
工作原理
为了在这个程序中表示井字棋棋盘,我们使用了一个字典,用键'1'
到'9'
来表示棋盘上的空格。数字空间的排列方式与手机键盘相同。本词典中的值是代表球员标记的字符串'X'
或'O'
和代表空白的' '
。
"""Tic-Tac-Toe, by Al Sweigart email@protected The classic board game. This code is available at https://nostarch.com/big-book-small-python-programming Tags: short, board game, game, two-player""" ALL_SPACES = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): print('Welcome to Tic-Tac-Toe!') gameBoard = getBlankBoard() # Create a TTT board dictionary. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: # Main game loop. # Display the board on the screen: print(getBoardStr(gameBoard)) # Keep asking the player until they enter a number 1-9: move = None while not isValidSpace(gameBoard, move): print('What is {}\'s move? (1-9)'.format(currentPlayer)) move = input('> ') updateBoard(gameBoard, move, currentPlayer) # Make the move. # Check if the game is over: if isWinner(gameBoard, currentPlayer): # Check for a winner. print(getBoardStr(gameBoard)) print(currentPlayer + ' has won the game!') break elif isBoardFull(gameBoard): # Check for a tie. print(getBoardStr(gameBoard)) print('The game is a tie!') break # Switch turns to the next player: currentPlayer, nextPlayer = nextPlayer, currentPlayer print('Thanks for playing!') def getBlankBoard(): """Create a new, blank tic-tac-toe board.""" # Map of space numbers: 1|2|3 # -+-+- # 4|5|6 # -+-+- # 7|8|9 # Keys are 1 through 9, the values are X, O, or BLANK: board = {} for space in ALL_SPACES: board[space] = BLANK # All spaces start as blank. return board def getBoardStr(board): """Return a text-representation of the board.""" return ''' {}|{}|{} 1 2 3 -+-+- {}|{}|{} 4 5 6 -+-+- {}|{}|{} 7 8 9'''.format(board['1'], board['2'], board['3'], board['4'], board['5'], board['6'], board['7'], board['8'], board['9']) def isValidSpace(board, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and board[space] == BLANK def isWinner(board, player): """Return True if player is a winner on this TTTBoard.""" # Shorter variable names used here for readablility: b, p = board, player # Check for 3 marks across 3 rows, 3 columns, and 2 diagonals. return ((b['1'] == b['2'] == b['3'] == p) or # Across top (b['4'] == b['5'] == b['6'] == p) or # Across middle (b['7'] == b['8'] == b['9'] == p) or # Across bottom (b['1'] == b['4'] == b['7'] == p) or # Down left (b['2'] == b['5'] == b['8'] == p) or # Down middle (b['3'] == b['6'] == b['9'] == p) or # Down right (b['3'] == b['5'] == b['7'] == p) or # Diagonal (b['1'] == b['5'] == b['9'] == p)) # Diagonal def isBoardFull(board): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if board[space] == BLANK: return False # If any space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(board, space, mark): """Sets the space on the board to mark.""" board[space] = mark if __name__ == '__main__': main() # Call main() if this module is run, but not when imported.
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 7 行的
X, O, BLANK = 'X', 'O', ' '
改成X, O, BLANK = 'X', 'X', ' '
会怎么样? - 如果把第 95 行的
board[space] = mark
改成board[space] = X
会怎么样? - 如果把第 50 行的
board[space] = BLANK
改成board[space] = X
会怎么样?
七十七、汉诺塔
汉诺塔是一款移动堆叠的益智游戏,有三根柱子,你可以在上面堆叠不同大小的圆盘。游戏的目标是将一个塔盘移到另一个柱子上。但是,一次只能移动一个磁盘,较大的磁盘不能放在较小的磁盘上。找出某种模式将有助于你解决这个难题。你能发现它吗?(提示:尝试将TOTAL_DISKS
变量设置为3
或4
,以先解决一个更简单的版本。)
运行示例
当您运行towerofhanoi.py
时,输出将如下所示:
The Tower of Hanoi, by Al Sweigart email@protected 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 || || || @email@protected || || @@email@protected@ || || @@@email@protected@@ || || @@@@email@protected@@@ || || @@@@@email@protected@@@@ || || A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g. AB to moves a disk from tower A to tower B.) > ab || || || || || || @@email@protected@ || || @@@email@protected@@ || || @@@@email@protected@@@ || || @@@@@email@protected@@@@ @email@protected || A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g. AB to moves a disk from tower A to tower B.) `--snip--`
工作原理
表示塔的数据结构是一个整数列表。每个整数都是磁盘的大小。列表中的第一个整数代表底部磁盘,最后一个整数代表顶部磁盘。例如,[5, 4, 2]
将代表以下塔:
|| || @@email@protected@ @@@@email@protected@@@ @@@@@email@protected@@@@
Python 的append()
和pop()
列表方法可以分别在列表末尾添加和删除值。正如someList[0]
和someList[1]
允许我们访问列表中的第一个和第二个值一样,Python 允许我们使用负索引来访问列表末尾的值,使用像someList[-1]
和someList[-2]
这样的表达式,它们分别访问列表中的最后一个和倒数第二个值。这对于查找当前位于塔顶的磁盘非常有用。
"""The Tower of Hanoi, by Al Sweigart email@protected A stack-moving puzzle game. This code is available at https://nostarch.com/big-book-small-python-programming Tags: short, game, puzzle""" import copy import sys TOTAL_DISKS = 5 # More disks means a more difficult puzzle. # Start with all disks on tower A: COMPLETE_TOWER = list(range(TOTAL_DISKS, 0, -1)) def main(): print("""The Tower of Hanoi, by Al Sweigart email@protected 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 """ ) # Set up the towers. The end of the list is the top of the tower. towers = {'A': copy.copy(COMPLETE_TOWER), 'B': [], 'C': []} while True: # Run a single turn. # Display the towers and disks: displayTowers(towers) # Ask the user for a move: fromTower, toTower = askForPlayerMove(towers) # Move the top disk from fromTower to toTower: disk = towers[fromTower].pop() towers[toTower].append(disk) # Check if the user has solved the puzzle: if COMPLETE_TOWER in (towers['B'], towers['C']): displayTowers(towers) # Display the towers one last time. print('You have solved the puzzle! Well done!') sys.exit() def askForPlayerMove(towers): """Asks the player for a move. Returns (fromTower, toTower).""" while True: # Keep asking player until they enter a valid move. 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.)') response = input('> ').upper().strip() if response == 'QUIT': print('Thanks for playing!') sys.exit() # Make sure the user entered valid tower letters: if response not in ('AB', 'AC', 'BA', 'BC', 'CA', 'CB'): print('Enter one of AB, AC, BA, BC, CA, or CB.') continue # Ask player again for their move. # Syntactic sugar - Use more descriptive variable names: fromTower, toTower = response[0], response[1] if len(towers[fromTower]) == 0: # The "from" tower cannot be an empty tower: print('You selected a tower with no disks.') continue # Ask player again for their move. elif len(towers[toTower]) == 0: # Any disk can be moved onto an empty "to" tower: return fromTower, toTower elif towers[toTower][-1] < towers[fromTower][-1]: print('Can\'t put larger disks on top of smaller ones.') continue # Ask player again for their move. else: # This is a valid move, so return the selected towers: return fromTower, toTower def displayTowers(towers): """Display the current state.""" # Display the three towers: for level in range(TOTAL_DISKS, -1, -1): for tower in (towers['A'], towers['B'], towers['C']): if level >= len(tower): displayDisk(0) # Display the bare pole with no disk. else: displayDisk(tower[level]) # Display the disk. print() # Display the tower labels A, B, and C. emptySpace = ' ' * (TOTAL_DISKS) print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace)) def displayDisk(width): """Display a disk of the given width. A width of 0 means no disk.""" emptySpace = ' ' * (TOTAL_DISKS - width) if width == 0: # Display a pole segment without a disk: print(emptySpace + '||' + emptySpace, end='') else: # Display the disk: disk = '@' * width numLabel = str(width).rjust(2, '_') print(emptySpace + disk + numLabel + disk + emptySpace, end='') # If the program is run (instead of imported), run the game: if __name__ == '__main__': main()
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果删除或注释掉第 73、74 和 75 行会发生什么?
- 如果把第 100 行的
emptySpace = ' ' * (TOTAL_DISKS - width)
改成emptySpace = ' '
会怎么样? - 如果把 102 行的
width == 0
改成width != 0
会怎么样?
七十八、脑筋急转弯
一块黄色的石头扔进蓝色的池塘会变成什么?英国有 7 月 4 日吗?医生怎么能 30 天不睡觉?不管你认为这些问题的答案是什么,你可能都错了。这个项目中的 54 个问题都是经过精心设计的,因此它们的答案简单、明显且容易误导人。找到真正的答案需要一些小聪明。
复制这本书的代码会破坏乐趣,因为你会看到答案,所以你可能想在看源代码之前下载并玩这个来自inventwithpython.com/trickquestions.py
的游戏。
运行示例
当您运行trickquestions.py
时,输出将如下所示:
Trick Questions, by Al Sweigart email@protected Can you figure out the answers to these trick questions? (Enter QUIT to quit at any time.) Press Enter to begin... `--snip--` Question: 1 Score: 0 / 54 QUESTION: A 39 year old person was born on the 22nd of February. What year is their birthday? ANSWER: 1981 Incorrect! The answer is: Their birthday is on February 22nd of every year. Press Enter for the next question... `--snip--` Question: 2 Score: 0 / 54 QUESTION: If there are ten apples and you take away two, how many do you have? ANSWER: Eight Incorrect! The answer is: Two. Press Enter for the next question... `--snip--`
工作原理
QUESTIONS
变量保存一个字典列表。每个字典代表一个单独的难题,并且有关键字'question'
、'answer'
和'accept'
。'question'
和'answer'
的值分别是程序向玩家提出问题并给出答案时显示的字符串。'accept'
键的值是一个字符串列表。如果玩家输入包含任何这些字符串的响应,它将被认为是正确的。这允许玩家输入自由格式的文本作为回复。该程序在检测他们何时提供了正确答案方面相当准确。
"""Trick Questions, by Al Sweigart email@protected A quiz of several trick questions. This code is available at https://nostarch.com/big-book-small-python-programming Tags: large, humor""" import random, sys # QUESTIONS is a list of dictionaries, each dictionary represents a # trick question and its answer. The dictionary has the keys 'question' # (which holds the text of the question), 'answer' (which holds the text # of the answer), and 'accept' (which holds a list of strings that, if # the player's answer contains any of, they've answered correctly). # (!) Try coming up with your own trick questions to add here: QUESTIONS = [ {'question': "How many times can you take 2 apples from a pile of 10 apples?", 'answer': "Once. Then you have a pile of 8 apples.", 'accept': ['once', 'one', '1']}, {'question': 'What begins with "e" and ends with "e" but only has one letter in it?', 'answer': "An envelope.", 'accept': ['envelope']}, {'question': "Is it possible to draw a square with three sides?", 'answer': "Yes. All squares have three sides. They also have a fourth side.", 'accept': ['yes']}, {'question': "How many times can a piece of paper be folded in half by hand without unfolding?", 'answer': "Once. Then you are folding it in quarters.", 'accept': ['one', '1', 'once']}, {'question': "What does a towel get as it dries?", 'answer': "Wet.", 'accept': ['wet']}, {'question': "What does a towel get as it dries?", 'answer': "Drier.", 'accept': ['drier', 'dry']}, {'question': "Imagine you are in a haunted house full of evil ghosts. What do you have to do to stay safe?", 'answer': "Nothing. You're only imagining it.", 'accept': ['nothing', 'stop']}, {'question': "A taxi driver is going the wrong way down a one-way street. She passes ten cops but doesn't get a ticket. Why not?", 'answer': "She was walking.", 'accept': ['walk']}, {'question': "What does a yellow stone thrown into a blue pond become?", 'answer': "Wet.", 'accept': ['wet']}, {'question': "How many miles does must a cyclist bike to get to training?", 'answer': "None. They're training as soon as they get on the bike.", 'accept': ['none', 'zero', '0']}, {'question': "What building do people want to leave as soon as they enter?", 'answer': "An airport.", 'accept': ['airport', 'bus', 'port', 'train', 'station', 'stop']}, {'question': "If you're in the middle of a square house facing the west side with the south side to your left and the north side to your right, which side of the house are you next to?", 'answer': "None. You're in the middle.", 'accept': ['none', 'middle', 'not', 'any']}, {'question': "How much dirt is in a hole 3 meters wide, 3 meters long, and 3 meters deep?", 'answer': "There is no dirt in a hole.", 'accept': ['no', 'none', 'zero']}, {'question': "A girl mails a letter from America to Japan. How many miles did the stamp move?", 'answer': "Zero. The stamp was in the same place on the envelope the whole time.", 'accept': ['zero', '0', 'none', 'no']}, {'question': "What was the highest mountain on Earth the day before Mount Everest was discovered?", 'answer': "Mount Everest was still the highest mountain of Earth the day before it was discovered.", 'accept': ['everest']}, {'question': "How many fingers do most people have on their two hands?", 'answer': "Eight. They also have two thumbs.", 'accept': ['eight', '8']}, {'question': "The 4th of July is a holiday in America. Do they have a 4th of July in England?", 'answer': "Yes. All countries have a 4th of July on their calendar.", 'accept': ['yes']}, {'question': "Which letter of the alphabet makes honey?", 'answer': "None. A bee is an insect, not a letter.", 'accept': ['no', 'none', 'not']}, {'question': "How can a doctor go 30 days without sleep?", 'answer': "By sleeping at night.", 'accept': ['night', 'evening']}, {'question': "How many months have 28 days?", 'answer': "12\. All months have 28 days. Some have more days as well.", 'accept': ['12', 'twelve', 'all']}, {'question': "How many two cent stamps are in a dozen?", 'answer': "A dozen.", 'accept': ['12', 'twelve', 'dozen']}, {'question': "Why is it illegal for a person living in North Dakota to be buried in South Dakota?", 'answer': "Because it is illegal to bury someone alive.", 'accept': ['alive', 'living', 'live']}, {'question': "How many heads does a two-headed coin have?", 'answer': "Zero. Coins are just circular pieces of metal. They don't have heads.", 'accept': ['zero', 'none', 'no', '0']}, {'question': "What kind of vehicle has four wheels and flies?", 'answer': "A garbage truck.", 'accept': ['garbage', 'dump', 'trash']}, {'question': "What kind of vehicle has four wheels and flies?", 'answer': "An airplane.", 'accept': ['airplane', 'plane']}, {'question': "What five-letter word becomes shorter by adding two letters?", 'answer': "Short.", 'accept': ['short']}, {'question': "Gwen's mother has five daughters. Four are named Haha, Hehe, Hihi, and Hoho. What's the fifth daughter's name?", 'answer': "Gwen.", 'accept': ['gwen']}, {'question': "How long is a fence if there are three fence posts each one meter apart?", 'answer': "Two meters long.", 'accept': ['2', 'two']}, {'question': "How many legs does a dog have if you count its tail as a leg?", 'answer': "Four. Calling a tail a leg doesn't make it one.", 'accept': ['four', '4']}, {'question': "How much more are 1976 pennies worth compared to 1975 pennies?", 'answer': "One cent.", 'accept': ['1', 'one']}, {'question': "What two things can you never eat for breakfast?", 'answer': "Lunch and dinner.", 'accept': ['lunch', 'dinner', 'supper']}, {'question': "How many birthdays does the average person have?", 'answer': "One. You're only born once.", 'accept': ['one', '1', 'once' 'born']}, {'question': "Where was the United States Declaration of Independence signed?", 'answer': "It was signed at the bottom.", 'accept': ['bottom']}, {'question': "A person puts two walnuts in their pocket but only has one thing in their pocket five minutes later. What is it?", 'answer': "A hole.", 'accept': ['hole']}, {'question': "What did the sculptor make that no one could see?", 'answer': "Noise.", 'accept': ['noise']}, {'question': "If you drop a raw egg on a concrete floor, will it crack?", 'answer': "No. Concrete is very hard to crack.", 'accept': ['no']}, {'question': "If it takes ten people ten hours to build a fence, how many hours does it take five people to build it?", 'answer': "Zero. It's already built.", 'accept': ['zero', 'no', '0', 'already', 'built']}, {'question': "Which is heavier, 100 pounds of rocks or 100 pounds of feathers?", 'answer': "Neither. They weigh the same.", 'accept': ['neither', 'none', 'no', 'same', 'even', 'balance']}, {'question': "What do you have to do to survive being bitten by a poisonous snake?", 'answer': "Nothing. Only venomous snakes are deadly.", 'accept': ['nothing', 'anything']}, {'question': "What three consecutive days don't include Sunday, Wednesday, or Friday?", 'answer': "Yesterday, today, and tomorrow.", 'accept': ['yesterday', 'today', 'tomorrow']}, {'question': "If there are ten apples and you take away two, how many do you have?", 'answer': "Two.", 'accept': ['2', 'two']}, {'question': "A 39 year old person was born on the 22nd of February. What year is their birthday?", 'answer': "Their birthday is on February 22nd of every year.", 'accept': ['every', 'each']}, {'question': "How far can you walk in the woods?", 'answer': "Halfway. Then you are walking out of the woods.", 'accept': ['half', '1/2']}, {'question': "Can a man marry his widow's sister?", 'answer': "No, because he's dead.", 'accept': ['no']}, {'question': "What do you get if you divide one hundred by half?", 'answer': "One hundred divided by half is two hundred. One hundred divided by two is fifty.", 'accept': ['two', '200']}, {'question': "What do you call someone who always knows where their spouse is?", 'answer': "A widow or widower.", 'accept': ['widow', 'widower']}, {'question': "How can someone take a photo but not be a photographer?", 'answer': "They can be a thief.", 'accept': ['thief', 'steal', 'take', 'literal']}, {'question': "An electric train leaves the windy city of Chicago at 4pm on a Monday heading south at 100 kilometers per hour. Which way does the smoke blow from the smokestack?", 'answer': "Electric trains don't have smokestacks.", 'accept': ["don't", "doesn't", 'not', 'no', 'none']}, {'question': 'What is the only word that rhymes with "orange"?', 'answer': "Orange.", 'accept': ['orange']}, {'question': "Who is the U.S. President if the U.S. Vice President dies?", 'answer': "The current U.S. President.", 'accept': ['president', 'current', 'already']}, {'question': "A doctor gives you three pills with instructions to take one every half-hour. How long will the pills last?", 'answer': "One hour.", 'accept': ['1', 'one']}, {'question': "Where is there an ocean with no water?", 'answer': "On a map.", 'accept': ['map']}, {'question': "What is the size of a rhino but weighs nothing?", 'answer': "A rhino's shadow.", 'accept': ['shadow']}, {'question': "The clerk at a butcher shop is exactly 177 centimeters tall. What do they weigh?", 'answer': "The clerk weighs meat.", 'accept': ['meat']}] CORRECT_TEXT = ['Correct!', 'That is right.', "You're right.", 'You got it.', 'Righto!'] INCORRECT_TEXT = ['Incorrect!', "Nope, that isn't it.", 'Nope.', 'Not quite.', 'You missed it.'] print('''Trick Questions, by Al Sweigart email@protected Can you figure out the answers to these trick questions? (Enter QUIT to quit at any time.) ''') input('Press Enter to begin...') random.shuffle(QUESTIONS) score = 0 for questionNumber, qa in enumerate(QUESTIONS): # Main program loop. print('\n' * 40) # "Clear" the screen. print('Question:', questionNumber + 1) print('Score:', score, '/', len(QUESTIONS)) print('QUESTION:', qa['question']) response = input(' ANSWER: ').lower() if response == 'quit': print('Thanks for playing!') sys.exit() correct = False for acceptanceWord in qa['accept']: if acceptanceWord in response: correct = True if correct: text = random.choice(CORRECT_TEXT) print(text, qa['answer']) score += 1 else: text = random.choice(INCORRECT_TEXT) print(text, 'The answer is:', qa['answer']) response = input('Press Enter for the next question...').lower() if response == 'quit': print('Thanks for playing!') sys.exit() print("That's all the questions. Thanks for playing!")
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。
探索程序
这是一个基础程序,所以没有太多的选项来定制它。相反,考虑一下问答节目的其他用途。
七十九、2048
网络开发者 Gabriele Cirulli 在一个周末就发明了游戏 2048。它的灵感来自于 Veewo 工作室的 1024 游戏,而这个游戏的灵感又来自于 Threes!,开发团队 Sirvo 的一款游戏。2048,你必须在一个4 × 4
的板上合并数字,才能把它们从屏幕上清除。两个 2 合并成一个 4,两个 4 合并成一个 8,以此类推。每次合并时,游戏都会在棋盘上添加一个新的 2。目标是在整个董事会填满之前达到 2048。
运行示例
当您运行twentyfortyeight.py
时,输出将如下所示:
Twenty Forty-Eight, by Al Sweigart email@protected `--snip--` +-----+-----+-----+-----+ | | | | | | | | 2 | 16 | | | | | | +-----+-----+-----+-----+ | | | | | | | 16 | 4 | 2 | | | | | | +-----+-----+-----+-----+ | | | | | | 2 | | 4 | 32 | | | | | | +-----+-----+-----+-----+ | | | | | | | | | 2 | | | | | | +-----+-----+-----+-----+ Score: 80 Enter move: (WASD or Q to quit) `--snip--`
工作原理
这个程序使用“列”数据结构实现它的滑动行为,由四个字符串列表表示:BLANK
(一个单空格字符串)、'2'
、'4'
、'8'
等等。该列表中的第一个值表示列的底部,而最后一个值表示列的顶部。无论玩家向上、向下、向左或向右滑动牌,组合在一列中的数字总是向下滑动。想象一下重力将瓷砖拉向这些方向。例如,图 79-1 显示了一块向右滑动的棋盘。我们将创建四个列表来表示列:
['2', '4', '8', ' ']
[' ', ' ', ' ', '4']
[' ', ' ', ' ', '2']
[' ', ' ', ' ', ' ']
combineTilesInColumn()
函数接受一个列列表并返回另一个列列表,匹配的数字被组合并向底部移动。调用combineTilesInColumn()
的代码负责在适当的方向创建列列表,并用返回的列表更新游戏板。
:游戏板向右滑动时的列(高亮显示)
"""Twenty Forty-Eight, by Al Sweigart email@protected A sliding tile game to combine exponentially-increasing numbers. Inspired by Gabriele Cirulli's 2048, which is a clone of Veewo Studios' 1024, which in turn is a clone of the Threes! game. More info at https://en.wikipedia.org/wiki/2048_(video_game) This code is available at https://nostarch.com/big-book-small-python-programming Tags: large, game, puzzle""" import random, sys # Set up the constants: BLANK = '' # A value that represents a blank space on the board. def main(): print('''Twenty Forty-Eight, by Al Sweigart email@protected Slide all the tiles on the board in one of four directions. Tiles with like numbers will combine into larger-numbered tiles. A new 2 tile is added to the board on each move. You win if you can create a 2048 tile. You lose if the board fills up the tiles before then.''') input('Press Enter to begin...') gameBoard = getNewBoard() while True: # Main game loop. drawBoard(gameBoard) print('Score:', getScore(gameBoard)) playerMove = askForPlayerMove() gameBoard = makeMove(gameBoard, playerMove) addTwoToBoard(gameBoard) if isFull(gameBoard): drawBoard(gameBoard) print('Game Over - Thanks for playing!') sys.exit() def getNewBoard(): """Returns a new data structure that represents a board. It's a dictionary with keys of (x, y) tuples and values of the tile at that space. The tile is either a power-of-two integer or BLANK. The coordinates are laid out as: X0 1 2 3 Y+-+-+-+-+ 0| | | | | +-+-+-+-+ 1| | | | | +-+-+-+-+ 2| | | | | +-+-+-+-+ 3| | | | | +-+-+-+-+""" newBoard = {} # Contains the board data structure to be returned. # Loop over every possible space and set all the tiles to blank: for x in range(4): for y in range(4): newBoard[(x, y)] = BLANK # Pick two random spaces for the two starting 2's: startingTwosPlaced = 0 # The number of starting spaces picked. while startingTwosPlaced < 2: # Repeat for duplicate spaces. randomSpace = (random.randint(0, 3), random.randint(0, 3)) # Make sure the randomly selected space isn't already taken: if newBoard[randomSpace] == BLANK: newBoard[randomSpace] = 2 startingTwosPlaced = startingTwosPlaced + 1 return newBoard def drawBoard(board): """Draws the board data structure on the screen.""" # Go through each possible space left to right, top to bottom, and # create a list of what each space's label should be. labels = [] # A list of strings for the number/blank for that tile. for y in range(4): for x in range(4): tile = board[(x, y)] # Get the tile at this space. # Make sure the label is 5 spaces long: labelForThisTile = str(tile).center(5) labels.append(labelForThisTile) # The {} are replaced with the label for that tile: print(""" +-----+-----+-----+-----+ | | | | | |{}|{}|{}|{}| | | | | | +-----+-----+-----+-----+ | | | | | |{}|{}|{}|{}| | | | | | +-----+-----+-----+-----+ | | | | | |{}|{}|{}|{}| | | | | | +-----+-----+-----+-----+ | | | | | |{}|{}|{}|{}| | | | | | +-----+-----+-----+-----+ """.format(*labels)) def getScore(board): """Returns the sum of all the tiles on the board data structure.""" score = 0 # Loop over every space and add the tile to the score: for x in range(4): for y in range(4): # Only add non-blank tiles to the score: if board[(x, y)] != BLANK: score = score + board[(x, y)] return score def combineTilesInColumn(column): """The column is a list of four tile. Index 0 is the "bottom" of the column, and tiles are pulled "down" and combine if they are the same. For example, combineTilesInColumn([2, BLANK, 2, BLANK]) returns [4, BLANK, BLANK, BLANK].""" # Copy only the numbers (not blanks) from column to combinedTiles combinedTiles = [] # A list of the non-blank tiles in column. for i in range(4): if column[i] != BLANK: combinedTiles.append(column[i]) # Keep adding blanks until there are 4 tiles: while len(combinedTiles) < 4: combinedTiles.append(BLANK) # Combine numbers if the one "above" it is the same, and double it. for i in range(3): # Skip index 3: it's the topmost space. if combinedTiles[i] == combinedTiles[i + 1]: combinedTiles[i] *= 2 # Double the number in the tile. # Move the tiles above it down one space: for aboveIndex in range(i + 1, 3): combinedTiles[aboveIndex] = combinedTiles[aboveIndex + 1] combinedTiles[3] = BLANK # Topmost space is always BLANK. return combinedTiles def makeMove(board, move): """Carries out the move on the board. The move argument is either 'W', 'A', 'S', or 'D' and the function returns the resulting board data structure.""" # The board is split up into four columns, which are different # depending on the direction of the move: if move == 'W': allColumnsSpaces = [[(0, 0), (0, 1), (0, 2), (0, 3)], [(1, 0), (1, 1), (1, 2), (1, 3)], [(2, 0), (2, 1), (2, 2), (2, 3)], [(3, 0), (3, 1), (3, 2), (3, 3)]] elif move == 'A': allColumnsSpaces = [[(0, 0), (1, 0), (2, 0), (3, 0)], [(0, 1), (1, 1), (2, 1), (3, 1)], [(0, 2), (1, 2), (2, 2), (3, 2)], [(0, 3), (1, 3), (2, 3), (3, 3)]] elif move == 'S': allColumnsSpaces = [[(0, 3), (0, 2), (0, 1), (0, 0)], [(1, 3), (1, 2), (1, 1), (1, 0)], [(2, 3), (2, 2), (2, 1), (2, 0)], [(3, 3), (3, 2), (3, 1), (3, 0)]] elif move == 'D': allColumnsSpaces = [[(3, 0), (2, 0), (1, 0), (0, 0)], [(3, 1), (2, 1), (1, 1), (0, 1)], [(3, 2), (2, 2), (1, 2), (0, 2)], [(3, 3), (2, 3), (1, 3), (0, 3)]] # The board data structure after making the move: boardAfterMove = {} for columnSpaces in allColumnsSpaces: # Loop over all 4 columns. # Get the tiles of this column (The first tile is the "bottom" # of the column): firstTileSpace = columnSpaces[0] secondTileSpace = columnSpaces[1] thirdTileSpace = columnSpaces[2] fourthTileSpace = columnSpaces[3] firstTile = board[firstTileSpace] secondTile = board[secondTileSpace] thirdTile = board[thirdTileSpace] fourthTile = board[fourthTileSpace] # Form the column and combine the tiles in it: column = [firstTile, secondTile, thirdTile, fourthTile] combinedTilesColumn = combineTilesInColumn(column) # Set up the new board data structure with the combined tiles: boardAfterMove[firstTileSpace] = combinedTilesColumn[0] boardAfterMove[secondTileSpace] = combinedTilesColumn[1] boardAfterMove[thirdTileSpace] = combinedTilesColumn[2] boardAfterMove[fourthTileSpace] = combinedTilesColumn[3] return boardAfterMove def askForPlayerMove(): """Asks the player for the direction of their next move (or quit). Ensures they enter a valid move: either 'W', 'A', 'S' or 'D'.""" print('Enter move: (WASD or Q to quit)') while True: # Keep looping until they enter a valid move. move = input('> ').upper() if move == 'Q': # End the program: print('Thanks for playing!') sys.exit() # Either return the valid move, or loop back and ask again: if move in ('W', 'A', 'S', 'D'): return move else: print('Enter one of "W", "A", "S", "D", or "Q".') def addTwoToBoard(board): """Adds a new 2 tile randomly to the board.""" while True: randomSpace = (random.randint(0, 3), random.randint(0, 3)) if board[randomSpace] == BLANK: board[randomSpace] = 2 return # Return after finding one non-blank tile. def isFull(board): """Returns True if the board data structure has no blanks.""" # Loop over every space on the board: for x in range(4): for y in range(4): # If a space is blank, return False: if board[(x, y)] == BLANK: return False return True # No space is blank, so return True. # If this program was run (instead of imported), run the game: if __name__ == '__main__': try: main() except KeyboardInterrupt: sys.exit() # When Ctrl-C is pressed, end the program.
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 118 行的
return score
改成return 9999
会怎么样? - 如果把 229 行的
board[randomSpace] = 2
改成board[randomSpace] = 256
会怎么样?
八十、维吉尼亚密码
被误认为是 19 世纪密码学家布莱斯·德·维吉尼亚(其他人早些时候独立发明了它)的维吉尼亚密码在数百年内都不可能被破解。它本质上是凯撒密码,除了它使用了多部分密钥。所谓的维吉尼亚密钥就是一个单词,甚至是一串随机的字母。每个字母代表一个数字,该数字代表消息中的字母移动: A
代表消息中的字母移动 0,B
代表 1,C
代表 2,依此类推。
例如,如果一个维吉尼亚密钥是单词CAT
,则C
代表移位 2,A
代表 0,T
代表 19。消息的第一个字母移动 2,第二个字母移动 0,第三个字母移动 19。对于第四个字母,我们重复 2 的密钥。
这种多重凯撒密钥的使用赋予了维吉尼亚密码的力量。可能的组合数太大,无法暴力破解。同时,维吉尼亚密码不存在可以破解简单替换密码的频率分析弱点。几个世纪以来,维吉尼亚密码代表了密码学的最高水平。
你会注意到维吉尼亚和凯撒密码程序的代码之间有许多相似之处。更多关于维吉尼亚密码的信息可以在en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
找到。如果你想了解更多关于密码和密码破解的知识,你可以阅读我的书《Python 密码破解指南》(NoStarch 出版社,2018)。
运行示例
当您运行vigenere.py
时,输出将如下所示:
Vigenère Cipher, by Al Sweigart email@protected The Vigenère cipher is a polyalphabetic substitution cipher that was powerful enough to remain unbroken for centuries. Do you want to (e)ncrypt or (d)ecrypt? > e Please specify the key to use. It can be a word or any combination of letters: > PIZZA Enter the message to encrypt. > Meet me by the rose bushes tonight. Encrypted message: Bmds mt jx sht znre qcrgeh bnmivps. Full encrypted text copied to clipboard.
工作原理
因为加密和解密过程非常相似,translateMessage()
函数处理这两个过程。encryptMessage()
和decryptMessage()
函数仅仅是translateMessage()
的包装函数。换句话说,它们是调整参数的函数,将这些参数转发给另一个函数,然后返回该函数的返回值。这个程序使用这些包装函数,这样它们可以以类似于项目 66“简单替换密码”中的encryptMessage()
和decryptMessage()
的方式被调用您可以将这些项目作为模块导入到其他程序中,以利用它们的加密代码,而不必将代码直接复制并粘贴到您的新程序中。
"""Vigenère Cipher, by Al Sweigart email@protected The Vigenère cipher is a polyalphabetic substitution cipher that was powerful enough to remain unbroken for centuries. More info at: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher This code is available at https://nostarch.com/big-book-small-python-programming Tags: short, cryptography, math""" try: import pyperclip # pyperclip copies text to the clipboard. except ImportError: pass # If pyperclip is not installed, do nothing. It's no big deal. # Every possible symbol that can be encrypted/decrypted: LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' def main(): print('''Vigenère Cipher, by Al Sweigart email@protected The Viegenère cipher is a polyalphabetic substitution cipher that was powerful enough to remain unbroken for centuries.''') # Let the user specify if they are encrypting or decrypting: while True: # Keep asking until the user enters e or d. print('Do you want to (e)ncrypt or (d)ecrypt?') response = input('> ').lower() if response.startswith('e'): myMode = 'encrypt' break elif response.startswith('d'): myMode = 'decrypt' break print('Please enter the letter e or d.') # Let the user specify the key to use: while True: # Keep asking until the user enters a valid key. print('Please specify the key to use.') print('It can be a word or any combination of letters:') response = input('> ').upper() if response.isalpha(): myKey = response break # Let the user specify the message to encrypt/decrypt: print('Enter the message to {}.'.format(myMode)) myMessage = input('> ') # Perform the encryption/decryption: if myMode == 'encrypt': translated = encryptMessage(myMessage, myKey) elif myMode == 'decrypt': translated = decryptMessage(myMessage, myKey) print('%sed message:' % (myMode.title())) print(translated) try: pyperclip.copy(translated) print('Full %sed text copied to clipboard.' % (myMode)) except: pass # Do nothing if pyperclip wasn't installed. def encryptMessage(message, key): """Encrypt the message using the key.""" return translateMessage(message, key, 'encrypt') def decryptMessage(message, key): """Decrypt the message using the key.""" return translateMessage(message, key, 'decrypt') def translateMessage(message, key, mode): """Encrypt or decrypt the message using the key.""" translated = [] # Stores the encrypted/decrypted message string. keyIndex = 0 key = key.upper() for symbol in message: # Loop through each character in message. num = LETTERS.find(symbol.upper()) if num != -1: # -1 means symbol.upper() was not in LETTERS. if mode == 'encrypt': # Add if encrypting: num += LETTERS.find(key[keyIndex]) elif mode == 'decrypt': # Subtract if decrypting: num -= LETTERS.find(key[keyIndex]) num %= len(LETTERS) # Handle the potential wrap-around. # Add the encrypted/decrypted symbol to translated. if symbol.isupper(): translated.append(LETTERS[num]) elif symbol.islower(): translated.append(LETTERS[num].lower()) keyIndex += 1 # Move to the next letter in the key. if keyIndex == len(key): keyIndex = 0 else: # Just add the symbol without encrypting/decrypting: translated.append(symbol) return ''.join(translated) # If this program was run (instead of imported), run the program: if __name__ == '__main__': main()
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 用密钥
'A'
加密会怎么样? - 删除或注释掉第 40 行的
myKey = response
会导致什么错误?
八十一、水桶谜题
在这个纸牌益智游戏中,您必须使用三个水桶(三升、五升和八升的水桶)在其中一个水桶中收集正好四升水。桶只能被清空、完全装满或倒入另一个桶中。例如,你可以装满一个 5 升的桶,然后把里面的东西倒入 3 升的桶,这样你就有了一个满满的 3 升桶,5 升桶里有 2 升水。
经过一些努力,你应该能解决这个难题。但是你能想出如何用最少的步数解决它吗?
运行示例
当您运行waterbucket.py
时,输出将如下所示:
Water Bucket Puzzle, by Al Sweigart email@protected Try to get 4L of water into one of these buckets: 8| | 7| | 6| | 5| | 5| | 4| | 4| | 3| | 3| | 3| | 2| | 2| | 2| | 1| | 1| | 1| | +------+ +------+ +------+ 8L 5L 3L You can: (F)ill the bucket (E)mpty the bucket (P)our one bucket into another (Q)uit > f Select a bucket 8, 5, 3, or QUIT: > 5 Try to get 4L of water into one of these buckets: 8| | 7| | 6| | 5| | 5|WWWWWW| 4| | 4|WWWWWW| 3| | 3|WWWWWW| 3| | 2| | 2|WWWWWW| 2| | 1| | 1|WWWWWW| 1| | +------+ +------+ +------+ 8L 5L 3L `--snip--`
工作原理
waterInBucket
变量存储了一个代表水桶状态的字典。这个字典的关键字是字符串'8'
、'5'
和'3'
(代表水桶),它们的值是整数(代表水桶中的水的公升数)。
第 48 到 59 行使用这个字典在屏幕上呈现水桶和水。waterDisplay
列表包含'WWWWWW'
(代表水)或' '
(代表空气),并被传递给format()
字符串方法。waterDisplay
列表中的前八串填充八升桶,接下来的五串填充五升桶,最后的三串填充三升桶。
"""Water Bucket Puzzle, by Al Sweigart email@protected A water pouring puzzle. More info: https://en.wikipedia.org/wiki/Water_pouring_puzzle This code is available at https://nostarch.com/big-book-small-python-programming Tags: large, game, math, puzzle""" import sys print('Water Bucket Puzzle, by Al Sweigart email@protected') GOAL = 4 # The exact amount of water to have in a bucket to win. steps = 0 # Keep track of how many steps the player made to solve this. # The amount of water in each bucket: waterInBucket = {'8': 0, '5': 0, '3': 0} while True: # Main game loop. # Display the current state of the buckets: print() print('Try to get ' + str(GOAL) + 'L of water into one of these') print('buckets:') waterDisplay = [] # Contains strings for water or empty space. # Get the strings for the 8L bucket: for i in range(1, 9): if waterInBucket['8'] < i: waterDisplay.append(' ') # Add empty space. else: waterDisplay.append('WWWWWW') # Add water. # Get the strings for the 5L bucket: for i in range(1, 6): if waterInBucket['5'] < i: waterDisplay.append(' ') # Add empty space. else: waterDisplay.append('WWWWWW') # Add water. # Get the strings for the 3L bucket: for i in range(1, 4): if waterInBucket['3'] < i: waterDisplay.append(' ') # Add empty space. else: waterDisplay.append('WWWWWW') # Add water. # Display the buckets with the amount of water in each one: print(''' 8|{7}| 7|{6}| 6|{5}| 5|{4}| 5|{12}| 4|{3}| 4|{11}| 3|{2}| 3|{10}| 3|{15}| 2|{1}| 2|{9}| 2|{14}| 1|{0}| 1|{8}| 1|{13}| +------+ +------+ +------+ 8L 5L 3L '''.format(*waterDisplay)) # Check if any of the buckets has the goal amount of water: for waterAmount in waterInBucket.values(): if waterAmount == GOAL: print('Good job! You solved it in', steps, 'steps!') sys.exit() # Let the player select an action to do with a bucket: print('You can:') print(' (F)ill the bucket') print(' (E)mpty the bucket') print(' (P)our one bucket into another') print(' (Q)uit') while True: # Keep asking until the player enters a valid action. move = input('> ').upper() if move == 'QUIT' or move == 'Q': print('Thanks for playing!') sys.exit() if move in ('F', 'E', 'P'): break # Player has selected a valid action. print('Enter F, E, P, or Q') # Let the player select a bucket: while True: # Keep asking until valid bucket entered. print('Select a bucket 8, 5, 3, or QUIT:') srcBucket = input('> ').upper() if srcBucket == 'QUIT': print('Thanks for playing!') sys.exit() if srcBucket in ('8', '5', '3'): break # Player has selected a valid bucket. # Carry out the selected action: if move == 'F': # Set the amount of water to the max size. srcBucketSize = int(srcBucket) waterInBucket[srcBucket] = srcBucketSize steps += 1 elif move == 'E': waterInBucket[srcBucket] = 0 # Set water amount to nothing. steps += 1 elif move == 'P': # Let the player select a bucket to pour into: while True: # Keep asking until valid bucket entered. print('Select a bucket to pour into: 8, 5, or 3') dstBucket = input('> ').upper() if dstBucket in ('8', '5', '3'): break # Player has selected a valid bucket. # Figure out the amount to pour: dstBucketSize = int(dstBucket) emptySpaceInDstBucket = dstBucketSize - waterInBucket[dstBucket] waterInSrcBucket = waterInBucket[srcBucket] amountToPour = min(emptySpaceInDstBucket, waterInSrcBucket) # Pour out water from this bucket: waterInBucket[srcBucket] -= amountToPour # Put the poured out water into the other bucket: waterInBucket[dstBucket] += amountToPour steps += 1 elif move == 'C': pass # If the player selected Cancel, do nothing.
在输入源代码并运行几次之后,尝试对其进行实验性的修改。你也可以自己想办法做到以下几点:
- 通过使游戏可配置来增加多样性,这样你可以为三个桶指定任何大小,为目标数量指定任何数量。
- 添加“提示”,检查每个水桶中的水量,并提供下一步要采取的措施。如果程序不知道下一步该做什么,它可以简单地显示“我不知道你下一步该做什么。也许重新开始?”
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 104 行的
waterInBucket[srcBucket] = 0
改成waterInBucket[srcBucket] = 1
会怎么样? - 如果把第 16 行的
{'8': 0, '5': 0, '3': 0}
改成{'8': 0, '5': 4, '3': 0}
会怎么样? - 如果把第 16 行的
{'8': 0, '5': 0, '3': 0}
改成{'8': 9, '5': 0, '3': 0}
会怎么样?