【Python】我的第一個Python游戲:石頭剪刀布
最近有粉絲朋友跟云朵君聊到用Python做個石頭剪刀布的小游戲。我一尋思,還挺好玩。其實游戲編程是學習如何編程的一個好方法,它會使用許多我們在現(xiàn)實世界中看到的工具,還可以玩一個游戲來測試我們的編程結果!作為Python游戲編程的入門游戲:石頭剪刀布,我們今天就來一起玩一玩。
在文中我們將一起學習如何:
為剪刀石頭布游戲碼代碼 用 input()接收用戶輸入使用 while循環(huán)連續(xù)玩幾個游戲用 Enum和函數(shù)簡化代碼用字典定義更復雜的規(guī)則
什么是石頭剪刀布?
大家以前都玩過石頭剪刀布吧。假裝你不熟悉,石頭剪刀布是一個供兩個或更多人玩的手部游戲。參與者說 "石頭、剪刀、布",然后同時將他們的手捏成石頭(拳頭)、一張布(手掌朝上)或一把剪刀(伸出兩個手指)的形狀。

規(guī)則是直截了當?shù)?
石頭砸剪刀。 布包石頭。 剪刀剪布。
現(xiàn)在用了這些規(guī)則,可以開始考慮如何將它們轉(zhuǎn)化為Python代碼。
在Python中玩單一的石頭剪刀布游戲
使用上面的描述和規(guī)則,我們可以做一個石頭剪刀布的游戲。首先需要導入用來模擬計算機選擇的模塊。
import random
現(xiàn)在我們能夠使用隨機里面的不同工具來隨機化計算機在游戲中的行動。由于我們的用戶也需要能夠選擇行動,所以需要接受用戶的輸入。
接收用戶輸入
從用戶那里獲取輸入信息在Python中是非常直接的。這里的目標是問用戶他們想選擇什么行動,然后把這個選擇分配給一個變量。
user_action = input("輸入一個選擇(石頭、剪刀、布):")
這將提示用戶輸入一個選擇,并將其保存在一個變量中供以后使用。用戶已經(jīng)選擇了一個行動后,輪到計算機決定做些什么。
計算機選擇
競爭性的石頭剪刀布游戲涉及策略
還正有人研究并把石頭剪刀布游戲策略寫成學術論文,感興趣的小伙伴可以查看論文(傳送門:https://arxiv.org/pdf/1404.5199v1.pdf)
研究人員將 360 名學生分成六人一組,讓他們隨機配對玩 300 輪石頭剪刀布。學生們每次贏得一輪比賽都會得到少量的錢。在他們玩游戲的過程中,研究人員觀察了玩家在輸贏時如何在三個游戲選項中輪換。
他們發(fā)現(xiàn),“如果一名玩家在一場比賽中戰(zhàn)勝對手,她在下一場比賽中重復相同動作的概率大大高于她改變動作的概率。” 如果一名玩家輸了兩次或兩次以上,她很可能會改變她的打法,并且更有可能轉(zhuǎn)向能夠擊敗剛剛擊敗她的對手而不是她的對手剛剛擊敗她的動作。例如,如果小紅對小明的石頭玩剪刀輸了,小紅最有可能改用紙,這會打敗小明的石頭。根據(jù)研究,這是一個合理的策略,因為小明很可能會繼續(xù)玩已經(jīng)獲勝的動作。作者將此稱為“贏-留,輸-轉(zhuǎn)變”策略。
因此,這是在剪刀石頭布上獲勝的最佳方法:如果你輸?shù)袅说谝惠啠袚Q到擊敗對手剛剛玩過的動作。如果你贏了,不要繼續(xù)玩同樣的動作,而是換成能打敗你剛剛玩的動作的動作。換句話說,玩你失敗的對手剛剛玩的動作。也就是說:你用石頭贏了一輪別人的剪刀,他們即將改用布,此時你應該改用剪刀。
根據(jù)上述的游戲策略,試圖開發(fā)一個模型,應該需要花費不少的時間。為了簡便,我們讓計算機選擇一個隨機的行動來節(jié)省一些時間。隨機選擇就是讓計算機選擇一個偽隨機值。
可以使用 random.choice() 來讓計算機在這些動作中隨機選擇。
possible_actions = ["石頭", "剪刀", "布"]
computer_action = random.choice(possible_actions)
這允許從列表中選擇一個隨機元素。我們也可以打印出用戶和計算機的選擇。
print(f"\n你選擇了 {user_action},
計算機選擇了 {computer_action}.\n")
打印輸出用戶和計算機的操作對用戶來說是有幫助的,而且還可以幫助我們在以后的調(diào)試中,以防結果不大對勁。
判斷輸贏
現(xiàn)在,兩個玩家都做出了選擇,我們只需要使用if ... elif ... else 代碼塊方法來決定誰輸誰贏,接下來比較玩家的選擇并決定贏家。
if user_action == computer_action:
print(f"兩個玩家都選擇了 {user_action}. 這是一個平局!")
elif user_action == "石頭":
if computer_action == "剪刀":
print("石頭砸碎剪刀!你贏了!")
else:
print("布包住石頭!你輸了。")
elif user_action == "布":
if computer_action == "石頭":
print("布包住石頭!你贏了!")
else:
print("剪刀剪碎布!你輸了。")
elif user_action == "剪刀":
if computer_action == "布":
print("剪刀剪碎布!你贏了!")
else:
print("石頭砸碎剪刀!你輸了。")
通過先比較平局條件,我們擺脫了相當多的情況。否則我們就需要檢查 user_action 的每一個可能的動作,并與 computer_action 的每一個可能的動作進行比較。通過先檢查平局條件,我們能夠知道計算機選擇了什么,只需對 computer_action 進行兩次條件檢查。
所以完整代碼現(xiàn)在應該是這樣的:
上下滑動查看更多源碼
import random
user_action = input("輸入一個選擇(石頭、剪刀、布):")
possible_actions = ["石頭", "剪刀", "布"]
computer_action = random.choice(possible_actions)
print(f"\n你選擇了 {user_action}, 計算機選擇了 {computer_action}.\n")
if user_action == computer_action:
print(f"兩個玩家都選擇了 {user_action}. 這是一個平局!")
elif user_action == "石頭":
if computer_action == "剪刀":
print("石頭砸碎剪刀!你贏了!")
else:
print("布包住石頭!你輸了。")
elif user_action == "布":
if computer_action == "石頭":
print("布包住石頭!你贏了!")
else:
print("剪刀剪碎布!你輸了。")
elif user_action == "剪刀":
if computer_action == "布":
print("剪刀剪碎布!你贏了!")
else:
print("石頭砸碎剪刀!你輸了。")
現(xiàn)在我們已經(jīng)寫好了代碼,可以接受用戶的輸入,并為計算機選擇一個隨機動作,最后決定勝負!這個初級代碼只能讓我們和電腦玩一局。
連續(xù)打幾場比賽
雖然單一的剪刀石頭布游戲比較有趣,但如果我們能連續(xù)玩幾場,不是更好嗎?此時我們想到 循環(huán) 是創(chuàng)建重復性事件的一個好方法。我們可以用一個 while循環(huán) 來無限期地玩這個游戲。
import random
while True:
# 包住上完整代碼
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break
注意我們補充的代碼,檢查用戶是否想再玩一次,如果他們不想玩就中斷,這一點很重要。如果沒有這個檢查,用戶就會被迫玩下去,直到他們用Ctrl+C或其他的方法強制終止程序。
對再次播放的檢查是對字符串 "y" 的檢查。但是,像這樣檢查特定的東西可能會使用戶更難停止游戲。如果用戶輸入 "yes" 或 "no" 怎么辦?字符串比較往往很棘手,因為我們永遠不知道用戶可能輸入什么。他們可能會做所有的小寫字母,所有的大寫字母,甚至是輸入中文。
下面是幾個不同的字符串比較的結果。
>>> play_again = "yes"
>>> play_again == "n"
False
>>> play_again != "y"
True
其實這不是我們想要的。如果用戶輸入 "yes",期望再次游戲,卻被踢出游戲,他們可能不會太高興。
enum.IntEnum描述動作
我們在之前的示意代碼中,定義的是中文字符串,但實際使用python開發(fā)時,代碼里一般不使用中文(除了注釋),因此了解這一節(jié)還是很有必要的。
所以我們將把石頭剪刀布翻譯成:"rock", "scissors", "paper"。
字符串比較可能導致像我們上面看到的問題,所以需要盡可能避免。然而,我們的程序要求的第一件事就是讓用戶輸入一個字符串!如果用戶錯誤地輸入了 "Rock "或 "rOck "怎么辦?如果用戶錯誤地輸入 "Rock "或 "rOck "怎么辦?大寫字母很重要,所以它們不會相等。
>>> print("rock" == "Rock")
False
由于大寫字母很重要,所以 "r" 和 "R" 并不相等。一個可能的解決方案是用數(shù)字代替。給每個動作分配一個數(shù)字可以為我們省去一些麻煩。
ROCK_ACTION = 0
SCISSORS_ACTION = 1
PAPER_ACTION = 2
我們通過分配的數(shù)字來引用不同的行動,整數(shù)不存在與字符串一樣的比較問題,所以這是可行的。現(xiàn)在讓用戶輸入一個數(shù)字,并直接與這些值進行比較。
user_input = input("輸入您的選擇 (石頭[0], 剪刀[1], 布[2]): ")
user_action = int(user_input)
if user_action == ROCK_ACTION:
# 處理 ROCK_ACTION
因為input()返回一個字符串,需要用int() 把返回值轉(zhuǎn)換成一個整數(shù)。然后可以將輸入值與上面的每個動作進行比較。雖然這樣做效果很好,但它可能依賴于對變量的正確命名。其實有一個更好的方法是使用**enum.IntEnum**來自定義動作類。
我們使用 enum.IntEnum創(chuàng)建屬性并給它們分配類似于上面所示的值,將動作歸入它們自己的命名空間,使代碼更有表現(xiàn)力。
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
這創(chuàng)建了一個自定義Action,可以用它來引用我們支持的不同類型的Action。它的工作原理是將其中的每個屬性分配給我們指定的值。
兩個動作的比較是直截了當?shù)模F(xiàn)在它們有一個有用的類名與之相關。
>>> Action.Rock == Action.Rock
True
因為成員的值是相同的,所以比較的結果是相等的。類的名稱也使我們想比較兩個動作的意思更加明顯。
注意:要了解更多關于enum的信息,請查看官方文檔[1]。
我們甚至可以從一個 int 創(chuàng)建一個 Action。
>>> Action.Rock == Action(0)
True
>>> Action(0)
<Action.Rock: 0>
Action 查看傳入的值并返回適當?shù)?Action。因此現(xiàn)在可以把用戶的輸入作為一個int,并從中創(chuàng)建一個Action,媽媽再也不用擔心拼寫問題了!
程序流程(圖)
雖然剪刀石頭布看起來并不復雜,但仔細考慮玩剪刀石頭布的步驟是很重要的,這樣才能確保我們的程序涵蓋所有可能的情況。對于任何項目,即使是小項目,我們有必要創(chuàng)建一個所需行為的流程圖并圍繞它實現(xiàn)代碼。我們可以用一個列表來達到類似的效果,但它更難捕捉到諸如循環(huán)和條件等相關邏輯。
流程圖不需要過于復雜,甚至不需要使用真正的代碼。只要提前描述所需的行為,就可以幫助我們在問題發(fā)生之前解決問題
這里有一個流程圖,描述了一個單一的剪刀石頭布游戲。

