四十一、幸运星
在这个碰运气游戏中,你掷骰子来收集星星。你掷得越多,你能得到的星星就越多,但是如果你得到三个头骨,你就失去了一切!这款快速多人游戏可以支持任意多的玩家,是聚会的理想选择。
在你的回合中,你从骰盅中随机抽取三个骰子并掷出它们。你可以滚动星星,头骨和问号。如果你结束你的回合,你每颗星得一分。如果你选择再次掷骰子,你保留问号,并重新掷骰子来代替星星和头骨。如果你收集到三个头骨,你将失去所有的星星并结束你的回合。
当一个玩家得到 13 分时,在游戏结束前,其他人都有一次机会。谁得分最多谁就赢。
杯子里有六个金骰子、四个银骰子和三个铜骰子。金骰子星星多,铜骰子骷髅头多,银是偶数。
运行示例
当您运行luckystars.py
时,输出将如下所示:
Lucky Stars, by Al Sweigart email@protected
`--snip--`
SCORES: Alice=0, Bob=0
It is Alice's turn.
+-----------+ +-----------+ +-----------+
| | | . | | |
| | | ,O, | | |
| ? | | 'ooOOOoo' | | ? |
| | | `OOO` | | |
| | | O' 'O | | |
+-----------+ +-----------+ +-----------+
GOLD GOLD BRONZE
Stars collected: 1 Skulls collected: 0
Do you want to roll again? Y/N
> y
+-----------+ +-----------+ +-----------+
| . | | ___ | | |
| ,O, | | / \ | | |
| 'ooOOOoo' | | |() ()| | | ? |
| `OOO` | | \ ^ / | | |
| O' 'O | | VVV | | |
+-----------+ +-----------+ +-----------+
GOLD BRONZE BRONZE
Stars collected: 2 Skulls collected: 1
Do you want to roll again? Y/N
`--snip--`
工作原理
该程序中基于文本的图形作为字符串存储在STAR_FACE
、SKULL_FACE
和QUESTION_FACE
变量的列表中。这种格式使它们易于在代码编辑器中编写,而第 154 到 157 行中的代码将它们显示在屏幕上。注意,因为三个骰子显示在一起,所以这段代码必须一次在骰子面上打印每一行水平文本。简单地运行像print(STAR_FACE)
这样的代码会导致三个骰子出现在彼此之上,而不是并排。
"""Lucky Stars, by Al Sweigart email@protected
A "press your luck" game where you roll dice to gather as many stars
as possible. You can roll as many times as you want, but if you roll
three skulls you lose all your stars.
Inspired by the Zombie Dice game from Steve Jackson Games.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, game, multiplayer"""
import random
# Set up the constants:
GOLD = 'GOLD'
SILVER = 'SILVER'
BRONZE = 'BRONZE'
STAR_FACE = ["+-----------+",
"| . |",
"| ,O, |",
"| 'ooOOOoo' |",
"| `OOO` |",
"| O' 'O |",
"+-----------+"]
SKULL_FACE = ['+-----------+',
'| ___ |',
'| / \\ |',
'| |() ()| |',
'| \\ ^ / |',
'| VVV |',
'+-----------+']
QUESTION_FACE = ['+-----------+',
'| |',
'| |',
'| ? |',
'| |',
'| |',
'+-----------+']
FACE_WIDTH = 13
FACE_HEIGHT = 7
print("""Lucky Stars, by Al Sweigart email@protected
A "press your luck" game where you roll dice with Stars, Skulls, and
Question Marks.
On your turn, you pull three random dice from the dice cup and roll
them. You can roll Stars, Skulls, and Question Marks. You can end your
turn and get one point per Star. If you choose to roll again, you keep
the Question Marks and pull new dice to replace the Stars and Skulls.
If you collect three Skulls, you lose all your Stars and end your turn.
When a player gets 13 points, everyone else gets one more turn before
the game ends. Whoever has the most points wins.
There are 6 Gold dice, 4 Silver dice, and 3 Bronze dice in the cup.
Gold dice have more Stars, Bronze dice have more Skulls, and Silver is
even.
""")
print('How many players are there?')
while True: # Loop until the user enters a number.
response = input('> ')
if response.isdecimal() and int(response) > 1:
numPlayers = int(response)
break
print('Please enter a number larger than 1.')
playerNames = [] # List of strings of player names.
playerScores = {} # Keys are player names, values are integer scores.
for i in range(numPlayers):
while True: # Keep looping until a name is entered.
print('What is player #' + str(i + 1) + '\'s name?')
response = input('> ')
if response != '' and response not in playerNames:
playerNames.append(response)
playerScores[response] = 0
break
print('Please enter a name.')
print()
turn = 0 # The player at playerNames[0] will go first.
# (!) Uncomment to let a player named 'Al' start with three points:
#playerScores['Al'] = 3
endGameWith = None
while True: # Main game loop.
# Display everyone's score:
print()
print('SCORES: ', end='')
for i, name in enumerate(playerNames):
print(name + ' = ' + str(playerScores[name]), end='')
if i != len(playerNames) - 1:
# All but the last player have commas separating their names.
print(', ', end='')
print('\n')
# Start the number of collected stars and skulls at 0.
stars = 0
skulls = 0
# A cup has 6 gold, 4 silver, and 3 bronze dice:
cup = ([GOLD] * 6) + ([SILVER] * 4) + ([BRONZE] * 3)
hand = [] # Your hand starts with no dice.
print('It is ' + playerNames[turn] + '\'s turn.')
while True: # Each iteration of this loop is rolling the dice.
print()
# Check that there's enough dice left in the cup:
if (3 - len(hand)) > len(cup):
# End this turn because there are not enough dice:
print('There aren\'t enough dice left in the cup to '
+ 'continue ' + playerNames[turn] + '\'s turn.')
break
# Pull dice from the cup until you have 3 in your hand:
random.shuffle(cup) # Shuffle the dice in the cup.
while len(hand) < 3:
hand.append(cup.pop())
# Roll the dice:
rollResults = []
for dice in hand:
roll = random.randint(1, 6)
if dice == GOLD:
# Roll a gold die (3 stars, 2 questions, 1 skull):
if 1 <= roll <= 3:
rollResults.append(STAR_FACE)
stars += 1
elif 4 <= roll <= 5:
rollResults.append(QUESTION_FACE)
else:
rollResults.append(SKULL_FACE)
skulls += 1
if dice == SILVER:
# Roll a silver die (2 stars, 2 questions, 2 skulls):
if 1 <= roll <= 2:
rollResults.append(STAR_FACE)
stars += 1
elif 3 <= roll <= 4:
rollResults.append(QUESTION_FACE)
else:
rollResults.append(SKULL_FACE)
skulls += 1
if dice == BRONZE:
# Roll a bronze die (1 star, 2 questions, 3 skulls):
if roll == 1:
rollResults.append(STAR_FACE)
stars += 1
elif 2 <= roll <= 4:
rollResults.append(QUESTION_FACE)
else:
rollResults.append(SKULL_FACE)
skulls += 1
# Display roll results:
for lineNum in range(FACE_HEIGHT):
for diceNum in range(3):
print(rollResults[diceNum][lineNum] + ' ', end='')
print() # Print a newline.
# Display the type of dice each one is (gold, silver, bronze):
for diceType in hand:
print(diceType.center(FACE_WIDTH) + ' ', end='')
print() # Print a newline.
print('Stars collected:', stars, ' Skulls collected:', skulls)
# Check if they've collected 3 or more skulls:
if skulls >= 3:
print('3 or more skulls means you\'ve lost your stars!')
input('Press Enter to continue...')
break
print(playerNames[turn] + ', do you want to roll again? Y/N')
while True: # Keep asking the player until they enter Y or N:
response = input('> ').upper()
if response != '' and response[0] in ('Y', 'N'):
break
print('Please enter Yes or No.')
if response.startswith('N'):
print(playerNames[turn], 'got', stars, 'stars!')
# Add stars to this player's point total:
playerScores[playerNames[turn]] += stars
# Check if they've reached 13 or more points:
# (!) Try changing this to 5 or 50 points.
if (endGameWith == None
and playerScores[playerNames[turn]] >= 13):
# Since this player reached 13 points, play one more
# round for all other players:
print('\n\n' + ('!' * 60))
print(playerNames[turn] + ' has reached 13 points!!!')
print('Everyone else will get one more turn!')
print(('!' * 60) + '\n\n')
endGameWith = playerNames[turn]
input('Press Enter to continue...')
break
# Discard the stars and skulls, but keep the question marks:
nextHand = []
for i in range(3):
if rollResults[i] == QUESTION_FACE:
nextHand.append(hand[i]) # Keep the question marks.
hand = nextHand
# Move on to the next player's turn:
turn = (turn + 1) % numPlayers
# If the game has ended, break out of this loop:
if endGameWith == playerNames[turn]:
break # End the game.
print('The game has ended...')
# Display everyone's score:
print()
print('SCORES: ', end='')
for i, name in enumerate(playerNames):
print(name + ' = ' + str(playerScores[name]), end='')
if i != len(playerNames) - 1:
# All but the last player have commas separating their names.
print(', ', end='')
print('\n')
# Find out who the winners are:
highestScore = 0
winners = []
for name, score in playerScores.items():
if score > highestScore:
# This player has the highest score:
highestScore = score
winners = [name] # Overwrite any previous winners.
elif score == highestScore:
# This player is tied with the highest score.
winners.append(name)
if len(winners) == 1:
# There is only one winner:
print('The winner is ' + winners[0] + '!!!')
else:
# There are multiple tied winners:
print('The winners are: ' + ', '.join(winners))
print('Thanks for playing!')
在输入源代码并运行几次之后,尝试对其进行实验性的修改。标有(!)
的注释对你可以做的小改变有建议。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果删除或注释掉第 114 行的
random.shuffle(cup)
会发生什么? - 如果把 167 行的
skulls >= 3
改成skulls > 3
会怎么样? - 如果将第 206 行的
(turn + 1) % numPlayers
改为(turn + 1)
,会得到什么错误信息? - 如果把 84 行的
endGameWith = None
改成endGameWith = playerNames[0]
会怎么样? - 如果删除或注释掉第 170 行的
break
会怎么样? - 如果把第 76 行的
playerScores[response] = 0
改成playerScores[response] = 10
会怎么样?
四十二、魔术幸运球
魔术幸运球可以预测未来,并使用 Python 的随机数模块以 100%的准确率回答你的是/否问题。这个程序类似于一个魔术八球玩具,除了你不需要摇动它。它还具有缓慢打印文本字符串的功能,每个字符之间有空格,给消息一种怪异、神秘的效果。
大部分代码致力于设置诡异的气氛。程序本身简单地选择一个消息来显示,以响应一个随机数。
运行示例
当您运行magicfortuneball.py
时,输出将如下所示:
M A G i C F O R T U N E B A L L , B Y A L S W E i G A R T
A S K M E Y O U R Y E S / N O Q U E S T i O N .
> Isn't fortune telling just a scam to trick money out of gullible people?
L E T M E T H i N K O N T H i S . . .
. . . . . . . .
i H A V E A N A N S W E R . . .
A F F i R M A T i V E
工作原理
魔术幸运球实际上做的唯一事情是显示一个随机选择的字符串。完全忽略了用户的疑问。当然,第 28 行调用了input('> ')
,但是它没有在任何变量中存储返回值,因为程序实际上并没有使用这个文本。让用户输入他们的问题给他们一种感觉,这个程序有一种千里眼的光环。
slowSpacePrint()
函数显示大写文本,任何字母I
用小写,使消息看起来独特。该函数还在字符串的每个字符之间插入空格,然后缓慢显示,中间有停顿。一个程序不需要复杂到可以预测未来才有趣!
"""Magic Fortune Ball, by Al Sweigart email@protected
Ask a yes/no question about your future. Inspired by the Magic 8 Ball.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: tiny, beginner, humor"""
import random, time
def slowSpacePrint(text, interval=0.1):
"""Slowly display text with spaces in between each letter and
lowercase letter i's."""
for character in text:
if character == 'I':
# I's are displayed in lowercase for style:
print('i ', end='', flush=True)
else:
# All other characters are displayed normally:
print(character + ' ', end='', flush=True)
time.sleep(interval)
print() # Print two newlines at the end.
print()
# Prompt for a question:
slowSpacePrint('MAGIC FORTUNE BALL, BY AL SWEiGART')
time.sleep(0.5)
slowSpacePrint('ASK ME YOUR YES/NO QUESTION.')
input('> ')
# Display a brief reply:
replies = [
'LET ME THINK ON THIS...',
'AN INTERESTING QUESTION...',
'HMMM... ARE YOU SURE YOU WANT TO KNOW..?',
'DO YOU THINK SOME THINGS ARE BEST LEFT UNKNOWN..?',
'I MIGHT TELL YOU, BUT YOU MIGHT NOT LIKE THE ANSWER...',
'YES... NO... MAYBE... I WILL THINK ON IT...',
'AND WHAT WILL YOU DO WHEN YOU KNOW THE ANSWER? WE SHALL SEE...',
'I SHALL CONSULT MY VISIONS...',
'YOU MAY WANT TO SIT DOWN FOR THIS...',
]
slowSpacePrint(random.choice(replies))
# Dramatic pause:
slowSpacePrint('.' * random.randint(4, 12), 0.7)
# Give the answer:
slowSpacePrint('I HAVE AN ANSWER...', 0.2)
time.sleep(1)
answers = [
'YES, FOR SURE',
'MY ANSWER IS NO',
'ASK ME LATER',
'I AM PROGRAMMED TO SAY YES',
'THE STARS SAY YES, BUT I SAY NO',
'I DUNNO MAYBE',
'FOCUS AND ASK ONCE MORE',
'DOUBTFUL, VERY DOUBTFUL',
'AFFIRMATIVE',
'YES, THOUGH YOU MAY NOT LIKE IT',
'NO, BUT YOU MAY WISH IT WAS SO',
]
slowSpacePrint(random.choice(answers), 0.05)
在输入源代码并运行几次之后,尝试对其进行实验性的修改。你也可以自己想办法做到以下几点:
- 检查玩家的问题是否以问号结尾。
- 添加程序可以给出的其他答案。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把第 45 行的
random.randint(4, 12)
改成random.randint(4, 9999)
会怎么样? - 如果把第 49 行的
time.sleep(1)
改成time.sleep(-1)
,会得到什么错误?
四十三、曼卡拉
棋盘游戏曼卡拉至少有 2000 年的历史,几乎和 63 号项目“乌尔的皇家游戏”一样古老这是一种“播种”游戏,两名玩家选择几袋种子,撒在棋盘上的其他口袋里,同时尽可能多地收集他们商店里的种子。在不同的文化中,这种游戏有几种变体。这个名字来自阿拉伯语naqala
,意思是“移动”
玩的时候,从你这边的一个坑里抓种子,然后在每个坑里放一个,逆时针方向走,跳过你对手的仓库。如果你的最后一粒种子落在你的一个空坑里,将对面坑的种子移到那个坑里。如果最后放置的种子在你的商店里,你有一次免费机会。
当一个玩家的所有坑都空了时,游戏结束。另一名玩家声称剩余的种子属于他们的商店,获胜者是拥有最多种子的人。更多关于曼卡拉及其变种的信息可以在en.wikipedia.org/wiki/Mancala
找到。
运行示例
当您运行mancala.py
时,输出将如下所示:
Mancala, by Al Sweigart email@protected
`--snip--`
+------+------+--<<<<<-Player 2----+------+------+------+
2 |G |H |I |J |K |L | 1
| 4 | 4 | 4 | 4 | 4 | 4 |
S | | | | | | | S
T 0 +------+------+------+------+------+------+ 0 T
O |A |B |C |D |E |F | O
R | 4 | 4 | 4 | 4 | 4 | 4 | R
E | | | | | | | E
+------+------+------+-Player 1->>>>>-----+------+------+
Player 1, choose move: A-F (or QUIT)
> f
+------+------+--<<<<<-Player 2----+------+------+------+
2 |G |H |I |J |K |L | 1
| 4 | 4 | 4 | 5 | 5 | 5 |
S | | | | | | | S
T 0 +------+------+------+------+------+------+ 1 T
O |A |B |C |D |E |F | O
R | 4 | 4 | 4 | 4 | 4 | 0 | R
E | | | | | | | E
+------+------+------+-Player 1->>>>>-----+------+------+
Player 2, choose move: G-L (or QUIT)
`--snip--`
工作原理
Mancala 使用 ASCII 艺术画来显示棋盘。请注意,每个口袋不仅需要有种子的数量,还需要有一个标签。为了避免混淆,标签上使用了从A
到L
的字母,这样就不会被误认为是每个口袋里的种子数量。字典NEXT_PIT
和OPPOSITE_PIT
分别将一个口袋的字母映射到它旁边或对面的坑的字母。这使得表达式NEXT_PIT['A']
计算为'B'
,表达式OPPOSITE_PIT['A']
计算为'G'
。注意代码是如何使用这些字典的。没有它们,我们的 Mancala 程序将需要一长串的if
和elif
语句来执行相同的游戏步骤。
"""Mancala, by Al Sweigart email@protected
The ancient seed-sowing game.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, board game, game, two-player"""
import sys
# A tuple of the player's pits:
PLAYER_1_PITS = ('A', 'B', 'C', 'D', 'E', 'F')
PLAYER_2_PITS = ('G', 'H', 'I', 'J', 'K', 'L')
# A dictionary whose keys are pits and values are opposite pit:
OPPOSITE_PIT = {'A': 'G', 'B': 'H', 'C': 'I', 'D': 'J', 'E': 'K',
'F': 'L', 'G': 'A', 'H': 'B', 'I': 'C', 'J': 'D',
'K': 'E', 'L': 'F'}
# A dictionary whose keys are pits and values are the next pit in order:
NEXT_PIT = {'A': 'B', 'B': 'C', 'C': 'D', 'D': 'E', 'E': 'F', 'F': '1',
'1': 'L', 'L': 'K', 'K': 'J', 'J': 'I', 'I': 'H', 'H': 'G',
'G': '2', '2': 'A'}
# Every pit label, in counterclockwise order starting with A:
PIT_LABELS = 'ABCDEF1LKJIHG2'
# How many seeds are in each pit at the start of a new game:
STARTING_NUMBER_OF_SEEDS = 4 # (!) Try changing this to 1 or 10.
def main():
print('''Mancala, by Al Sweigart email@protected
The ancient two-player, seed-sowing game. Grab the seeds from a pit on
your side and place one in each following pit, going counterclockwise
and skipping your opponent's store. If your last seed lands in an empty
pit of yours, move the opposite pit's seeds into your store. The
goal is to get the most seeds in your store on the side of the board.
If the last placed seed is in your store, you get a free turn.
The game ends when all of one player's pits are empty. The other player
claims the remaining seeds for their store, and the winner is the one
with the most seeds.
More info at https://en.wikipedia.org/wiki/Mancala
''')
input('Press Enter to begin...')
gameBoard = getNewBoard()
playerTurn = '1' # Player 1 goes first.
while True: # Run a player's turn.
# "Clear" the screen by printing many newlines, so the old
# board isn't visible anymore.
print('\n' * 60)
# Display board and get the player's move:
displayBoard(gameBoard)
playerMove = askForPlayerMove(playerTurn, gameBoard)
# Carry out the player's move:
playerTurn = makeMove(gameBoard, playerTurn, playerMove)
# Check if the game ended and a player has won:
winner = checkForWinner(gameBoard)
if winner == '1' or winner == '2':
displayBoard(gameBoard) # Display the board one last time.
print('Player ' + winner + ' has won!')
sys.exit()
elif winner == 'tie':
displayBoard(gameBoard) # Display the board one last time.
print('There is a tie!')
sys.exit()
def getNewBoard():
"""Return a dictionary representing a Mancala board in the starting
state: 4 seeds in each pit and 0 in the stores."""
# Syntactic sugar - Use a shorter variable name:
s = STARTING_NUMBER_OF_SEEDS
# Create the data structure for the board, with 0 seeds in the
# stores and the starting number of seeds in the pits:
return {'1': 0, '2': 0, 'A': s, 'B': s, 'C': s, 'D': s, 'E': s,
'F': s, 'G': s, 'H': s, 'I': s, 'J': s, 'K': s, 'L': s}
def displayBoard(board):
"""Displays the game board as ASCII-art based on the board
dictionary."""
seedAmounts = []
# This 'GHIJKL21ABCDEF' string is the order of the pits left to
# right and top to bottom:
for pit in 'GHIJKL21ABCDEF':
numSeedsInThisPit = str(board[pit]).rjust(2)
seedAmounts.append(numSeedsInThisPit)
print("""
+------+------+--<<<<<-Player 2----+------+------+------+
2 |G |H |I |J |K |L | 1
| {} | {} | {} | {} | {} | {} |
S | | | | | | | S
T {} +------+------+------+------+------+------+ {} T
O |A |B |C |D |E |F | O
R | {} | {} | {} | {} | {} | {} | R
E | | | | | | | E
+------+------+------+-Player 1->>>>>-----+------+------+
""".format(*seedAmounts))
def askForPlayerMove(playerTurn, board):
"""Asks the player which pit on their side of the board they
select to sow seeds from. Returns the uppercase letter label of the
selected pit as a string."""
while True: # Keep asking the player until they enter a valid move.
# Ask the player to select a pit on their side:
if playerTurn == '1':
print('Player 1, choose move: A-F (or QUIT)')
elif playerTurn == '2':
print('Player 2, choose move: G-L (or QUIT)')
response = input('> ').upper().strip()
# Check if the player wants to quit:
if response == 'QUIT':
print('Thanks for playing!')
sys.exit()
# Make sure it is a valid pit to select:
if (playerTurn == '1' and response not in PLAYER_1_PITS) or (
playerTurn == '2' and response not in PLAYER_2_PITS
):
print('Please pick a letter on your side of the board.')
continue # Ask player again for their move.
if board.get(response) == 0:
print('Please pick a non-empty pit.')
continue # Ask player again for their move.
return response
def makeMove(board, playerTurn, pit):
"""Modify the board data structure so that the player 1 or 2 in
turn selected pit as their pit to sow seeds from. Returns either
'1' or '2' for whose turn it is next."""
seedsToSow = board[pit] # Get number of seeds from selected pit.
board[pit] = 0 # Empty out the selected pit.
while seedsToSow > 0: # Continue sowing until we have no more seeds.
pit = NEXT_PIT[pit] # Move on to the next pit.
if (playerTurn == '1' and pit == '2') or (
playerTurn == '2' and pit == '1'
):
continue # Skip opponent's store.
board[pit] += 1
seedsToSow -= 1
# If the last seed went into the player's store, they go again.
if (pit == playerTurn == '1') or (pit == playerTurn == '2'):
# The last seed landed in the player's store; take another turn.
return playerTurn
# Check if last seed was in an empty pit; take opposite pit's seeds.
if playerTurn == '1' and pit in PLAYER_1_PITS and board[pit] == 1:
oppositePit = OPPOSITE_PIT[pit]
board['1'] += board[oppositePit]
board[oppositePit] = 0
elif playerTurn == '2' and pit in PLAYER_2_PITS and board[pit] == 1:
oppositePit = OPPOSITE_PIT[pit]
board['2'] += board[oppositePit]
board[oppositePit] = 0
# Return the other player as the next player:
if playerTurn == '1':
return '2'
elif playerTurn == '2':
return '1'
def checkForWinner(board):
"""Looks at board and returns either '1' or '2' if there is a
winner or 'tie' or 'no winner' if there isn't. The game ends when a
player's pits are all empty; the other player claims the remaining
seeds for their store. The winner is whoever has the most seeds."""
player1Total = board['A'] + board['B'] + board['C']
player1Total += board['D'] + board['E'] + board['F']
player2Total = board['G'] + board['H'] + board['I']
player2Total += board['J'] + board['K'] + board['L']
if player1Total == 0:
# Player 2 gets all the remaining seeds on their side:
board['2'] += player2Total
for pit in PLAYER_2_PITS:
board[pit] = 0 # Set all pits to 0.
elif player2Total == 0:
# Player 1 gets all the remaining seeds on their side:
board['1'] += player1Total
for pit in PLAYER_1_PITS:
board[pit] = 0 # Set all pits to 0.
else:
return 'no winner' # No one has won yet.
# Game is over, find player with largest score.
if board['1'] > board['2']:
return '1'
elif board['2'] > board['1']:
return '2'
else:
return 'tie'
# If the program is run (instead of imported), run the game:
if __name__ == '__main__':
main()
在输入源代码并运行几次之后,尝试对其进行实验性的修改。你也可以自己想办法做到以下几点:
- 换个板多点坑。
- 随机选择一个奖励坑,当最后一粒种子落在坑中时,让玩家进行另一轮游戏。
- 为四个玩家而不是两个玩家创建一个正方形的棋盘。
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 175 行的
return '2'
改成return '1'
会怎么样? - 如果把 208 行的
return '2'
改成return '1'
会怎么样? - 如果把 125 行的
response == 'QUIT'
改成response == 'quit'
会怎么样? - 如果把 147 行的
board[pit] = 0
改成board[pit] = 1
会怎么样? - 如果把 53 行的
print('\n' * 60)
改成print('\n' * 0)
会怎么样? - 如果把第 48 行的
playerTurn = '1'
改成playerTurn = '2'
会怎么样? - 如果把 135 行的
board.get(response) == 0
改成board.get(response) == -1
会怎么样?
四十四、迷宫逃亡者 2D
这个二维迷宫向导向玩家展示了你在文本编辑器中创建的迷宫文件的俯视鸟瞰图,比如你用来编写.py
文件的 IDE。使用WASD
键,玩家可以分别向上、向左、向下和向右移动,将@
符号导向由X
字符标记的出口。
要创建一个迷宫文件,打开一个文本编辑器并创建以下模式。不要沿着顶部和左侧键入数字;它们仅供参考:
123456789
1#########
2#S# # # #
3##**#**#**#**#**#**##
4# # # # #
5##**#**#**#**#**#**##
6# # # # #
7##**#**#**#**#**#**##
8# # # #E#
9#########
#
字符代表墙壁,S
标记起点,E
标记出口。粗体的#字符代表您可以移除以形成迷宫的墙壁。奇数列和奇数行的墙不要拆,迷宫的边界不要拆。完成后,将迷宫保存为txt
(文本)文件。它可能看起来像这样:
#########
#S # #
# ### # #
# # # #
# ##### #
# # #
### # # #
# #E#
#########
当然,这是一个简单的迷宫。您可以创建任意大小的迷宫文件,只要它们的行数和列数为奇数。但是,请确保它仍然适合屏幕!你也可以从invpy.com/mazes
下载迷宫文件。
运行示例
当您运行mazerunner2d.py
时,输出将如下所示:
Maze Runner 2D, by Al Sweigart email@protected
(Maze files are generated by mazemakerrec.py)
Enter the filename of the maze (or LIST or QUIT):
> maze65x11s1.txt
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░@░ ░ ░ ░ ░ ░ ░
░ ░░░░░ ░ ░░░ ░ ░ ░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░ ░░░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░░░░░ ░ ░ ░░░░░ ░ ░░░░░ ░░░ ░ ░░░░░░░░░ ░ ░░░ ░░░ ░ ░░░░░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░░░░░░░░░ ░░░ ░░░ ░ ░░░░░░░░░░░ ░░░░░ ░ ░░░░░ ░ ░ ░░░ ░░░░░ ░░░░░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░░░ ░ ░░░ ░ ░ ░ ░░░░░░░░░░░ ░░░░░░░░░░░░░ ░ ░░░░░ ░ ░░░░░ ░
░ ░ ░ ░ ░ ░ X░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
W
Enter direction, or QUIT: ASD
`--snip--`
工作原理
该程序从一个文本文件中加载迷宫墙壁的数据,并将其加载到存储在maze
变量中的字典中。这个字典有用于键的(x, y)
元组和用于值的WALL
、EMPTY
、START
或EXIT
常量中的字符串。项目 45“迷宫逃亡者 3D”使用了类似的迷宫字典表示。这两个项目的区别在于在屏幕上呈现迷宫的代码。由于迷宫逃亡者 2D 更简单,我推荐在进入迷宫逃亡者 3D 之前先熟悉这个程序。
"""Maze Runner 2D, by Al Sweigart email@protected
Move around a maze and try to escape. Maze files are generated by
mazemakerrec.py.
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: large, game, maze"""
import sys, os
# Maze file constants:
WALL = '#'
EMPTY = ' '
START = 'S'
EXIT = 'E'
PLAYER = '@' # (!) Try changing this to '+' or 'o'.
BLOCK = chr(9617) # Character 9617 is '░'
def displayMaze(maze):
# Display the maze:
for y in range(HEIGHT):
for x in range(WIDTH):
if (x, y) == (playerx, playery):
print(PLAYER, end='')
elif (x, y) == (exitx, exity):
print('X', end='')
elif maze[(x, y)] == WALL:
print(BLOCK, end='')
else:
print(maze[(x, y)], end='')
print() # Print a newline after printing the row.
print('''Maze Runner 2D, by Al Sweigart email@protected
(Maze files are generated by mazemakerrec.py)''')
# Get the maze file's filename from the user:
while True:
print('Enter the filename of the maze (or LIST or QUIT):')
filename = input('> ')
# List all the maze files in the current folder:
if filename.upper() == 'LIST':
print('Maze files found in', os.getcwd())
for fileInCurrentFolder in os.listdir():
if (fileInCurrentFolder.startswith('maze') and
fileInCurrentFolder.endswith('.txt')):
print(' ', fileInCurrentFolder)
continue
if filename.upper() == 'QUIT':
sys.exit()
if os.path.exists(filename):
break
print('There is no file named', filename)
# Load the maze from a file:
mazeFile = open(filename)
maze = {}
lines = mazeFile.readlines()
playerx = None
playery = None
exitx = None
exity = None
y = 0
for line in lines:
WIDTH = len(line.rstrip())
for x, character in enumerate(line.rstrip()):
assert character in (WALL, EMPTY, START, EXIT), 'Invalid character at column {}, line {}'.format(x + 1, y + 1)
if character in (WALL, EMPTY):
maze[(x, y)] = character
elif character == START:
playerx, playery = x, y
maze[(x, y)] = EMPTY
elif character == EXIT:
exitx, exity = x, y
maze[(x, y)] = EMPTY
y += 1
HEIGHT = y
assert playerx != None and playery != None, 'No start in maze file.'
assert exitx != None and exity != None, 'No exit in maze file.'
while True: # Main game loop.
displayMaze(maze)
while True: # Get user move.
print(' W')
print('Enter direction, or QUIT: ASD')
move = input('> ').upper()
if move == 'QUIT':
print('Thanks for playing!')
sys.exit()
if move not in ['W', 'A', 'S', 'D']:
print('Invalid direction. Enter one of W, A, S, or D.')
continue
# Check if the player can move in that direction:
if move == 'W' and maze[(playerx, playery - 1)] == EMPTY:
break
elif move == 'S' and maze[(playerx, playery + 1)] == EMPTY:
break
elif move == 'A' and maze[(playerx - 1, playery)] == EMPTY:
break
elif move == 'D' and maze[(playerx + 1, playery)] == EMPTY:
break
print('You cannot move in that direction.')
# Keep moving in this direction until you encounter a branch point.
if move == 'W':
while True:
playery -= 1
if (playerx, playery) == (exitx, exity):
break
if maze[(playerx, playery - 1)] == WALL:
break # Break if we've hit a wall.
if (maze[(playerx - 1, playery)] == EMPTY
or maze[(playerx + 1, playery)] == EMPTY):
break # Break if we've reached a branch point.
elif move == 'S':
while True:
playery += 1
if (playerx, playery) == (exitx, exity):
break
if maze[(playerx, playery + 1)] == WALL:
break # Break if we've hit a wall.
if (maze[(playerx - 1, playery)] == EMPTY
or maze[(playerx + 1, playery)] == EMPTY):
break # Break if we've reached a branch point.
elif move == 'A':
while True:
playerx -= 1
if (playerx, playery) == (exitx, exity):
break
if maze[(playerx - 1, playery)] == WALL:
break # Break if we've hit a wall.
if (maze[(playerx, playery - 1)] == EMPTY
or maze[(playerx, playery + 1)] == EMPTY):
break # Break if we've reached a branch point.
elif move == 'D':
while True:
playerx += 1
if (playerx, playery) == (exitx, exity):
break
if maze[(playerx + 1, playery)] == WALL:
break # Break if we've hit a wall.
if (maze[(playerx, playery - 1)] == EMPTY
or maze[(playerx, playery + 1)] == EMPTY):
break # Break if we've reached a branch point.
if (playerx, playery) == (exitx, exity):
displayMaze(maze)
print('You have reached the exit! Good job!')
print('Thanks for playing!')
sys.exit()
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果将第 74 行的
character == START
改为character == EXIT
,会得到什么错误信息? - 如果把 105 行的
playery + 1
改成playery – 1
会怎么样? - 如果把 156 行的
(exitx, exity)
改成(None, None)
会怎么样? - 如果将第 89 行的
while True:
改为while False:
,会得到什么错误信息? - 如果把 104 行的
break
改成continue
会怎么样? - 如果将第 121 行的
break
改为continue
,会得到什么错误信息?
四十五、迷宫逃亡者 3D
这款三维迷宫游戏为玩家提供了第一人称视角的迷宫。试着找到你的出路!你可以按照项目 44“迷宫逃亡者 2D”中的说明生成迷宫文件,或者从invpy.com/mazes
下载迷宫文件。
运行示例
当您运行mazerunner3d.py
时,输出将如下所示:
Maze Runner 3D, by Al Sweigart email@protected
(Maze files are generated by mazemakerrec.py)
Enter the filename of the maze (or LIST or QUIT):
> maze75x11s1.txt
░░░░░░░░░░░░░░░░░░░
░ \ / ░
░ \_________/ ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ |_______| ░
░ / \ ░
░ / \ ░
░░░░░░░░░░░░░░░░░░░
Location (1, 1) Direction: NORTH
(W)
Enter direction: (A) (D) or QUIT.
> d
░░░░░░░░░░░░░░░░░░░
░ \ ░
░ \_____________░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ |____________░
░ / ░
░ / ░
░░░░░░░░░░░░░░░░░░░
Location (1, 1) Direction: EAST
`--snip--`
工作原理
这个 3D 透视 ASCII 艺术画从存储在ALL_OPEN
中的多行字符串开始。该字符串描述了没有路径被墙封闭的位置。然后,程序在ALL_OPEN
字符串的顶部绘制存储在CLOSED
字典中的墙,为封闭路径的任何可能组合生成 ASCII 艺术画。例如,下面是程序如何生成视图,其中墙在玩家的左边:
\ \
____ ____ \_ \_ ____
|\ /| | | /|
|| || | | ||
||__ __|| | |__ __||
|| |\ /| || | | |\ /| ||
|| | X | || + | = | | X | ||
|| |/ \| || | | |/ \| ||
||_/ \_|| | |_/ \_||
|| || | | ||
___|/ \|___ | | \|___
/ /
/ /
在显示字符串之前,源代码中 ASCII 字符中的句点会被删除;它们的存在只是为了使输入代码更容易,所以你不要插入或遗漏空格。
这是 3D 迷宫的源代码:
"""Maze 3D, by Al Sweigart email@protected
Move around a maze and try to escape... in 3D!
This code is available at https://nostarch.com/big-book-small-python-programming
Tags: extra-large, artistic, maze, game"""
import copy, sys, os
# Set up the constants:
WALL = '#'
EMPTY = ' '
START = 'S'
EXIT = 'E'
BLOCK = chr(9617) # Character 9617 is '░'
NORTH = 'NORTH'
SOUTH = 'SOUTH'
EAST = 'EAST'
WEST = 'WEST'
def wallStrToWallDict(wallStr):
"""Takes a string representation of a wall drawing (like those in
ALL_OPEN or CLOSED) and returns a representation in a dictionary
with (x, y) tuples as keys and single-character strings of the
character to draw at that x, y location."""
wallDict = {}
height = 0
width = 0
for y, line in enumerate(wallStr.splitlines()):
if y > height:
height = y
for x, character in enumerate(line):
if x > width:
width = x
wallDict[(x, y)] = character
wallDict['height'] = height + 1
wallDict['width'] = width + 1
return wallDict
EXIT_DICT = {(0, 0): 'E', (1, 0): 'X', (2, 0): 'I',
(3, 0): 'T', 'height': 1, 'width': 4}
# The way we create the strings to display is by converting the pictures
# in these multiline strings to dictionaries using wallStrToWallDict().
# Then we compose the wall for the player's location and direction by
# "pasting" the wall dictionaries in CLOSED on top of the wall dictionary
# in ALL_OPEN.
ALL_OPEN = wallStrToWallDict(r'''
.................
____.........____
...|\......./|...
...||.......||...
...||__...__||...
...||.|\./|.||...
...||.|.X.|.||...
...||.|/.\|.||...
...||_/...\_||...
...||.......||...
___|/.......\|___
.................
.................'''.strip())
# The strip() call is used to remove the newline
# at the start of this multiline string.
CLOSED = {}
CLOSED['A'] = wallStrToWallDict(r'''
_____
.....
.....
.....
_____'''.strip()) # Paste to 6, 4.
CLOSED['B'] = wallStrToWallDict(r'''
.\.
..\
...
...
...
../
./.'''.strip()) # Paste to 4, 3.
CLOSED['C'] = wallStrToWallDict(r'''
___________
...........
...........
...........
...........
...........
...........
...........
...........
___________'''.strip()) # Paste to 3, 1.
CLOSED['D'] = wallStrToWallDict(r'''
./.
/..
...
...
...
\..
.\.'''.strip()) # Paste to 10, 3.
CLOSED['E'] = wallStrToWallDict(r'''
..\..
...\_
....|
....|
....|
....|
....|
....|
....|
....|
....|
.../.
../..'''.strip()) # Paste to 0, 0.
CLOSED['F'] = wallStrToWallDict(r'''
../..
_/...
|....
|....
|....
|....
|....
|....
|....
|....
|....
.\...
..\..'''.strip()) # Paste to 12, 0.
def displayWallDict(wallDict):
"""Display a wall dictionary, as returned by wallStrToWallDict(), on
the screen."""
print(BLOCK * (wallDict['width'] + 2))
for y in range(wallDict['height']):
print(BLOCK, end='')
for x in range(wallDict['width']):
wall = wallDict[(x, y)]
if wall == '.':
wall = ' '
print(wall, end='')
print(BLOCK) # Print block with a newline.
print(BLOCK * (wallDict['width'] + 2))
def pasteWallDict(srcWallDict, dstWallDict, left, top):
"""Copy the wall representation dictionary in srcWallDict on top of
the one in dstWallDict, offset to the position given by left, top."""
dstWallDict = copy.copy(dstWallDict)
for x in range(srcWallDict['width']):
for y in range(srcWallDict['height']):
dstWallDict[(x + left, y + top)] = srcWallDict[(x, y)]
return dstWallDict
def makeWallDict(maze, playerx, playery, playerDirection, exitx, exity):
"""From the player's position and direction in the maze (which has
an exit at exitx, exity), create the wall representation dictionary
by pasting wall dictionaries on top of ALL_OPEN, then return it."""
# The A-F "sections" (which are relative to the player's direction)
# determine which walls in the maze we check to see if we need to
# paste them over the wall representation dictionary we're creating.
if playerDirection == NORTH:
# Map of the sections, relative A
# to the player @: BCD (Player facing north)
# email@protected
offsets = (('A', 0, -2), ('B', -1, -1), ('C', 0, -1),
('D', 1, -1), ('E', -1, 0), ('F', 1, 0))
if playerDirection == SOUTH:
# Map of the sections, relative email@protected
# to the player @: DCB (Player facing south)
# A
offsets = (('A', 0, 2), ('B', 1, 1), ('C', 0, 1),
('D', -1, 1), ('E', 1, 0), ('F', -1, 0))
if playerDirection == EAST:
# Map of the sections, relative EB
# to the player @: @CA (Player facing east)
# FD
offsets = (('A', 2, 0), ('B', 1, -1), ('C', 1, 0),
('D', 1, 1), ('E', 0, -1), ('F', 0, 1))
if playerDirection == WEST:
# Map of the sections, relative DF
# to the player @: email@protected (Player facing west)
# BE
offsets = (('A', -2, 0), ('B', -1, 1), ('C', -1, 0),
('D', -1, -1), ('E', 0, 1), ('F', 0, -1))
section = {}
for sec, xOff, yOff in offsets:
section[sec] = maze.get((playerx + xOff, playery + yOff), WALL)
if (playerx + xOff, playery + yOff) == (exitx, exity):
section[sec] = EXIT
wallDict = copy.copy(ALL_OPEN)
PASTE_CLOSED_TO = {'A': (6, 4), 'B': (4, 3), 'C': (3, 1),
'D': (10, 3), 'E': (0, 0), 'F': (12, 0)}
for sec in 'ABDCEF':
if section[sec] == WALL:
wallDict = pasteWallDict(CLOSED[sec], wallDict,
PASTE_CLOSED_TO[sec][0], PASTE_CLOSED_TO[sec][1])
# Draw the EXIT sign if needed:
if section['C'] == EXIT:
wallDict = pasteWallDict(EXIT_DICT, wallDict, 7, 9)
if section['E'] == EXIT:
wallDict = pasteWallDict(EXIT_DICT, wallDict, 0, 11)
if section['F'] == EXIT:
wallDict = pasteWallDict(EXIT_DICT, wallDict, 13, 11)
return wallDict
print('Maze Runner 3D, by Al Sweigart email@protected')
print('(Maze files are generated by mazemakerrec.py)')
# Get the maze file's filename from the user:
while True:
print('Enter the filename of the maze (or LIST or QUIT):')
filename = input('> ')
# List all the maze files in the current folder:
if filename.upper() == 'LIST':
print('Maze files found in', os.getcwd())
for fileInCurrentFolder in os.listdir():
if (fileInCurrentFolder.startswith('maze')
and fileInCurrentFolder.endswith('.txt')):
print(' ', fileInCurrentFolder)
continue
if filename.upper() == 'QUIT':
sys.exit()
if os.path.exists(filename):
break
print('There is no file named', filename)
# Load the maze from a file:
mazeFile = open(filename)
maze = {}
lines = mazeFile.readlines()
px = None
py = None
exitx = None
exity = None
y = 0
for line in lines:
WIDTH = len(line.rstrip())
for x, character in enumerate(line.rstrip()):
assert character in (WALL, EMPTY, START, EXIT), 'Invalid character at column {}, line {}'.format(x + 1, y + 1)
if character in (WALL, EMPTY):
maze[(x, y)] = character
elif character == START:
px, py = x, y
maze[(x, y)] = EMPTY
elif character == EXIT:
exitx, exity = x, y
maze[(x, y)] = EMPTY
y += 1
HEIGHT = y
assert px != None and py != None, 'No start point in file.'
assert exitx != None and exity != None, 'No exit point in file.'
pDir = NORTH
while True: # Main game loop.
displayWallDict(makeWallDict(maze, px, py, pDir, exitx, exity))
while True: # Get user move.
print('Location ({}, {}) Direction: {}'.format(px, py, pDir))
print(' (W)')
print('Enter direction: (A) (D) or QUIT.')
move = input('> ').upper()
if move == 'QUIT':
print('Thanks for playing!')
sys.exit()
if (move not in ['F', 'L', 'R', 'W', 'A', 'D']
and not move.startswith('T')):
print('Please enter one of F, L, or R (or W, A, D).')
continue
# Move the player according to their intended move:
if move == 'F' or move == 'W':
if pDir == NORTH and maze[(px, py - 1)] == EMPTY:
py -= 1
break
if pDir == SOUTH and maze[(px, py + 1)] == EMPTY:
py += 1
break
if pDir == EAST and maze[(px + 1, py)] == EMPTY:
px += 1
break
if pDir == WEST and maze[(px - 1, py)] == EMPTY:
px -= 1
break
elif move == 'L' or move == 'A':
pDir = {NORTH: WEST, WEST: SOUTH,
SOUTH: EAST, EAST: NORTH}[pDir]
break
elif move == 'R' or move == 'D':
pDir = {NORTH: EAST, EAST: SOUTH,
SOUTH: WEST, WEST: NORTH}[pDir]
break
elif move.startswith('T'): # Cheat code: 'T x,y'
px, py = move.split()[1].split(',')
px = int(px)
py = int(py)
break
else:
print('You cannot move in that direction.')
if (px, py) == (exitx, exity):
print('You have reached the exit! Good job!')
print('Thanks for playing!')
sys.exit()
探索程序
试着找出下列问题的答案。尝试对代码进行一些修改,然后重新运行程序,看看这些修改有什么影响。
- 如果把 279 行的
move == 'QUIT'
改成move == 'quit'
会导致什么 bug? - 怎样才能解除心灵运输的欺骗?