二十六、斐波那契
斐波那契数列是一个著名的数学模式,被认为是 13 世纪意大利数学家斐波那契的杰作(尽管其他人发现它的时间更早)。序列从 0 和 1 开始,下一个数字总是前两个数字的和。这个序列永远继续下去:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 . . .
斐波那契数列在音乐创作、股票市场预测、向日葵头上的小花图案以及许多其他领域都有应用。这个程序可以让你计算出你想要的序列。更多关于斐波那契数列的信息可以在en.wikipedia.org/wiki/Fibonacci_number
找到。
运行示例
当您运行fibonacci.py
时,输出如下所示:
Fibonacci Sequence, by Al Sweigart email@protected `--snip--` Enter the Nth Fibonacci number you wish to calculate (such as 5, 50, 1000, 9999), or QUIT to quit: > 50 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049
工作原理
因为斐波那契数字很快变得非常大,所以第 46 到 50 行检查用户是否输入了 10,000 或更大的数字,并显示一个警告,提示输出可能需要一些时间才能在屏幕上完成。虽然你的程序几乎可以瞬间完成数百万次计算,但将文本打印到屏幕上相对较慢,可能需要几秒钟。我们程序中的警告提醒用户,他们总是可以通过按下Ctrl+C
来终止程序。
"""Fibonacci Sequence, by Al Sweigart email@protected Calculates numbers of the Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13... This code is available at https://nostarch.com/big-book-small-python-programming Tags: short, math""" import sys print('''Fibonacci Sequence, by Al Sweigart email@protected The Fibonacci sequence begins with 0 and 1, and the next number is the sum of the previous two numbers. The sequence continues forever: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987... ''') while True: # Main program loop. while True: # Keep asking until the user enters valid input. print('Enter the Nth Fibonacci number you wish to') print('calculate (such as 5, 50, 1000, 9999), or QUIT to quit:') response = input('> ').upper() if response == 'QUIT': print('Thanks for playing!') sys.exit() if response.isdecimal() and int(response) != 0: nth = int(response) break # Exit the loop when the user enteres a valid number. print('Please enter a number greater than 0, or QUIT.') print() # Handle the special cases if the user entered 1 or 2: if nth == 1: print('0') print() print('The #1 Fibonacci number is 0.') continue elif nth == 2: print('0, 1') print() print('The #2 Fibonacci number is 1.') continue # Display warning if the user entered a large number: if nth >= 10000: print('WARNING: This will take a while to display on the') print('screen. If you want to quit this program before it is') print('done, press Ctrl-C.') input('Press Enter to begin...') # Calculate the Nth Fibonacci number: secondToLastNumber = 0 lastNumber = 1 fibNumbersCalculated = 2 print('0, 1, ', end='') # Display the first two Fibonacci numbers. # Display all the later numbers of the Fibonacci sequence: while True: nextNumber = secondToLastNumber + lastNumber fibNumbersCalculated += 1 # Display the next number in the sequence: print(nextNumber, end='') # Check if we've found the Nth number the user wants: if fibNumbersCalculated == nth: print() print() print('The #', fibNumbersCalculated, ' Fibonacci ', 'number is ', nextNumber, sep='') break # Print a comma in between the sequence numbers: print(', ', end='') # Shift the last two numbers: secondToLastNumber = lastNumber lastNumber = nextNumber
在输入源代码并运行几次之后,尝试对其进行实验性的修改。你也可以自己想办法做到以下几点:
- 使用不同于 0 和 1 的起始数字。
- 通过将前三个数字而不是前两个数字相加来创建下一个数字。
探索程序
这是一个基础程序,所以没有太多的选项来定制它。相反,考虑一下:你如何使用这个程序?还有哪些有用的序列可以被编程?
二十七、鱼缸
在一个虚拟鱼缸里观看你自己的虚拟鱼,里面有气泡器和海藻植物。每次你运行这个程序,它会用不同的鱼类型和颜色随机生成鱼。休息一下,享受这个软件水族馆的平静安详,或者尝试在一些虚拟鲨鱼中编程来恐吓它的居民!您不能从 IDE 或编辑器中运行该程序。该程序使用bext
模块,必须从命令提示符或终端运行才能正确显示。关于bext
模块的更多信息可以在pypi.org/project/bext
找到。
运行示例
图 27-1 显示了运行fishtank.py
时的输出。
:鱼缸程序的输出,有几条鱼、海藻和泡泡
工作原理
现代图形程序通常通过擦除整个窗口并每秒重绘 30 或 60 次来生成动画。这给了他们每秒 30 或 60 帧(FPS)的帧速率。FPS 越高,动画运动就越流畅。
绘制到终端窗口要慢得多。如果我们擦除整个终端窗口,用bext
模块重新绘制它的内容,我们通常只能得到大约 3 或 4 FPS。这将导致窗口明显闪烁。
我们可以通过只在终端窗口发生变化的部分绘制字符来加快速度。鱼缸程序的大部分输出是空白空间,所以为了让元素移动,clearAquarium()
只需要在鱼、海藻和泡泡当前所在的地方画出' '
个空格字符。这增加了我们的帧速率,减少了闪烁,并使鱼缸动画更加令人愉快。
"""Fish Tank, by Al Sweigart email@protected A peaceful animation of a fish tank. Press Ctrl-C to stop. Similar to ASCIIQuarium or @EmojiAquarium, but mine is based on an older ASCII fish tank program for DOS. https://robobunny.com/projects/asciiquarium/html/ https://twitter.com/EmojiAquarium This code is available at https://nostarch.com/big-book-small-python-programming Tags: extra-large, artistic, bext""" import random, sys, time try: import bext except ImportError: print('This program requires the bext module, which you') print('can install by following the instructions at') print('https://pypi.org/project/Bext/') sys.exit() # Set up the constants: WIDTH, HEIGHT = bext.size() # We can't print to the last column on Windows without it adding a # newline automatically, so reduce the width by one: WIDTH -= 1 NUM_KELP = 2 # (!) Try changing this to 10. NUM_FISH = 10 # (!) Try changing this to 2 or 100. NUM_BUBBLERS = 1 # (!) Try changing this to 0 or 10. FRAMES_PER_SECOND = 4 # (!) Try changing this number to 1 or 60. # (!) Try changing the constants to create a fish tank with only kelp, # or only bubblers. # NOTE: Every string in a fish dictionary should be the same length. FISH_TYPES = [ {'right': ['><>'], 'left': ['<><']}, {'right': ['>||>'], 'left': ['<||<']}, {'right': ['>))>'], 'left': ['<[[<']}, {'right': ['>||o', '>||.'], 'left': ['o||<', '.||<']}, {'right': ['>))o', '>)).'], 'left': ['o[[<', '.[[<']}, {'right': ['>-==>'], 'left': ['<==-<']}, {'right': [r'>\\>'], 'left': ['<//<']}, {'right': ['><)))*>'], 'left': ['<*(((><']}, {'right': ['}-[[[*>'], 'left': ['<*]]]-{']}, {'right': [']-<)))b>'], 'left': ['<d(((>-[']}, {'right': ['><XXX*>'], 'left': ['<*XXX><']}, {'right': ['_.-._.-^=>', '.-._.-.^=>', '-._.-._^=>', '._.-._.^=>'], 'left': ['<=^-._.-._', '<=^.-._.-.', '<=^_.-._.-', '<=^._.-._.']}, ] # (!) Try adding your own fish to FISH_TYPES. LONGEST_FISH_LENGTH = 10 # Longest single string in FISH_TYPES. # The x and y positions where a fish runs into the edge of the screen: LEFT_EDGE = 0 RIGHT_EDGE = WIDTH - 1 - LONGEST_FISH_LENGTH TOP_EDGE = 0 BOTTOM_EDGE = HEIGHT - 2 def main(): global FISHES, BUBBLERS, BUBBLES, KELPS, STEP bext.bg('black') bext.clear() # Generate the global variables: FISHES = [] for i in range(NUM_FISH): FISHES.append(generateFish()) # NOTE: Bubbles are drawn, but not the bubblers themselves. BUBBLERS = [] for i in range(NUM_BUBBLERS): # Each bubbler starts at a random position. BUBBLERS.append(random.randint(LEFT_EDGE, RIGHT_EDGE)) BUBBLES = [] KELPS = [] for i in range(NUM_KELP): kelpx = random.randint(LEFT_EDGE, RIGHT_EDGE) kelp = {'x': kelpx, 'segments': []} # Generate each segment of the kelp: for i in range(random.randint(6, HEIGHT - 1)): kelp['segments'].append(random.choice(['(', ')'])) KELPS.append(kelp) # Run the simulation: STEP = 1 while True: simulateAquarium() drawAquarium() time.sleep(1 / FRAMES_PER_SECOND) clearAquarium() STEP += 1 def getRandomColor(): """Return a string of a random color.""" return random.choice(('black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white')) def generateFish(): """Return a dictionary that represents a fish.""" fishType = random.choice(FISH_TYPES) # Set up colors for each character in the fish text: colorPattern = random.choice(('random', 'head-tail', 'single')) fishLength = len(fishType['right'][0]) if colorPattern == 'random': # All parts are randomly colored. colors = [] for i in range(fishLength): colors.append(getRandomColor()) if colorPattern == 'single' or colorPattern == 'head-tail': colors = [getRandomColor()] * fishLength # All the same color. if colorPattern == 'head-tail': # Head/tail different from body. headTailColor = getRandomColor() colors[0] = headTailColor # Set head color. colors[-1] = headTailColor # Set tail color. # Set up the rest of fish data structure: fish = {'right': fishType['right'], 'left': fishType['left'], 'colors': colors, 'hSpeed': random.randint(1, 6), 'vSpeed': random.randint(5, 15), 'timeToHDirChange': random.randint(10, 60), 'timeToVDirChange': random.randint(2, 20), 'goingRight': random.choice([True, False]), 'goingDown': random.choice([True, False])} # 'x' is always the leftmost side of the fish body: fish['x'] = random.randint(0, WIDTH - 1 - LONGEST_FISH_LENGTH) fish['y'] = random.randint(0, HEIGHT - 2) return fish def simulateAquarium(): """Simulate the movements in the aquarium for one step.""" global FISHES, BUBBLERS, BUBBLES, KELP, STEP # Simulate the fish for one step: for fish in FISHES: # Move the fish horizontally: if STEP % fish['hSpeed'] == 0: if fish['goingRight']: if fish['x'] != RIGHT_EDGE: fish['x'] += 1 # Move the fish right. else: fish['goingRight'] = False # Turn the fish around. fish['colors'].reverse() # Turn the colors around. else: if fish['x'] != LEFT_EDGE: fish['x'] -= 1 # Move the fish left. else: fish['goingRight'] = True # Turn the fish around. fish['colors'].reverse() # Turn the colors around. # Fish can randomly change their horizontal direction: fish['timeToHDirChange'] -= 1 if fish['timeToHDirChange'] == 0: fish['timeToHDirChange'] = random.randint(10, 60) # Turn the fish around: fish['goingRight'] = not fish['goingRight'] # Move the fish vertically: if STEP % fish['vSpeed'] == 0: if fish['goingDown']: if fish['y'] != BOTTOM_EDGE: fish['y'] += 1 # Move the fish down. else: fish['goingDown'] = False # Turn the fish around. else: if fish['y'] != TOP_EDGE: fish['y'] -= 1 # Move the fish up. else: fish['goingDown'] = True # Turn the fish around. # Fish can randomly change their vertical direction: fish['timeToVDirChange'] -= 1 if fish['timeToVDirChange'] == 0: fish['timeToVDirChange'] = random.randint(2, 20) # Turn the fish around: fish['goingDown'] = not fish['goingDown'] # Generate bubbles from bubblers: for bubbler in BUBBLERS: # There is a 1 in 5 chance of making a bubble: if random.randint(1, 5) == 1: BUBBLES.append({'x': bubbler, 'y': HEIGHT - 2}) # Move the bubbles: for bubble in BUBBLES: diceRoll = random.randint(1, 6) if (diceRoll == 1) and (bubble['x'] != LEFT_EDGE): bubble['x'] -= 1 # Bubble goes left. elif (diceRoll == 2) and (bubble['x'] != RIGHT_EDGE): bubble['x'] += 1 # Bubble goes right. bubble['y'] -= 1 # The bubble always goes up. # Iterate over BUBBLES in reverse because I'm deleting from BUBBLES # while iterating over it. for i in range(len(BUBBLES) - 1, -1, -1): if BUBBLES[i]['y'] == TOP_EDGE: # Delete bubbles that reach the top. del BUBBLES[i] # Simulate the kelp waving for one step: for kelp in KELPS: for i, kelpSegment in enumerate(kelp['segments']): # 1 in 20 chance to change waving: if random.randint(1, 20) == 1: if kelpSegment == '(': kelp['segments'][i] = ')' elif kelpSegment == ')': kelp['segments'][i] = '(' def drawAquarium(): """Draw the aquarium on the screen.""" global FISHES, BUBBLERS, BUBBLES, KELP, STEP # Draw quit message. bext.fg('white') bext.goto(0, 0) print('Fish Tank, by Al Sweigart Ctrl-C to quit.', end='') # Draw the bubbles: bext.fg('white') for bubble in BUBBLES: bext.goto(bubble['x'], bubble['y']) print(random.choice(('o', 'O')), end='') # Draw the fish: for fish in FISHES: bext.goto(fish['x'], fish['y']) # Get the correct right- or left-facing fish text. if fish['goingRight']: fishText = fish['right'][STEP % len(fish['right'])] else: fishText = fish['left'][STEP % len(fish['left'])] # Draw each character of the fish text in the right color. for i, fishPart in enumerate(fishText): bext.fg(fish['colors'][i]) print(fishPart, end='') # Draw the kelp: bext.fg('green') for kelp in KELPS: for i, kelpSegment in enumerate(kelp['segments']): if kelpSegment == '(': bext.goto(kelp['x'], BOTTOM_EDGE - i) elif kelpSegment == ')': bext.goto(kelp['x'] + 1, BOTTOM_EDGE - i) print(kelpSegment, end='') # Draw the sand on the bottom: bext.fg('yellow') bext.goto(0, HEIGHT - 1) print(chr(9617) * (WIDTH - 1), end='') # Draws '░' characters. sys.stdout.flush() # (Required for bext-using programs.) def clearAquarium(): """Draw empty spaces over everything on the screen.""" global FISHES, BUBBLERS, BUBBLES, KELP # Draw the bubbles: for bubble in BUBBLES: bext.goto(bubble['x'], bubble['y']) print(' ', end='') # Draw the fish: for fish in FISHES: bext.goto(fish['x'], fish['y']) # Draw each character of the fish text in the right color. print(' ' * len(fish['left'][0]), end='') # Draw the kelp: for kelp in KELPS: for i, kelpSegment in enumerate(kelp['segments']): bext.goto(kelp['x'], HEIGHT - 2 - i) print(' ', end='') sys.stdout.flush() # (Required for bext-using programs.) # 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.
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 加上在沙质海底移动的螃蟹。
- 添加一个随机出现在沙底的 ASCII 艺术画城堡。
- 让鱼在短时间内随机提高速度。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 51 行的
LONGEST_FISH_LENGTH = 10
改成LONGEST_FISH_LENGTH = 50
会怎么样? - 如果把 121 行的
'right': fishType['right']
改成'right': fishType['left']
会怎么样? - 如果把 249 行的
bext.fg('green')
改成bext.fg('red')
会怎么样? - 如果删除或注释掉第 92 行的
clearAquarium()
会发生什么? - 如果把 245 行的
bext.fg(fish['colors'][i])
改成bext.fg('random')
会怎么样? - 如果把 161 行的
random.randint(10, 60)
改成1
会怎么样?
二十八、填充器
填充器是一款色彩丰富的游戏,玩家试图通过改变左上角瓷砖的颜色来用单一颜色填充棋盘。这种新颜色会扩展到所有与原始颜色匹配的相邻图块。类似于填充它的手游。这个程序也有一个色盲模式,它使用形状而不是扁平的彩色瓷砖。它依靠递归填充填充算法来绘制棋盘,其工作方式类似于许多绘画应用中的“油漆桶”或“填充”工具。
运行示例
图 28-1 显示了运行flooder.py
时的输出。
:色盲模式下投光灯游戏的输出,显示不同的形状而不是矩形
工作原理
可访问性是视频游戏中的一个大问题,解决这个问题可以采取多种形式。例如,红绿色盲,会导致红色和绿色的阴影看起来相同,从而很难区分屏幕上的红色物体和绿色物体。我们可以通过使用不同的形状而不是不同的颜色来使填充器更容易使用。请注意,即使是色盲模式仍然使用颜色。这意味着如果你愿意,你可以取消“标准”模式,甚至让色盲用户也可以在色盲模式下玩游戏。最好的易访问性设计是那些从一开始就包含易访问性考虑的设计,而不是将它们作为一个单独的模式添加进去。这减少了我们必须编写的代码量,并使未来的错误修复变得更加容易。
其他可访问性问题包括确保文本足够大,即使没有完美的视觉也可以阅读,声音效果有视觉提示,口语有针对听力障碍者的字幕,以及控件可以重新映射到其他键盘按键,以便人们可以用一只手玩游戏。YouTube 频道游戏制作工具包有一个名为“为残疾人设计”的视频系列,涵盖了在设计游戏时考虑无障碍的许多方面。
"""Flooder, by Al Sweigart email@protected A colorful game where you try to fill the board with a single color. Has a mode for colorblind players. Inspired by the "Flood It!" game. This code is available at https://nostarch.com/big-book-small-python-programming Tags: large, bext, game""" import random, sys try: import bext except ImportError: print('This program requires the bext module, which you') print('can install by following the instructions at') print('https://pypi.org/project/Bext/') sys.exit() # Set up the constants: BOARD_WIDTH = 16 # (!) Try changing this to 4 or 40. BOARD_HEIGHT = 14 # (!) Try changing this to 4 or 20. MOVES_PER_GAME = 20 # (!) Try changing this to 3 or 300. # Constants for the different shapes used in colorblind mode: HEART = chr(9829) # Character 9829 is '♥'. DIAMOND = chr(9830) # Character 9830 is '♦'. SPADE = chr(9824) # Character 9824 is '♠'. CLUB = chr(9827) # Character 9827 is '♣'. BALL = chr(9679) # Character 9679 is '●'. TRIANGLE = chr(9650) # Character 9650 is '▲'. BLOCK = chr(9608) # Character 9608 is '█' LEFTRIGHT = chr(9472) # Character 9472 is '─' UPDOWN = chr(9474) # Character 9474 is '│' DOWNRIGHT = chr(9484) # Character 9484 is '┌' DOWNLEFT = chr(9488) # Character 9488 is '┐' UPRIGHT = chr(9492) # Character 9492 is '└' UPLEFT = chr(9496) # Character 9496 is '┘' # A list of chr() codes is at https://inventwithpython.com/chr # All the color/shape tiles used on the board: TILE_TYPES = (0, 1, 2, 3, 4, 5) COLORS_MAP = {0: 'red', 1: 'green', 2:'blue', 3:'yellow', 4:'cyan', 5:'purple'} COLOR_MODE = 'color mode' SHAPES_MAP = {0: HEART, 1: TRIANGLE, 2: DIAMOND, 3: BALL, 4: CLUB, 5: SPADE} SHAPE_MODE = 'shape mode' def main(): bext.bg('black') bext.fg('white') bext.clear() print('''Flooder, by Al Sweigart email@protected Set the upper left color/shape, which fills in all the adjacent squares of that color/shape. Try to make the entire board the same color/shape.''') print('Do you want to play in colorblind mode? Y/N') response = input('> ') if response.upper().startswith('Y'): displayMode = SHAPE_MODE else: displayMode = COLOR_MODE gameBoard = getNewBoard() movesLeft = MOVES_PER_GAME while True: # Main game loop. displayBoard(gameBoard, displayMode) print('Moves left:', movesLeft) playerMove = askForPlayerMove(displayMode) changeTile(playerMove, gameBoard, 0, 0) movesLeft -= 1 if hasWon(gameBoard): displayBoard(gameBoard, displayMode) print('You have won!') break elif movesLeft == 0: displayBoard(gameBoard, displayMode) print('You have run out of moves!') break def getNewBoard(): """Return a dictionary of a new Flood It board.""" # Keys are (x, y) tuples, values are the tile at that position. board = {} # Create random colors for the board. for x in range(BOARD_WIDTH): for y in range(BOARD_HEIGHT): board[(x, y)] = random.choice(TILE_TYPES) # Make several tiles the same as their neighbor. This creates groups # of the same color/shape. for i in range(BOARD_WIDTH * BOARD_HEIGHT): x = random.randint(0, BOARD_WIDTH - 2) y = random.randint(0, BOARD_HEIGHT - 1) board[(x + 1, y)] = board[(x, y)] return board def displayBoard(board, displayMode): """Display the board on the screen.""" bext.fg('white') # Display the top edge of the board: print(DOWNRIGHT + (LEFTRIGHT * BOARD_WIDTH) + DOWNLEFT) # Display each row: for y in range(BOARD_HEIGHT): bext.fg('white') if y == 0: # The first row begins with '>'. print('>', end='') else: # Later rows begin with a white vertical line. print(UPDOWN, end='') # Display each tile in this row: for x in range(BOARD_WIDTH): bext.fg(COLORS_MAP[board[(x, y)]]) if displayMode == COLOR_MODE: print(BLOCK, end='') elif displayMode == SHAPE_MODE: print(SHAPES_MAP[board[(x, y)]], end='') bext.fg('white') print(UPDOWN) # Rows end with a white vertical line. # Display the bottom edge of the board: print(UPRIGHT + (LEFTRIGHT * BOARD_WIDTH) + UPLEFT) def askForPlayerMove(displayMode): """Let the player select a color to paint the upper left tile.""" while True: bext.fg('white') print('Choose one of ', end='') if displayMode == COLOR_MODE: bext.fg('red') print('(R)ed ', end='') bext.fg('green') print('(G)reen ', end='') bext.fg('blue') print('(B)lue ', end='') bext.fg('yellow') print('(Y)ellow ', end='') bext.fg('cyan') print('(C)yan ', end='') bext.fg('purple') print('(P)urple ', end='') elif displayMode == SHAPE_MODE: bext.fg('red') print('(H)eart, ', end='') bext.fg('green') print('(T)riangle, ', end='') bext.fg('blue') print('(D)iamond, ', end='') bext.fg('yellow') print('(B)all, ', end='') bext.fg('cyan') print('(C)lub, ', end='') bext.fg('purple') print('(S)pade, ', end='') bext.fg('white') print('or QUIT:') response = input('> ').upper() if response == 'QUIT': print('Thanks for playing!') sys.exit() if displayMode == COLOR_MODE and response in tuple('RGBYCP'): # Return a tile type number based on the response: return {'R': 0, 'G': 1, 'B': 2, 'Y': 3, 'C': 4, 'P': 5}[response] if displayMode == SHAPE_MODE and response in tuple('HTDBCS'): # Return a tile type number based on the response: return {'H': 0, 'T': 1, 'D':2, 'B': 3, 'C': 4, 'S': 5}[response] def changeTile(tileType, board, x, y, charToChange=None): """Change the color/shape of a tile using the recursive flood fill algorithm.""" if x == 0 and y == 0: charToChange = board[(x, y)] if tileType == charToChange: return # Base Case: Already is the same tile. board[(x, y)] = tileType if x > 0 and board[(x - 1, y)] == charToChange: # Recursive Case: Change the left neighbor's tile: changeTile(tileType, board, x - 1, y, charToChange) if y > 0 and board[(x, y - 1)] == charToChange: # Recursive Case: Change the top neighbor's tile: changeTile(tileType, board, x, y - 1, charToChange) if x < BOARD_WIDTH - 1 and board[(x + 1, y)] == charToChange: # Recursive Case: Change the right neighbor's tile: changeTile(tileType, board, x + 1, y, charToChange) if y < BOARD_HEIGHT - 1 and board[(x, y + 1)] == charToChange: # Recursive Case: Change the bottom neighbor's tile: changeTile(tileType, board, x, y + 1, charToChange) def hasWon(board): """Return True if the entire board is one color/shape.""" tile = board[(0, 0)] for x in range(BOARD_WIDTH): for y in range(BOARD_HEIGHT): if board[(x, y)] != tile: return False return True # If this program was run (instead of imported), run the game: if __name__ == '__main__': main()
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 添加其他形状和颜色。
- 创建除矩形之外的其他纸板形状。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果将第 92 行的
board = {}
改为board = []
,会得到什么错误信息? - 如果将第 105 行的
return board
改为return None
,会得到什么错误信息? - 如果把第 76 行的
movesLeft -= 1
改成movesLeft -= 0
会怎么样?
二十九、森林火灾模拟
这个模拟展示了一个森林,它的树木不断生长,然后被烧毁。在模拟的每一步中,有 1%的可能性一片空白长成一棵树,有 1%的可能性一棵树被闪电击中并烧毁。大火会蔓延到邻近的树木,所以密集的森林比稀疏的森林更容易遭受更大的火灾。这个模拟的灵感来自尼基·凯斯在ncase.me/simulating/model
的表情符号。
运行示例
当您运行forestfiresim.py
时,输出将如下所示:
:森林火灾模拟,绿色A
表示树木,红色W
表示火焰
工作原理
这个模拟是涌现行为的一个例子——系统中简单部分之间的相互作用创造了复杂的模式。空地长成树,闪电把树变成火,火又把树变回空地,同时蔓延到邻近的树。通过调整树木生长和雷击率,您可以使森林显示不同的现象。例如,闪电几率低但生长率高会导致持续的大面积森林火灾,因为树木往往彼此靠近并迅速补充。增长率低,但雷击几率高,会引发几起小火灾,但由于附近缺乏树木,这些小火灾很快就会熄灭。我们不会显式地对这些行为进行编程;相反,它自然地从我们创造的系统中出现。
"""Forest Fire Sim, by Al Sweigart email@protected A simulation of wildfires spreading in a forest. Press Ctrl-C to stop. Inspired by Nicky Case's Emoji Sim http://ncase.me/simulating/model/ This code is available at https://nostarch.com/big-book-small-python-programming Tags: short, bext, simulation""" import random, sys, time try: import bext except ImportError: print('This program requires the bext module, which you') print('can install by following the instructions at') print('https://pypi.org/project/Bext/') sys.exit() # Set up the constants: WIDTH = 79 HEIGHT = 22 TREE = 'A' FIRE = 'W' EMPTY = ' ' # (!) Try changing these settings to anything between 0.0 and 1.0: INITIAL_TREE_DENSITY = 0.20 # Amount of forest that starts with trees. GROW_CHANCE = 0.01 # Chance a blank space turns into a tree. FIRE_CHANCE = 0.01 # Chance a tree is hit by lightning & burns. # (!) Try setting the pause length to 1.0 or 0.0: PAUSE_LENGTH = 0.5 def main(): forest = createNewForest() bext.clear() while True: # Main program loop. displayForest(forest) # Run a single simulation step: nextForest = {'width': forest['width'], 'height': forest['height']} for x in range(forest['width']): for y in range(forest['height']): if (x, y) in nextForest: # If we've already set nextForest[(x, y)] on a # previous iteration, just do nothing here: continue if ((forest[(x, y)] == EMPTY) and (random.random() <= GROW_CHANCE)): # Grow a tree in this empty space. nextForest[(x, y)] = TREE elif ((forest[(x, y)] == TREE) and (random.random() <= FIRE_CHANCE)): # Lightning sets this tree on fire. nextForest[(x, y)] = FIRE elif forest[(x, y)] == FIRE: # This tree is currently burning. # Loop through all the neighboring spaces: for ix in range(-1, 2): for iy in range(-1, 2): # Fire spreads to neighboring trees: if forest.get((x + ix, y + iy)) == TREE: nextForest[(x + ix, y + iy)] = FIRE # The tree has burned down now, so erase it: nextForest[(x, y)] = EMPTY else: # Just copy the existing object: nextForest[(x, y)] = forest[(x, y)] forest = nextForest time.sleep(PAUSE_LENGTH) def createNewForest(): """Returns a dictionary for a new forest data structure.""" forest = {'width': WIDTH, 'height': HEIGHT} for x in range(WIDTH): for y in range(HEIGHT): if (random.random() * 100) <= INITIAL_TREE_DENSITY: forest[(x, y)] = TREE # Start as a tree. else: forest[(x, y)] = EMPTY # Start as an empty space. return forest def displayForest(forest): """Display the forest data structure on the screen.""" bext.goto(0, 0) for y in range(forest['height']): for x in range(forest['width']): if forest[(x, y)] == TREE: bext.fg('green') print(TREE, end='') elif forest[(x, y)] == FIRE: bext.fg('red') print(FIRE, end='') elif forest[(x, y)] == EMPTY: print(EMPTY, end='') print() bext.fg('reset') # Use the default font color. print('Grow chance: {}% '.format(GROW_CHANCE * 100), end='') print('Lightning chance: {}% '.format(FIRE_CHANCE * 100), end='') print('Press Ctrl-C to quit.') # 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.
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 添加随机创建的湖泊和河流,作为火焰无法穿越的防火屏障。
- 加上一棵树从它的邻居那里着火的百分比几率。
- 添加不同种类的树,着火几率不同。
- 添加燃烧树木的不同状态,以便一棵树被烧毁需要多个模拟步骤。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 96 行的
bext.fg('green')
改成bext.fg('random')
会怎么样? - 如果把第 23 行的
EMPTY = ' '
改成EMPTY = '.'
会怎么样? - 如果把第 66 行的
forest.get((x + ix, y + iy)) == TREE
改成forest.get((x + ix, y + iy)) == EMPTY
会怎么样? - 如果把第 69 行的
nextForest[(x, y)] = EMPTY
改成nextForest[(x, y)] = FIRE
会怎么样? - 如果把 86 行的
forest[(x, y)] = EMPTY
改成forest[(x, y)] = TREE
会怎么样?
三十、四个一排
在这款经典的双人掷瓷砖棋盘游戏中,你必须设法让你的四块瓷砖水平、垂直或对角排成一行,同时防止你的对手做同样的事情。这个程序类似于“连接四个”。
运行示例
当您运行fourinarow.py
时,输出将如下所示:
Four in a Row, by Al Sweigart email@protected `--snip--` 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |.......| +-------+ Player X, enter a column or QUIT: > 3 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |..X....| +-------+ Player O, enter a column or QUIT: > 5 `--snip--` Player O, enter a column or QUIT: > 4 1234567 +-------+ |.......| |.......| |XXX.XO.| |OOOOXO.| |OOOXOX.| |OXXXOXX| +-------+ Player O has won!
工作原理
本书中的棋盘游戏项目遵循类似的程序结构。通常有一个字典或列表来表示棋盘的状态,一个getNewBoard()
函数返回棋盘的数据结构,一个displayBoard()
函数在屏幕上呈现棋盘的数据结构,等等。你可以查看本书中带有桌游标签的其他项目,并相互比较,尤其是当你想创建自己的原创桌游程序时。
"""Four in a Row, by Al Sweigart email@protected A tile-dropping game to get four in a row, similar to Connect Four. This code is available at https://nostarch.com/big-book-small-python-programming Tags: large, game, board game, two-player""" import sys # Constants used for displaying the board: EMPTY_SPACE = '.' # A period is easier to count than a space. PLAYER_X = 'X' PLAYER_O = 'O' # Note: Update displayBoard() & COLUMN_LABELS if BOARD_WIDTH is changed. BOARD_WIDTH = 7 BOARD_HEIGHT = 6 COLUMN_LABELS = ('1', '2', '3', '4', '5', '6', '7') assert len(COLUMN_LABELS) == BOARD_WIDTH def main(): print("""Four in a Row, by Al Sweigart email@protected Two players take turns dropping tiles into one of seven columns, trying to make four in a row horizontally, vertically, or diagonally. """) # Set up a new game: gameBoard = getNewBoard() playerTurn = PLAYER_X while True: # Run a player's turn. # Display the board and get player's move: displayBoard(gameBoard) playerMove = askForPlayerMove(playerTurn, gameBoard) gameBoard[playerMove] = playerTurn # Check for a win or tie: if isWinner(playerTurn, gameBoard): displayBoard(gameBoard) # Display the board one last time. print('Player ' + playerTurn + ' has won!') sys.exit() elif isFull(gameBoard): displayBoard(gameBoard) # Display the board one last time. print('There is a tie!') sys.exit() # Switch turns to other player: if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X def getNewBoard(): """Returns a dictionary that represents a Four in a Row board. The keys are (columnIndex, rowIndex) tuples of two integers, and the values are one of the 'X', 'O' or '.' (empty space) strings.""" board = {} for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT): board[(columnIndex, rowIndex)] = EMPTY_SPACE return board def displayBoard(board): """Display the board and its tiles on the screen.""" '''Prepare a list to pass to the format() string method for the board template. The list holds all of the board's tiles (and empty spaces) going left to right, top to bottom:''' tileChars = [] for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): tileChars.append(board[(columnIndex, rowIndex)]) # Display the board: print(""" 1234567 +-------+ |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| +-------+""".format(*tileChars)) def askForPlayerMove(playerTile, board): """Let a player select a column on the board to drop a tile into. Returns a tuple of the (column, row) that the tile falls into.""" while True: # Keep asking player until they enter a valid move. print('Player {}, enter a column or QUIT:'.format(playerTile)) response = input('> ').upper().strip() if response == 'QUIT': print('Thanks for playing!') sys.exit() if response not in COLUMN_LABELS: print('Enter a number from 1 to {}.'.format(BOARD_WIDTH)) continue # Ask player again for their move. columnIndex = int(response) - 1 # -1 for 0-based the index. # If the column is full, ask for a move again: if board[(columnIndex, 0)] != EMPTY_SPACE: print('That column is full, select another one.') continue # Ask player again for their move. # Starting from the bottom, find the first empty space. for rowIndex in range(BOARD_HEIGHT - 1, -1, -1): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return (columnIndex, rowIndex) def isFull(board): """Returns True if the `board` has no empty spaces, otherwise returns False.""" for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return False # Found an empty space, so return False. return True # All spaces are full. def isWinner(playerTile, board): """Returns True if `playerTile` has four tiles in a row on `board`, otherwise returns False.""" # Go through the entire board, checking for four-in-a-row: for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT): # Check for horizontal four-in-a-row going right: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex)] tile3 = board[(columnIndex + 2, rowIndex)] tile4 = board[(columnIndex + 3, rowIndex)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT - 3): # Check for vertical four-in-a-row going down: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex, rowIndex + 1)] tile3 = board[(columnIndex, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT - 3): # Check for four-in-a-row going right-down diagonal: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex + 1)] tile3 = board[(columnIndex + 2, rowIndex + 2)] tile4 = board[(columnIndex + 3, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True # Check for four-in-a-row going left-down diagonal: tile1 = board[(columnIndex + 3, rowIndex)] tile2 = board[(columnIndex + 2, rowIndex + 1)] tile3 = board[(columnIndex + 1, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True return False # If the program is run (instead of imported), run the game: if __name__ == '__main__': main()
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 创建三排或五排变体。
- 做一个这个游戏的三人变种。
- 添加一个“通配符”牌,该牌在玩家回合后随机掉落,并且可以由任何一个玩家使用。
- 添加任一玩家都不能使用的“阻止”牌。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 11 行的
PLAYER_O = 'O'
改成PLAYER_O = 'X'
会怎么样? - 如果把 116 行的
return (columnIndex, rowIndex)
改成return (columnIndex, 0)
会怎么样? - 如果把第 98 行的
response == 'QUIT'
改成response != 'QUIT'
会怎么样? - 如果将第 72 行的
tileChars = []
改为tileChars = {}
,会得到什么错误信息?