每個玩家選擇一個行動,然后確定一個贏家。這個流程圖對于我們所編碼的單個游戲來說是準確的,但對于現(xiàn)實生活中的游戲來說卻不一定準確。在現(xiàn)實生活中,玩家會同時選擇他們的行動,而不是像流程圖中建議的那樣一次一個。
然而,在編碼版本中,這一點是可行的,因為玩家的選擇對電腦是隱藏的,而電腦的選擇對玩家也是隱藏的。兩個玩家可以在不同的時間做出選擇而不影響游戲的公平性。
流程圖可以幫助我們在早期發(fā)現(xiàn)可能的錯誤,也可以讓我們看到是否要增加更多的功能。例如這個流程圖,描述了如何重復玩游戲,直到用戶決定停止。

如果不寫代碼,我們可以看到第一個流程圖沒有辦法重復玩。我們可以使用這種繪制流程圖的方法在編程前解決類似的問題,這有助于我們碼出更整潔、更易于管理的代碼!
拆分代碼并封裝函數(shù)
現(xiàn)在我已經(jīng)用流程圖概述了程序的流程,我們可以試著組織我們的代碼,使它更接近于所確定的步驟。一個方法是為流程圖中的每個步驟創(chuàng)建一個函數(shù)。 其實函數(shù)是一種很好的方法,可以將大塊的代碼拆分成更小的、更容易管理的部分。
我們不一定需要為條件檢查的再次播放創(chuàng)建一個函數(shù),但如果我們愿意,我們可以。如果我們還沒有,我們可以從導入隨機開始,并定義我們的Action類。
import random
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
接下來定義 get_user_selection() 的代碼,它不接受任何參數(shù)并返回一個 Action。
def get_user_selection():
user_input = input("輸入您的選擇 (石頭[0], 剪刀[1], 布[2]):")
selection = int(user_input)
action = Action(selection)
return action
注意這里是如何將用戶的輸入作為一個 int,然后得到一個 Action。不過,給用戶的那條長信息有點麻煩。如果我們想增加更多的動作,就不得不在提示中添加更多的文字。
我們可以使用一個列表推導式來生成一部分輸入。
def get_user_selection():
choices = [f"{action.name}[{action.value}]" for action in Action]
choices_str = ", ".join(choices)
selection = int(input(f"輸出您的選擇 ({choices_str}): "))
action = Action(selection)
return action
現(xiàn)在不再需要擔心將來添加或刪除動作的問題了!接下來測試一下,我們可以看到代碼是如何提示用戶并返回一個與用戶輸入值相關的動作。
>>> get_user_selection()
輸入您的選擇 (石頭[0], 剪刀[1], 布[2]): 0
<Action.Rock: 0>
現(xiàn)在我們需要一個函數(shù)來獲取計算機的動作。和 get_user_selection() 一樣,這個函數(shù)應該不需要參數(shù),并返回一個 Action。因為 Action 的值范圍是0到2,所以使用 random.randint() 幫助我們在這個范圍內(nèi)生成一個隨機數(shù)。
random.randint() 返回一個在指定的最小值和最大值(包括)之間的隨機值。可以使用 len() 來幫助計算代碼中的上限應該是多少。
def get_computer_selection():
selection = random.randint(0, len(Action) - 1)
action = Action(selection)
return action
因為 Action 的值從0開始計算,而len()從1開始計算,所以需要額外做個 len(Action)-1。
測試該函數(shù),它簡單地返回與隨機數(shù)相關的動作。
>>> get_computer_selection()
<Action.Scissors: 2>
看起來還不錯!接下來,需要一個函數(shù)來決定輸贏,這個函數(shù)將接受兩個參數(shù),用戶的行動和計算機的行動。它只需要將結果顯示在控制臺上,而不需要返回任何東西。
def determine_winner(user_action, computer_action):
if user_action == computer_action:
print(f"兩個玩家都選擇了 {user_action.name}. 這是一個平局!")
elif user_action == Action.Rock:
if computer_action == Action.Scissors:
print("石頭砸碎剪刀!你贏了!")
else:
print("布包住石頭!你輸了。")
elif user_action == Action.Paper:
if computer_action == Action.Rock:
print("布包住石頭!你贏了!")
else:
print("剪刀剪碎布!你輸了。")
elif user_action == Action.Scissors:
if computer_action == Action.Paper:
print("剪刀剪碎布!你贏了!")
else:
print("石頭砸碎剪刀!你輸了。")
這里決定勝負的寫法與剛開始的代碼非常相似。而現(xiàn)在可以直接比較行動類型,而不必擔心那些討厭的字符串!
我們甚至可以通過向 determinal_winner() 傳遞不同的參數(shù)來測試函數(shù),看看會打印出什么。
>>> determine_winner(Action.Rock, Action.Scissors)
石頭砸碎剪刀!你贏了!
既然我們要從一個數(shù)字創(chuàng)建一個動作,如果用戶想用數(shù)字3創(chuàng)建一個動作,會發(fā)生什么?(我們定義的最大數(shù)字是2)。
>>> Action(3)
ValueError: 3 is not a valid Action
報錯了!這并不是我們希望發(fā)生這種情況。接下來可以在流程圖上添加一些邏輯,來補充這個 bug,以確保用戶始終輸入一個有效的選擇。
在用戶做出選擇后立即加入檢查是有意義的。

