三十六、沙漏
这个可视化程序有一个粗糙的物理引擎,模拟沙子通过沙漏的小孔落下。沙子堆积在沙漏的下半部分;然后把沙漏翻过来,重复这个过程。
运行示例
图 36-1 显示了运行hourglass.py
时的输出。
:沙漏程序在落砂时的输出
工作原理
沙漏程序实现了一个基本的物理引擎。一个物理引擎是模拟物理物体在重力作用下下落,相互碰撞,按照物理定律运动的软件。你会发现在视频游戏、计算机动画和科学模拟中使用的物理引擎。在第 91 到 102 行,每一粒沙子检查它下面的空间是否是空的,如果是,就向下移动。否则,它检查它是否可以向左下方移动(第 104 到 112 行)或向右下方移动(第 114 到 122 行)。当然,运动学,经典物理学的一个分支,处理宏观物体的运动,远不止这些。然而,你不需要一个物理学学位来制作一个沙漏中沙子的原始模拟,它看起来是令人愉快的。
"""Hourglass, by Al Sweigart email@protected
An animation of an hourglass with falling sand. Press Ctrl-C to stop.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, artistic, 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:
PAUSE_LENGTH = 0.2 # (!) Try changing this to 0.0 or 1.0.
# (!) Try changing this to any number between 0 and 100:
WIDE_FALL_CHANCE = 50
SCREEN_WIDTH = 79
SCREEN_HEIGHT = 25
X = 0 # The index of X values in an (x, y) tuple is 0.
Y = 1 # The index of Y values in an (x, y) tuple is 1.
SAND = chr(9617)
WALL = chr(9608)
# Set up the walls of the hour glass:
HOURGLASS = set() # Has (x, y) tuples for where hourglass walls are.
# (!) Try commenting out some HOURGLASS.add() lines to erase walls:
for i in range(18, 37):
HOURGLASS.add((i, 1)) # Add walls for the top cap of the hourglass.
HOURGLASS.add((i, 23)) # Add walls for the bottom cap.
for i in range(1, 5):
HOURGLASS.add((18, i)) # Add walls for the top left straight wall.
HOURGLASS.add((36, i)) # Add walls for the top right straight wall.
HOURGLASS.add((18, i + 19)) # Add walls for the bottom left.
HOURGLASS.add((36, i + 19)) # Add walls for the bottom right.
for i in range(8):
HOURGLASS.add((19 + i, 5 + i)) # Add the top left slanted wall.
HOURGLASS.add((35 - i, 5 + i)) # Add the top right slanted wall.
HOURGLASS.add((25 - i, 13 + i)) # Add the bottom left slanted wall.
HOURGLASS.add((29 + i, 13 + i)) # Add the bottom right slanted wall.
# Set up the initial sand at the top of the hourglass:
INITIAL_SAND = set()
for y in range(8):
for x in range(19 + y, 36 - y):
INITIAL_SAND.add((x, y + 4))
def main():
bext.fg('yellow')
bext.clear()
# Draw the quit message:
bext.goto(0, 0)
print('Ctrl-C to quit.', end='')
# Display the walls of the hourglass:
for wall in HOURGLASS:
bext.goto(wall[X], wall[Y])
print(WALL, end='')
while True: # Main program loop.
allSand = list(INITIAL_SAND)
# Draw the initial sand:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(SAND, end='')
runHourglassSimulation(allSand)
def runHourglassSimulation(allSand):
"""Keep running the sand falling simulation until the sand stops
moving."""
while True: # Keep looping until sand has run out.
random.shuffle(allSand) # Random order of grain simulation.
sandMovedOnThisStep = False
for i, sand in enumerate(allSand):
if sand[Y] == SCREEN_HEIGHT - 1:
# Sand is on the very bottom, so it won't move:
continue
# If nothing is under this sand, move it down:
noSandBelow = (sand[X], sand[Y] + 1) not in allSand
noWallBelow = (sand[X], sand[Y] + 1) not in HOURGLASS
canFallDown = noSandBelow and noWallBelow
if canFallDown:
# Draw the sand in its new position down one space:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Clear the old position.
bext.goto(sand[X], sand[Y] + 1)
print(SAND, end='')
# Set the sand in its new position down one space:
allSand[i] = (sand[X], sand[Y] + 1)
sandMovedOnThisStep = True
else:
# Check if the sand can fall to the left:
belowLeft = (sand[X] - 1, sand[Y] + 1)
noSandBelowLeft = belowLeft not in allSand
noWallBelowLeft = belowLeft not in HOURGLASS
left = (sand[X] - 1, sand[Y])
noWallLeft = left not in HOURGLASS
notOnLeftEdge = sand[X] > 0
canFallLeft = (noSandBelowLeft and noWallBelowLeft
and noWallLeft and notOnLeftEdge)
# Check if the sand can fall to the right:
belowRight = (sand[X] + 1, sand[Y] + 1)
noSandBelowRight = belowRight not in allSand
noWallBelowRight = belowRight not in HOURGLASS
right = (sand[X] + 1, sand[Y])
noWallRight = right not in HOURGLASS
notOnRightEdge = sand[X] < SCREEN_WIDTH - 1
canFallRight = (noSandBelowRight and noWallBelowRight
and noWallRight and notOnRightEdge)
# Set the falling direction:
fallingDirection = None
if canFallLeft and not canFallRight:
fallingDirection = -1 # Set the sand to fall left.
elif not canFallLeft and canFallRight:
fallingDirection = 1 # Set the sand to fall right.
elif canFallLeft and canFallRight:
# Both are possible, so randomly set it:
fallingDirection = random.choice((-1, 1))
# Check if the sand can "far" fall two spaces to
# the left or right instead of just one space:
if random.random() * 100 <= WIDE_FALL_CHANCE:
belowTwoLeft = (sand[X] - 2, sand[Y] + 1)
noSandBelowTwoLeft = belowTwoLeft not in allSand
noWallBelowTwoLeft = belowTwoLeft not in HOURGLASS
notOnSecondToLeftEdge = sand[X] > 1
canFallTwoLeft = (canFallLeft and noSandBelowTwoLeft
and noWallBelowTwoLeft and notOnSecondToLeftEdge)
belowTwoRight = (sand[X] + 2, sand[Y] + 1)
noSandBelowTwoRight = belowTwoRight not in allSand
noWallBelowTwoRight = belowTwoRight not in HOURGLASS
notOnSecondToRightEdge = sand[X] < SCREEN_WIDTH - 2
canFallTwoRight = (canFallRight
and noSandBelowTwoRight and noWallBelowTwoRight
and notOnSecondToRightEdge)
if canFallTwoLeft and not canFallTwoRight:
fallingDirection = -2
elif not canFallTwoLeft and canFallTwoRight:
fallingDirection = 2
elif canFallTwoLeft and canFallTwoRight:
fallingDirection = random.choice((-2, 2))
if fallingDirection == None:
# This sand can't fall, so move on.
continue
# Draw the sand in its new position:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Erase old sand.
bext.goto(sand[X] + fallingDirection, sand[Y] + 1)
print(SAND, end='') # Draw new sand.
# Move the grain of sand to its new position:
allSand[i] = (sand[X] + fallingDirection, sand[Y] + 1)
sandMovedOnThisStep = True
sys.stdout.flush() # (Required for bext-using programs.)
time.sleep(PAUSE_LENGTH) # Pause after this
# If no sand has moved on this step, reset the hourglass:
if not sandMovedOnThisStep:
time.sleep(2)
# Erase all of the sand:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(' ', end='')
break # Break out of main simulation loop.
# 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.
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 创建除沙漏以外的墙壁形状。
- 在屏幕上创建点,不断涌出新的沙粒。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 31 行的
range(18, 37)
改成range(18, 30)
会怎么样? - 如果把第 39 行的
range(8)
改成range(0)
会怎么样? - 如果把第 82 行的
sandMovedOnThisStep = False
改成sandMovedOnThisStep = True
会怎么样? - 如果把 125 行的
fallingDirection = None
改成fallingDirection = 1
会怎么样? - 如果把 136 行的
random.random() * 100 <= WIDE_FALL_CHANCE
改成random.random() * 0 <= WIDE_FALL_CHANCE
会怎么样?
三十七、饥饿机器人
你和饥饿的机器人被困在一个迷宫里!你不知道机器人为什么需要吃饭,但你也不想知道。机器人的程序设计很糟糕,即使被墙挡住,它们也会直接向你移动。你必须欺骗机器人互相碰撞(或死亡的机器人)而不被抓住。
你有一个个人传送装置,可以把你送到一个随机的新地方,但它的电池只够两次旅行。此外,你和机器人可以溜过角落!
运行示例
当您运行hungryrobots.py
时,输出将如下所示:
Hungry Robots, by Al Sweigart email@protected
`--snip--`
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░ ░ R R ░ ░ ░
░ ░ ░░░ R░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░░░ ░
░ R░ ░ ░ ░░ ░░ ░ ░
░ ░░ ░ ░ ░░░ ░ ░ ░
░ ░░ ░ RX░░░ ░ ░ ░ ░ ░
░ ░ R R R ░ ░ ░
░ ░ ░ ░ ░ R ░ ░
░ ░ R R ░ R ░ R ░
░ ░ ░ ░ ░ ░ ░ ░ ░
░ @ ░ ░ R░░░ ░ ░
░ ░ ░░ ░░ ░ ░
░ ░ ░░ ░ ░ R ░░
░░X ░ ░ ░ R ░░RR ░ R ░
░RR R R ░ ░ ░ R░
░ ░░ RRR R ░
░ ░░R ░ ░
░ R ░ ░ ░ ░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
(T)eleports remaining: 2
(Q) (W) ( )
(A) (S) (D)
Enter move or QUIT: (Z) (X) ( )
`--snip--`
工作原理
在这个游戏中代表位置的 x 和 y 笛卡尔坐标允许我们使用数学来确定机器人应该移动的方向。在编程中,x 坐标向右增加,y 坐标向下增加。这意味着如果机器人的 x 坐标大于玩家的坐标,它应该向左移动(即代码应该从其当前的 x 坐标中减去)以靠近玩家。如果机器人的 x 坐标更小,它应该向右移动(也就是说,代码应该添加到其当前的 x 坐标中)。这同样适用于基于相对 y 坐标的上下移动。
"""Hungry Robots, by Al Sweigart email@protected
Escape the hungry robots by making them crash into each other.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, game"""
import random, sys
# Set up the constants:
WIDTH = 40 # (!) Try changing this to 70 or 10.
HEIGHT = 20 # (!) Try changing this to 10.
NUM_ROBOTS = 10 # (!) Try changing this to 1 or 30.
NUM_TELEPORTS = 2 # (!) Try changing this to 0 or 9999.
NUM_DEAD_ROBOTS = 2 # (!) Try changing this to 0 or 20.
NUM_WALLS = 100 # (!) Try changing this to 0 or 300.
EMPTY_SPACE = ' ' # (!) Try changing this to '.'.
PLAYER = '@' # (!) Try changing this to 'R'.
ROBOT = 'R' # (!) Try changing this to '@'.
DEAD_ROBOT = 'X' # (!) Try changing this to 'R'.
# (!) Try changing this to '#' or 'O' or ' ':
WALL = chr(9617) # Character 9617 is '░'
def main():
print('''Hungry Robots, by Al Sweigart email@protected
You are trapped in a maze with hungry robots! You don't know why robots
need to eat, but you don't want to find out. The robots are badly
programmed and will move directly toward you, even if blocked by walls.
You must trick the robots into crashing into each other (or dead robots)
without being caught. You have a personal teleporter device, but it only
has enough battery for {} trips. Keep in mind, you and robots can slip
through the corners of two diagonal walls!
'''.format(NUM_TELEPORTS))
input('Press Enter to begin...')
# Set up a new game:
board = getNewBoard()
robots = addRobots(board)
playerPosition = getRandomEmptySpace(board, robots)
while True: # Main game loop.
displayBoard(board, robots, playerPosition)
if len(robots) == 0: # Check if the player has won.
print('All the robots have crashed into each other and you')
print('lived to tell the tale! Good job!')
sys.exit()
# Move the player and robots:
playerPosition = askForPlayerMove(board, robots, playerPosition)
robots = moveRobots(board, robots, playerPosition)
for x, y in robots: # Check if the player has lost.
if (x, y) == playerPosition:
displayBoard(board, robots, playerPosition)
print('You have been caught by a robot!')
sys.exit()
def getNewBoard():
"""Returns a dictionary that represents the board. The keys are
(x, y) tuples of integer indexes for board positions, the values are
WALL, EMPTY_SPACE, or DEAD_ROBOT. The dictionary also has the key
'teleports' for the number of teleports the player has left.
The living robots are stored separately from the board dictionary."""
board = {'teleports': NUM_TELEPORTS}
# Create an empty board:
for x in range(WIDTH):
for y in range(HEIGHT):
board[(x, y)] = EMPTY_SPACE
# Add walls on the edges of the board:
for x in range(WIDTH):
board[(x, 0)] = WALL # Make top wall.
board[(x, HEIGHT - 1)] = WALL # Make bottom wall.
for y in range(HEIGHT):
board[(0, y)] = WALL # Make left wall.
board[(WIDTH - 1, y)] = WALL # Make right wall.
# Add the random walls:
for i in range(NUM_WALLS):
x, y = getRandomEmptySpace(board, [])
board[(x, y)] = WALL
# Add the starting dead robots:
for i in range(NUM_DEAD_ROBOTS):
x, y = getRandomEmptySpace(board, [])
board[(x, y)] = DEAD_ROBOT
return board
def getRandomEmptySpace(board, robots):
"""Return a (x, y) integer tuple of an empty space on the board."""
while True:
randomX = random.randint(1, WIDTH - 2)
randomY = random.randint(1, HEIGHT - 2)
if isEmpty(randomX, randomY, board, robots):
break
return (randomX, randomY)
def isEmpty(x, y, board, robots):
"""Return True if the (x, y) is empty on the board and there's also
no robot there."""
return board[(x, y)] == EMPTY_SPACE and (x, y) not in robots
def addRobots(board):
"""Add NUM_ROBOTS number of robots to empty spaces on the board and
return a list of these (x, y) spaces where robots are now located."""
robots = []
for i in range(NUM_ROBOTS):
x, y = getRandomEmptySpace(board, robots)
robots.append((x, y))
return robots
def displayBoard(board, robots, playerPosition):
"""Display the board, robots, and player on the screen."""
# Loop over every space on the board:
for y in range(HEIGHT):
for x in range(WIDTH):
# Draw the appropriate character:
if board[(x, y)] == WALL:
print(WALL, end='')
elif board[(x, y)] == DEAD_ROBOT:
print(DEAD_ROBOT, end='')
elif (x, y) == playerPosition:
print(PLAYER, end='')
elif (x, y) in robots:
print(ROBOT, end='')
else:
print(EMPTY_SPACE, end='')
print() # Print a newline.
def askForPlayerMove(board, robots, playerPosition):
"""Returns the (x, y) integer tuple of the place the player moves
next, given their current location and the walls of the board."""
playerX, playerY = playerPosition
# Find which directions aren't blocked by a wall:
q = 'Q' if isEmpty(playerX - 1, playerY - 1, board, robots) else ' '
w = 'W' if isEmpty(playerX + 0, playerY - 1, board, robots) else ' '
e = 'E' if isEmpty(playerX + 1, playerY - 1, board, robots) else ' '
d = 'D' if isEmpty(playerX + 1, playerY + 0, board, robots) else ' '
c = 'C' if isEmpty(playerX + 1, playerY + 1, board, robots) else ' '
x = 'X' if isEmpty(playerX + 0, playerY + 1, board, robots) else ' '
z = 'Z' if isEmpty(playerX - 1, playerY + 1, board, robots) else ' '
a = 'A' if isEmpty(playerX - 1, playerY + 0, board, robots) else ' '
allMoves = (q + w + e + d + c + x + a + z + 'S')
while True:
# Get player's move:
print('(T)eleports remaining: {}'.format(board["teleports"]))
print(' ({}) ({}) ({})'.format(q, w, e))
print(' ({}) (S) ({})'.format(a, d))
print('Enter move or QUIT: ({}) ({}) ({})'.format(z, x, c))
move = input('> ').upper()
if move == 'QUIT':
print('Thanks for playing!')
sys.exit()
elif move == 'T' and board['teleports'] > 0:
# Teleport the player to a random empty space:
board['teleports'] -= 1
return getRandomEmptySpace(board, robots)
elif move != '' and move in allMoves:
# Return the new player position based on their move:
return {'Q': (playerX - 1, playerY - 1),
'W': (playerX + 0, playerY - 1),
'E': (playerX + 1, playerY - 1),
'D': (playerX + 1, playerY + 0),
'C': (playerX + 1, playerY + 1),
'X': (playerX + 0, playerY + 1),
'Z': (playerX - 1, playerY + 1),
'A': (playerX - 1, playerY + 0),
'S': (playerX, playerY)}[move]
def moveRobots(board, robotPositions, playerPosition):
"""Return a list of (x, y) tuples of new robot positions after they
have tried to move toward the player."""
playerx, playery = playerPosition
nextRobotPositions = []
while len(robotPositions) > 0:
robotx, roboty = robotPositions[0]
# Determine the direction the robot moves.
if robotx < playerx:
movex = 1 # Move right.
elif robotx > playerx:
movex = -1 # Move left.
elif robotx == playerx:
movex = 0 # Don't move horizontally.
if roboty < playery:
movey = 1 # Move up.
elif roboty > playery:
movey = -1 # Move down.
elif roboty == playery:
movey = 0 # Don't move vertically.
# Check if the robot would run into a wall, and adjust course:
if board[(robotx + movex, roboty + movey)] == WALL:
# Robot would run into a wall, so come up with a new move:
if board[(robotx + movex, roboty)] == EMPTY_SPACE:
movey = 0 # Robot can't move horizontally.
elif board[(robotx, roboty + movey)] == EMPTY_SPACE:
movex = 0 # Robot can't move vertically.
else:
# Robot can't move.
movex = 0
movey = 0
newRobotx = robotx + movex
newRoboty = roboty + movey
if (board[(robotx, roboty)] == DEAD_ROBOT
or board[(newRobotx, newRoboty)] == DEAD_ROBOT):
# Robot is at a crash site, remove it.
del robotPositions[0]
continue
# Check if it moves into a robot, then destroy both robots:
if (newRobotx, newRoboty) in nextRobotPositions:
board[(newRobotx, newRoboty)] = DEAD_ROBOT
nextRobotPositions.remove((newRobotx, newRoboty))
else:
nextRobotPositions.append((newRobotx, newRoboty))
# Remove robots from robotPositions as they move.
del robotPositions[0]
return nextRobotPositions
# If this program was run (instead of imported), run the game:
if __name__ == '__main__':
main()
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 创造两种不同的机器人:只能沿对角线移动的机器人和只能沿基本方向移动的机器人。
- 给玩家一定数量的陷阱,他们可以留下来阻止任何机器人踩到陷阱。
- 给玩家有限数量的“瞬间墙”,他们可以建立自己的防御。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 22 行的
WALL = chr(9617)
改成WALL = 'R'
会怎么样? - 如果把 237 行的
return nextRobotPositions
改成return robotPositions
会怎么样? - 如果删除或注释掉第 44 行的
displayBoard(board, robots, playerPosition)
会发生什么? - 如果删除或注释掉第 53 行的
robots = moveRobots(board, robots, playerPosition)
会发生什么?
三十八、我控诉
你就是举世闻名的大侦探玛蒂尔德·加缪。猫佐菲不见了,你必须筛选线索。嫌疑人要么总是说谎,要么总是说真话。你会及时找到猫佐菲并指控有罪的一方吗?
在这个游戏中,你乘出租车到城市的不同地点。每个地方都有一个嫌疑犯和一件物品。可以向嫌疑人询问其他嫌疑人和物品的情况,将他们的回答与自己的探索笔记进行对比,确定他们是在说谎还是在说真话。有些人会知道谁绑架了佐菲(或者她在哪里,或者在绑架者的位置发现了什么物品),但是你必须确定你是否能相信他们。你有五分钟时间找到罪犯,但是如果你三次指控错误,你就输了。这款游戏的灵感来源于 Homestar Runner 的“鸡蛋在哪里?”游戏。
运行示例
当您运行jaccuse.py
时,输出将如下所示:
J'ACCUSE! (a mystery game)
`--snip--`
Time left: 5 min, 0 sec
You are in your TAXI. Where do you want to go?
(A)LBINO ALLIGATOR PIT
(B)OWLING ALLEY
(C)ITY HALL
(D)UCK POND
(H)IPSTER CAFE
(O)LD BARN
(U)NIVERSITY LIBRARY
(V)IDEO GAME MUSEUM
(Z)OO
> a
Time left: 4 min, 48 sec
You are at the ALBINO ALLIGATOR PIT.
ESPRESSA TOFFEEPOT with the ONE COWBOY BOOT is here.
(J) "J'ACCUSE!" (3 accusations left)
(Z) Ask if they know where ZOPHIE THE CAT is.
(T) Go back to the TAXI.
(1) Ask about ESPRESSA TOFFEEPOT
(2) Ask about ONE COWBOY BOOT
> z
They give you this clue: "DUKE HAUTDOG"
Press Enter to continue...
`--snip--`
工作原理
要完全理解这个程序,您应该密切关注clues
字典,它位于第 51 行到第 109 行。您可以取消对第 151 到 154 行的注释,以便在屏幕上显示它。这个字典有来自SUSPECTS
列表的字符串作为键,有“线索字典”作为值。每个线索字典都包含来自SUSPECTS
和ITEMS
的字符串。当被问及另一个嫌疑人或物品时,最初的嫌疑人会用这些字符串回答。例如,如果clues['DUKE HAUTDOG']['CANDLESTICK']
设定为'DUCK POND'
,那么当玩家向杜克·豪特多格询问烛台时,他们会说它在鸭塘。每次玩游戏时,嫌疑人、物品、地点和罪犯都会被洗牌。
这个程序的代码围绕着这个数据结构,所以理解它对于理解程序的其余部分是必要的。
"""J'ACCUSE!, by Al Sweigart email@protected
A mystery game of intrigue and a missing cat.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: extra-large, game, humor, puzzle"""
# Play the original Flash game at:
# https://homestarrunner.com/videlectrix/wheresanegg.html
# More info at: http://www.hrwiki.org/wiki/Where's_an_Egg%3F
import time, random, sys
# Set up the constants:
SUSPECTS = ['DUKE HAUTDOG', 'MAXIMUM POWERS', 'BILL MONOPOLIS', 'SENATOR SCHMEAR', 'MRS. FEATHERTOSS', 'DR. JEAN SPLICER', 'RAFFLES THE CLOWN', 'ESPRESSA TOFFEEPOT', 'CECIL EDGAR VANDERTON']
ITEMS = ['FLASHLIGHT', 'CANDLESTICK', 'RAINBOW FLAG', 'HAMSTER WHEEL', 'ANIME VHS TAPE', 'JAR OF PICKLES', 'ONE COWBOY BOOT', 'CLEAN UNDERPANTS', '5 DOLLAR GIFT CARD']
PLACES = ['ZOO', 'OLD BARN', 'DUCK POND', 'CITY HALL', 'HIPSTER CAFE', 'BOWLING ALLEY', 'VIDEO GAME MUSEUM', 'UNIVERSITY LIBRARY', 'ALBINO ALLIGATOR PIT']
TIME_TO_SOLVE = 300 # 300 seconds (5 minutes) to solve the game.
# First letters and longest length of places are needed for menu display:
PLACE_FIRST_LETTERS = {}
LONGEST_PLACE_NAME_LENGTH = 0
for place in PLACES:
PLACE_FIRST_LETTERS[place[0]] = place
if len(place) > LONGEST_PLACE_NAME_LENGTH:
LONGEST_PLACE_NAME_LENGTH = len(place)
# Basic sanity checks of the constants:
assert len(SUSPECTS) == 9
assert len(ITEMS) == 9
assert len(PLACES) == 9
# First letters must be unique:
assert len(PLACE_FIRST_LETTERS.keys()) == len(PLACES)
knownSuspectsAndItems = []
# visitedPlaces: Keys=places, values=strings of the suspect & item there.
visitedPlaces = {}
currentLocation = 'TAXI' # Start the game at the taxi.
accusedSuspects = [] # Accused suspects won't offer clues.
liars = random.sample(SUSPECTS, random.randint(3, 4))
accusationsLeft = 3 # You can accuse up to 3 people.
culprit = random.choice(SUSPECTS)
# Common indexes link these; e.g. SUSPECTS[0] and ITEMS[0] are at PLACES[0].
random.shuffle(SUSPECTS)
random.shuffle(ITEMS)
random.shuffle(PLACES)
# Create data structures for clues the truth-tellers give about each
# item and suspect.
# clues: Keys=suspects being asked for a clue, value="clue dictionary".
clues = {}
for i, interviewee in enumerate(SUSPECTS):
if interviewee in liars:
continue # Skip the liars for now.
# This "clue dictionary" has keys=items & suspects,
# value=the clue given.
clues[interviewee] = {}
clues[interviewee]['debug_liar'] = False # Useful for debugging.
for item in ITEMS: # Select clue about each item.
if random.randint(0, 1) == 0: # Tells where the item is:
clues[interviewee][item] = PLACES[ITEMS.index(item)]
else: # Tells who has the item:
clues[interviewee][item] = SUSPECTS[ITEMS.index(item)]
for suspect in SUSPECTS: # Select clue about each suspect.
if random.randint(0, 1) == 0: # Tells where the suspect is:
clues[interviewee][suspect] = PLACES[SUSPECTS.index(suspect)]
else: # Tells what item the suspect has:
clues[interviewee][suspect] = ITEMS[SUSPECTS.index(suspect)]
# Create data structures for clues the liars give about each item
# and suspect:
for i, interviewee in enumerate(SUSPECTS):
if interviewee not in liars:
continue # We've already handled the truth-tellers.
# This "clue dictionary" has keys=items & suspects,
# value=the clue given:
clues[interviewee] = {}
clues[interviewee]['debug_liar'] = True # Useful for debugging.
# This interviewee is a liar and gives wrong clues:
for item in ITEMS:
if random.randint(0, 1) == 0:
while True: # Select a random (wrong) place clue.
# Lies about where the item is.
clues[interviewee][item] = random.choice(PLACES)
if clues[interviewee][item] != PLACES[ITEMS.index(item)]:
# Break out of the loop when wrong clue is selected.
break
else:
while True: # Select a random (wrong) suspect clue.
clues[interviewee][item] = random.choice(SUSPECTS)
if clues[interviewee][item] != SUSPECTS[ITEMS.index(item)]:
# Break out of the loop when wrong clue is selected.
break
for suspect in SUSPECTS:
if random.randint(0, 1) == 0:
while True: # Select a random (wrong) place clue.
clues[interviewee][suspect] = random.choice(PLACES)
if clues[interviewee][suspect] != PLACES[ITEMS.index(item)]:
# Break out of the loop when wrong clue is selected.
break
else:
while True: # Select a random (wrong) item clue.
clues[interviewee][suspect] = random.choice(ITEMS)
if clues[interviewee][suspect] != ITEMS[SUSPECTS.index(suspect)]:
# Break out of the loop when wrong clue is selected.
break
# Create the data structures for clues given when asked about Zophie:
zophieClues = {}
for interviewee in random.sample(SUSPECTS, random.randint(3, 4)):
kindOfClue = random.randint(1, 3)
if kindOfClue == 1:
if interviewee not in liars:
# They tell you who has Zophie.
zophieClues[interviewee] = culprit
elif interviewee in liars:
while True:
# Select a (wrong) suspect clue.
zophieClues[interviewee] = random.choice(SUSPECTS)
if zophieClues[interviewee] != culprit:
# Break out of the loop when wrong clue is selected.
break
elif kindOfClue == 2:
if interviewee not in liars:
# They tell you where Zophie is.
zophieClues[interviewee] = PLACES[SUSPECTS.index(culprit)]
elif interviewee in liars:
while True:
# Select a (wrong) place clue.
zophieClues[interviewee] = random.choice(PLACES)
if zophieClues[interviewee] != PLACES[SUSPECTS.index(culprit)]:
# Break out of the loop when wrong clue is selected.
break
elif kindOfClue == 3:
if interviewee not in liars:
# They tell you what item Zophie is near.
zophieClues[interviewee] = ITEMS[SUSPECTS.index(culprit)]
elif interviewee in liars:
while True:
# Select a (wrong) item clue.
zophieClues[interviewee] = random.choice(ITEMS)
if zophieClues[interviewee] != ITEMS[SUSPECTS.index(culprit)]:
# Break out of the loop when wrong clue is selected.
break
# EXPERIMENT: Uncomment this code to view the clue data structures:
#import pprint
#pprint.pprint(clues)
#pprint.pprint(zophieClues)
#print('culprit =', culprit)
# START OF THE GAME
print("""J'ACCUSE! (a mystery game)")
By Al Sweigart email@protected
Inspired by Homestar Runner\'s "Where\'s an Egg?" game
You are the world-famous detective, Mathilde Camus.
ZOPHIE THE CAT has gone missing, and you must sift through the clues.
Suspects either always tell lies, or always tell the truth. Ask them
about other people, places, and items to see if the details they give are
truthful and consistent with your observations. Then you will know if
their clue about ZOPHIE THE CAT is true or not. Will you find ZOPHIE THE
CAT in time and accuse the guilty party?
""")
input('Press Enter to begin...')
startTime = time.time()
endTime = startTime + TIME_TO_SOLVE
while True: # Main game loop.
if time.time() > endTime or accusationsLeft == 0:
# Handle "game over" condition:
if time.time() > endTime:
print('You have run out of time!')
elif accusationsLeft == 0:
print('You have accused too many innocent people!')
culpritIndex = SUSPECTS.index(culprit)
print('It was {} at the {} with the {} who catnapped her!'.format(culprit, PLACES[culpritIndex], ITEMS[culpritIndex]))
print('Better luck next time, Detective.')
sys.exit()
print()
minutesLeft = int(endTime - time.time()) // 60
secondsLeft = int(endTime - time.time()) % 60
print('Time left: {} min, {} sec'.format(minutesLeft, secondsLeft))
if currentLocation == 'TAXI':
print(' You are in your TAXI. Where do you want to go?')
for place in sorted(PLACES):
placeInfo = ''
if place in visitedPlaces:
placeInfo = visitedPlaces[place]
nameLabel = '(' + place[0] + ')' + place[1:]
spacing = " " * (LONGEST_PLACE_NAME_LENGTH - len(place))
print('{} {}{}'.format(nameLabel, spacing, placeInfo))
print('(Q)UIT GAME')
while True: # Keep asking until a valid response is given.
response = input('> ').upper()
if response == '':
continue # Ask again.
if response == 'Q':
print('Thanks for playing!')
sys.exit()
if response in PLACE_FIRST_LETTERS.keys():
break
currentLocation = PLACE_FIRST_LETTERS[response]
continue # Go back to the start of the main game loop.
# At a place; player can ask for clues.
print(' You are at the {}.'.format(currentLocation))
currentLocationIndex = PLACES.index(currentLocation)
thePersonHere = SUSPECTS[currentLocationIndex]
theItemHere = ITEMS[currentLocationIndex]
print(' {} with the {} is here.'.format(thePersonHere, theItemHere))
# Add the suspect and item at this place to our list of known
# suspects and items:
if thePersonHere not in knownSuspectsAndItems:
knownSuspectsAndItems.append(thePersonHere)
if ITEMS[currentLocationIndex] not in knownSuspectsAndItems:
knownSuspectsAndItems.append(ITEMS[currentLocationIndex])
if currentLocation not in visitedPlaces.keys():
visitedPlaces[currentLocation] = '({}, {})'.format(thePersonHere.lower(), theItemHere.lower())
# If the player has accused this person wrongly before, they
# won't give clues:
if thePersonHere in accusedSuspects:
print('They are offended that you accused them,')
print('and will not help with your investigation.')
print('You go back to your TAXI.')
print()
input('Press Enter to continue...')
currentLocation = 'TAXI'
continue # Go back to the start of the main game loop.
# Display menu of known suspects & items to ask about:
print()
print('(J) "J\'ACCUSE!" ({} accusations left)'.format(accusationsLeft))
print('(Z) Ask if they know where ZOPHIE THE CAT is.')
print('(T) Go back to the TAXI.')
for i, suspectOrItem in enumerate(knownSuspectsAndItems):
print('({}) Ask about {}'.format(i + 1, suspectOrItem))
while True: # Keep asking until a valid response is given.
response = input('> ').upper()
if response in 'JZT' or (response.isdecimal() and 0 < int(response) <= len(knownSuspectsAndItems)):
break
if response == 'J': # Player accuses this suspect.
accusationsLeft -= 1 # Use up an accusation.
if thePersonHere == culprit:
# You've accused the correct suspect.
print('You\'ve cracked the case, Detective!')
print('It was {} who had catnapped ZOPHIE THE CAT.'.format(culprit))
minutesTaken = int(time.time() - startTime) // 60
secondsTaken = int(time.time() - startTime) % 60
print('Good job! You solved it in {} min, {} sec.'.format(minutesTaken, secondsTaken))
sys.exit()
else:
# You've accused the wrong suspect.
accusedSuspects.append(thePersonHere)
print('You have accused the wrong person, Detective!')
print('They will not help you with anymore clues.')
print('You go back to your TAXI.')
currentLocation = 'TAXI'
elif response == 'Z': # Player asks about Zophie.
if thePersonHere not in zophieClues:
print('"I don\'t know anything about ZOPHIE THE CAT."')
elif thePersonHere in zophieClues:
print(' They give you this clue: "{}"'.format(zophieClues[thePersonHere]))
# Add non-place clues to the list of known things:
if zophieClues[thePersonHere] not in knownSuspectsAndItems and zophieClues[thePersonHere] not in PLACES:
knownSuspectsAndItems.append(zophieClues[thePersonHere])
elif response == 'T': # Player goes back to the taxi.
currentLocation = 'TAXI'
continue # Go back to the start of the main game loop.
else: # Player asks about a suspect or item.
thingBeingAskedAbout = knownSuspectsAndItems[int(response) - 1]
if thingBeingAskedAbout in (thePersonHere, theItemHere):
print(' They give you this clue: "No comment."')
else:
print(' They give you this clue: "{}"'.format(clues[thePersonHere][thingBeingAskedAbout]))
# Add non-place clues to the list of known things:
if clues[thePersonHere][thingBeingAskedAbout] not in knownSuspectsAndItems and clues[thePersonHere][thingBeingAskedAbout] not in PLACES:
knownSuspectsAndItems.append(clues[thePersonHere][thingBeingAskedAbout])
input('Press Enter to continue...')
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 16 行的
TIME_TO_SOLVE = 300
改成TIME_TO_SOLVE = 0
会怎么样? - 如果把 176 行的
time.time() > endTime or accusationsLeft == 0
改成time.time() > endTime and accusationsLeft == 0
会怎么样? - 如果把 198 行的
place[1:]
改成place
会怎么样? - 如果把 173 行的
startTime + TIME_TO_SOLVE
改成startTime * TIME_TO_SOLVE
会怎么样?
三十九、兰顿的蚂蚁
兰顿的蚂蚁是二维网格上的元胞自动机模拟,类似于项目 13“康威的生命游戏”。在模拟中,一只“蚂蚁”从两种颜色之一的正方形开始。如果空间是第一种颜色,蚂蚁将它切换到第二种颜色,向右旋转 90 度,并向前移动一个空间。如果空间是第二种颜色,蚂蚁将它切换到第一种颜色,向左旋转 90 度,并向前移动一个空间。尽管规则非常简单,但模拟显示了复杂的突发行为。模拟可以在同一个空间中展示多只蚂蚁,当它们彼此相遇时,会产生有趣的互动。兰顿的蚂蚁是计算机科学家克里斯·兰顿在 1986 年发明的。更多关于兰顿蚂蚁的信息可以在en.wikipedia.org/wiki/Langton%27s_ant
找到。
运行示例
图 39-1 显示了运行langtonsant.py
时的输出。
:兰顿蚂蚁细胞自动机的催眠输出
工作原理
这个程序使用了两种“方向”的含义一方面,代表每只蚂蚁的字典存储了基本方向:北、南、东、西。然而,向左或向右(或逆时针和顺时针,因为我们是从上面看蚂蚁)是一个旋转方向。蚂蚁应该根据它们所站的瓷砖向左转或向右转,所以第 78 到 100 行根据蚂蚁当前的基本方向和它们转向的方向设置了一个新的基本方向。
"""Langton's Ant, by Al Sweigart email@protected
A cellular automata animation. Press Ctrl-C to stop.
More info: https://en.wikipedia.org/wiki/Langton%27s_ant
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, artistic, bext, simulation"""
import copy, 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
HEIGHT -= 1 # Adjustment for the quit message at the bottom.
NUMBER_OF_ANTS = 10 # (!) Try changing this to 1 or 50.
PAUSE_AMOUNT = 0.1 # (!) Try changing this to 1.0 or 0.0.
# (!) Try changing these to make the ants look different:
ANT_UP = '^'
ANT_DOWN = 'v'
ANT_LEFT = '<'
ANT_RIGHT = '>'
# (!) Try changing these colors to one of 'black', 'red', 'green',
# 'yellow', 'blue', 'purple', 'cyan', or 'white'. (These are the only
# colors that the bext module supports.)
ANT_COLOR = 'red'
BLACK_TILE = 'black'
WHITE_TILE = 'white'
NORTH = 'north'
SOUTH = 'south'
EAST = 'east'
WEST = 'west'
def main():
bext.fg(ANT_COLOR) # The ants' color is the foreground color.
bext.bg(WHITE_TILE) # Set the background to white to start.
bext.clear()
# Create a new board data structure:
board = {'width': WIDTH, 'height': HEIGHT}
# Create ant data structures:
ants = []
for i in range(NUMBER_OF_ANTS):
ant = {
'x': random.randint(0, WIDTH - 1),
'y': random.randint(0, HEIGHT - 1),
'direction': random.choice([NORTH, SOUTH, EAST, WEST]),
}
ants.append(ant)
# Keep track of which tiles have changed and need to be redrawn on
# the screen:
changedTiles = []
while True: # Main program loop.
displayBoard(board, ants, changedTiles)
changedTiles = []
# nextBoard is what the board will look like on the next step in
# the simulation. Start with a copy of the current step's board:
nextBoard = copy.copy(board)
# Run a single simulation step for each ant:
for ant in ants:
if board.get((ant['x'], ant['y']), False) == True:
nextBoard[(ant['x'], ant['y'])] = False
# Turn clockwise:
if ant['direction'] == NORTH:
ant['direction'] = EAST
elif ant['direction'] == EAST:
ant['direction'] = SOUTH
elif ant['direction'] == SOUTH:
ant['direction'] = WEST
elif ant['direction'] == WEST:
ant['direction'] = NORTH
else:
nextBoard[(ant['x'], ant['y'])] = True
# Turn counter clockwise:
if ant['direction'] == NORTH:
ant['direction'] = WEST
elif ant['direction'] == WEST:
ant['direction'] = SOUTH
elif ant['direction'] == SOUTH:
ant['direction'] = EAST
elif ant['direction'] == EAST:
ant['direction'] = NORTH
changedTiles.append((ant['x'], ant['y']))
# Move the ant forward in whatever direction it's facing:
if ant['direction'] == NORTH:
ant['y'] -= 1
if ant['direction'] == SOUTH:
ant['y'] += 1
if ant['direction'] == WEST:
ant['x'] -= 1
if ant['direction'] == EAST:
ant['x'] += 1
# If the ant goes past the edge of the screen,
# it should wrap around to other side.
ant['x'] = ant['x'] % WIDTH
ant['y'] = ant['y'] % HEIGHT
changedTiles.append((ant['x'], ant['y']))
board = nextBoard
def displayBoard(board, ants, changedTiles):
"""Displays the board and ants on the screen. The changedTiles
argument is a list of (x, y) tuples for tiles on the screen that
have changed and need to be redrawn."""
# Draw the board data structure:
for x, y in changedTiles:
bext.goto(x, y)
if board.get((x, y), False):
bext.bg(BLACK_TILE)
else:
bext.bg(WHITE_TILE)
antIsHere = False
for ant in ants:
if (x, y) == (ant['x'], ant['y']):
antIsHere = True
if ant['direction'] == NORTH:
print(ANT_UP, end='')
elif ant['direction'] == SOUTH:
print(ANT_DOWN, end='')
elif ant['direction'] == EAST:
print(ANT_LEFT, end='')
elif ant['direction'] == WEST:
print(ANT_RIGHT, end='')
break
if not antIsHere:
print(' ', end='')
# Display the quit message at the bottom of the screen:
bext.goto(0, HEIGHT)
bext.bg(WHITE_TILE)
print('Press Ctrl-C to quit.', end='')
sys.stdout.flush() # (Required for bext-using programs.)
time.sleep(PAUSE_AMOUNT)
# If this program was run (instead of imported), run the game:
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("Langton's Ant, by Al Sweigart email@protected")
sys.exit() # When Ctrl-C is pressed, end the program.
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。你也可以自己想办法做到以下几点:
- 让玩家从文本文件中加载并保存棋盘的状态。
- 使用新的移动规则创建额外的平铺状态,看看会出现什么行为。
- 为兰顿的蚂蚁实现维基百科文章中建议的一些想法。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 149 行的
print(' ', end='')
改成print('.', end='')
会怎么样? - 如果把 106 行的
ant['y'] += 1
改成ant['y'] -= 1
会怎么样? - 如果把第 79 行的
nextBoard[(ant['x'], ant['y'])] = False
改成nextBoard[(ant['x'], ant['y'])] = True
会怎么样? - 如果把第 21 行的
WIDTH -= 1
改成WIDTH -= 40
会怎么样? - 如果把 119 行的
board = nextBoard
改成board = board
会怎么样?
四十、黑客语
没有比用数字替换文本中的字母更好的方法来展示您疯狂的黑客技能了:m4d h4x0r 5k1llz
!!!这个单词程序自动将普通英语转换成黑客语,这是最酷的在线交谈方式。或者至少是在 1993 年。
这需要一段时间来适应,但经过一些练习,你最终会流利地阅读黑客语。比如1t email@protected]<3s 4 w|-|1le +o g37 |_|s3|) 70, b|_|+ y0u (an 3\/3nt|_|/-\lly r3a|) l33t$peak phl|_|3n+ly
。黑客语可能一开始很难读懂,但程序本身很简单,对初学者来说很好。更多关于黑客语的信息可以在en.wikipedia.org/wiki/Leet
找到。
运行示例
当您运行leetspeak.py
时,输出将如下所示:
L3375P34]< (leetspeek)
By Al Sweigart email@protected
Enter your leet message:
> I am a leet hacker. Fear my mad skills. The 90s were over two decades ago.
! @m a l33t email@protected(]<er. email@protected my m4|) $k|ll$. +h3 90s w3r3 0ver tw0 d3(ad3$ 4g0.
(Copied leetspeak to clipboard.)
工作原理
第 36 行的charMapping
变量中的字典将普通英语字符映射到黑客语字符。然而,由于可能有多个可能的黑客语字符(例如字母't'
的'7'
或'+'
),charMapping
字典中的每个值都是一个字符串列表。当创建新的黑客语字符串时,程序有 30%的机会简单地使用原始英文消息中的字符,有 70%的机会使用黑客语字符之一。这意味着同一个英语信息有多种可能的翻译。
"""Leetspeak, by Al Sweigart email@protected
Translates English messages into l33t5p34]<.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: tiny, beginner, word"""
import random
try:
import pyperclip # pyperclip copies text to the clipboard.
except ImportError:
pass # If pyperclip is not installed, do nothing. It's no big deal.
def main():
print('''L3375P34]< (leetspeek)
By Al Sweigart email@protected
Enter your leet message:''')
english = input('> ')
print()
leetspeak = englishToLeetspeak(english)
print(leetspeak)
try:
# Trying to use pyperclip will raise a NameError exception if
# it wasn't imported:
pyperclip.copy(leetspeak)
print('(Copied leetspeak to clipboard.)')
except NameError:
pass # Do nothing if pyperclip wasn't installed.
def englishToLeetspeak(message):
"""Convert the English string in message and return leetspeak."""
# Make sure all the keys in `charMapping` are lowercase.
charMapping = {
'a': ['4', '@', '/-\\'], 'c': ['('], 'd': ['|)'], 'e': ['3'],
'f': ['ph'], 'h': [']-[', '|-|'], 'i': ['1', '!', '|'], 'k': [']<'],
'o': ['0'], 's': ['$', '5'], 't': ['7', '+'], 'u': ['|_|'],
'v': ['\\/']}
leetspeak = ''
for char in message: # Check each character:
# There is a 70% chance we change the character to leetspeak.
if char.lower() in charMapping and random.random() <= 0.70:
possibleLeetReplacements = charMapping[char.lower()]
leetReplacement = random.choice(possibleLeetReplacements)
leetspeak = leetspeak + leetReplacement
else:
# Don't translate this character:
leetspeak = leetspeak + char
return leetspeak
# If this program was run (instead of imported), run the game:
if __name__ == '__main__':
main()
在输入源代码并运行几次之后,尝试对其进行实验性的修改。你也可以自己想办法做到以下几点:
- 修改
charMapping
字典,使其支持新的黑客语字符。 - 添加一个功能,可以将黑客语转换回简单的英语。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 51 行的
return leetspeak
改成return message
会怎么样? - 如果把第 44 行的
char.lower()
改成char
会怎么样? - 如果把第 44 行的
char.lower()
改成char.upper()
会怎么样? - 如果把第 47 行的
leetspeak = leetspeak + leetReplacement
改成leetspeak = leetReplacement
会怎么样?