如果用戶輸入了一個無效的值,那么我們就重復這個步驟來獲得用戶的選擇。對用戶選擇的唯一真正要求是它在【0, 1, 2】之間的一個數(shù)。如果用戶的輸入超出了這個范圍,那么就會產(chǎn)生一個ValueError異常。我們可以處理這個異常,從而不會向用戶顯示默認的錯誤信息。
現(xiàn)在我們已經(jīng)定義了一些反映流程圖中的步驟的函數(shù),我們的游戲邏輯就更有條理和緊湊了。這就是我們的while循環(huán)現(xiàn)在需要包含的所有內(nèi)容。
while True:
try:
user_action = get_user_selection()
except ValueError as e:
range_str = f"[0, {len(Action) - 1}]"
print(f"Invalid selection. Enter a value in range {range_str}")
continue
computer_action = get_computer_selection()
determine_winner(user_action, computer_action)
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break
這看起來是不是干凈多了?注意,如果用戶未能選擇一個有效的范圍,那么我們就使用continue而不是break。這使得代碼繼續(xù)到循環(huán)的下一個迭代,而不是跳出該循環(huán)。
Rock Paper Scissors … Lizard Spock
如果我們看過《生活大爆炸》,那么我們可能對石頭剪子布蜥蜴斯波克很熟悉。如果沒有,那么這里有一張圖,描述了這個游戲和決定勝負的規(guī)則。

我們可以使用我們在上面學到的同樣的工具來實現(xiàn)這個游戲。例如,我們可以在Action中加入Lizard和Spock的值。然后我們只需要修改 get_user_selection() 和 get_computer_selection(),以納入這些選項。然而,更新determinal_winner()。
與其在我們的代碼中加入大量的if ... elif ... else語句,我們可以使用字典來幫助顯示動作之間的關系。字典是顯示 鍵值關系 的一個好方法。在這種情況下,鍵 可以是一個動作,如剪刀,而 值 可以是一個它所擊敗的動作的列表。
那么,對于只有三個選項的 determinal_winner() 來說,這將是什么樣子呢?好吧,每個 Action 只能打敗一個其他的 Action,所以列表中只包含一個項目。下面是我們的代碼之前的樣子。
def determine_winner(user_action, computer_action):
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif user_action == Action.Rock:
if computer_action == Action.Scissors:
print("Rock smashes scissors! You win!")
else:
print("Paper covers rock! You lose.")
elif user_action == Action.Paper:
if computer_action == Action.Rock:
print("Paper covers rock! You win!")
else:
print("Scissors cuts cpaper! You lose.")
elif user_action == Action.Scissors:
if computer_action == Action.Paper:
print("Scissors cuts cpaper! You win!")
else:
print("Rock smashes scissors! You lose.")
現(xiàn)在,我們可以有一個描述勝利條件的字典,而不是與每個行動相比較。
def determine_winner(user_action, computer_action):
victories = {
Action.Rock: [Action.Scissors], # Rock beats scissors
Action.Paper: [Action.Rock], # Paper beats rock
Action.Scissors: [Action.Paper] # Scissors beats cpaper
}
defeats = victories[user_action]
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif computer_action in defeats:
print(f"{user_action.name} beats {computer_action.name}! You win!")
else:
print(f"{computer_action.name} beats {user_action.name}! You lose.")
我們還是和以前一樣,先檢查平局條件。但我們不是比較每一個 Action,而是比較用戶輸入的 Action 與電腦輸入的 Action。由于鍵值對是一個列表,我們可以使用成員運算符 in 來檢查一個元素是否在其中。

由于我們不再使用冗長的if ... elif ... else語句,為這些新的動作添加檢查是相對容易的。我們可以先把Lizard和Spock加入到Action中。
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
Lizard = 3
Spock = 4
接下來,從圖中添加所有的勝利關系。
victories = {
Action.Scissors: [Action.Lizard, Action.Paper],
Action.Paper: [Action.Spock, Action.Rock],
Action.Rock: [Action.Lizard, Action.Scissors],
Action.Lizard: [Action.Spock, Action.Paper],
Action.Spock: [Action.Scissors, Action.Rock]
}
注意,現(xiàn)在每個 Action 都有一個包含可以擊敗的兩個元素的列表。而在基本的 "剪刀石頭布 " 實現(xiàn)中,只有一個元素。
我們寫了 get_user_selection() 來適應新的動作,所以不需要改變該代碼的任何內(nèi)容。get_computer_selection() 的情況也是如此。由于 Action 的長度發(fā)生了變化,隨機數(shù)的范圍也將發(fā)生變化。
看看現(xiàn)在的代碼有多簡潔,有多容易維護管理!完整程序的完整代碼:
上下滑動查看更多源碼
import random
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Paper = 1
Scissors = 2
Lizard = 3
Spock = 4
victories = {
Action.Scissors: [Action.Lizard, Action.Paper],
Action.Paper: [Action.Spock, Action.Rock],
Action.Rock: [Action.Lizard, Action.Scissors],
Action.Lizard: [Action.Spock, Action.Paper],
Action.Spock: [Action.Scissors, Action.Rock]
}
def get_user_selection():
choices = [f"{action.name}[{action.value}]" for action in Action]
choices_str = ", ".join(choices)
selection = int(input(f"Enter a choice ({choices_str}): "))
action = Action(selection)
return action
def get_computer_selection():
selection = random.randint(0, len(Action) - 1)
action = Action(selection)
return action
def determine_winner(user_action, computer_action):
defeats = victories[user_action]
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif computer_action in defeats:
print(f"{user_action.name} beats {computer_action.name}! You win!")
else:
print(f"{computer_action.name} beats {user_action.name}! You lose.")
while True:
try:
user_action = get_user_selection()
except ValueError as e:
range_str = f"[0, {len(Action) - 1}]"
print(f"Invalid selection. Enter a value in range {range_str}")
continue
computer_action = get_computer_selection()
determine_winner(user_action, computer_action)
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break到這里我們已經(jīng)用Python代碼實現(xiàn)了rock paper scissors lizard Spock 。接下來你就可以仔細檢查一下,確保我們沒有遺漏任何東西,然后進行一次游戲。
總結
看到這里,必須點個贊,因為我們剛剛完成了第一個Python游戲。現(xiàn)在,我們知道了如何從頭開始創(chuàng)建剪刀石頭布游戲,而且我可以以最小的代價擴展游戲中可能的行動數(shù)量。
參考資料
官方文檔: https://docs.python.org/3/library/enum.html

往期精彩回顧
適合初學者入門人工智能的路線及資料下載 (圖文+視頻)機器學習入門系列下載 中國大學慕課《機器學習》(黃海廣主講) 機器學習及深度學習筆記等資料打印 《統(tǒng)計學習方法》的代碼復現(xiàn)專輯 機器學習交流qq群955171419,加入微信群請掃碼
