在文中我們將一起學習如何:
為剪刀石頭布游戲碼代碼
用 input()
接收用戶輸入
使用 while
循環連續玩幾個游戲
用 Enum
和函數簡化代碼
用字典定義更復雜的規則
大家以前都玩過石頭剪刀布吧。假裝你不熟悉,石頭剪刀布是一個供兩個或更多人玩的手部游戲。參與者說 "石頭、剪刀、布"
,然後同時將他們的手捏成石頭(拳頭)、一張布(手掌朝上)或一把剪刀(伸出兩個手指)的形狀。
規則是直截了當的:
石頭砸剪刀。
布包石頭。
剪刀剪布。
現在用了這些規則,可以開始考慮如何將它們轉化為Python代碼。
使用上面的描述和規則,我們可以做一個石頭剪刀布的游戲。首先需要導入用來模擬計算機選擇的模塊。
import random
現在我們能夠使用隨機裡面的不同工具來隨機化計算機在游戲中的行動。由於我們的用戶也需要能夠選擇行動,所以需要接受用戶的輸入。
從用戶那裡獲取輸入信息在Python中是非常直接的。這裡的目標是問用戶他們想選擇什麼行動,然後把這個選擇分配給一個變量。
user_action = input("輸入一個選擇(石頭、剪刀、布):")
這將提示用戶輸入一個選擇,並將其保存在一個變量中供以後使用。用戶已經選擇了一個行動後,輪到計算機決定做些什麼。
競爭性的石頭剪刀布游戲涉及策略
還正有人研究並把石頭剪刀布游戲策略寫成學術論文,感興趣的小伙伴可以查看論文(傳送門:https://arxiv.org/pdf/1404.5199v1.pdf)
研究人員將 360 名學生分成六人一組,讓他們隨機配對玩 300 輪石頭剪刀布。學生們每次贏得一輪比賽都會得到少量的錢。在他們玩游戲的過程中,研究人員觀察了玩家在輸贏時如何在三個游戲選項中輪換。
他們發現,“如果一名玩家在一場比賽中戰勝對手,她在下一場比賽中重復相同動作的概率大大高於她改變動作的概率。” 如果一名玩家輸了兩次或兩次以上,她很可能會改變她的打法,並且更有可能轉向能夠擊敗剛剛擊敗她的對手而不是她的對手剛剛擊敗她的動作。例如,如果小紅對小明的石頭玩剪刀輸了,小紅最有可能改用紙,這會打敗小明的石頭。根據研究,這是一個合理的策略,因為小明很可能會繼續玩已經獲勝的動作。作者將此稱為“贏-留,輸-轉變”策略。
因此,這是在剪刀石頭布上獲勝的最佳方法:如果你輸掉了第一輪,切換到擊敗對手剛剛玩過的動作。如果你贏了,不要繼續玩同樣的動作,而是換成能打敗你剛剛玩的動作的動作。換句話說,玩你失敗的對手剛剛玩的動作。也就是說:你用石頭贏了一輪別人的剪刀,他們即將改用布,此時你應該改用剪刀。
根據上述的游戲策略,試圖開發一個模型,應該需要花費不少的時間。為了簡便,我們讓計算機選擇一個隨機的行動來節省一些時間。隨機選擇就是讓計算機選擇一個偽隨機值。
可以使用 random.choice()
來讓計算機在這些動作中隨機選擇。
possible_actions = ["石頭", "剪刀", "布"]
computer_action = random.choice(possible_actions)
這允許從列表中選擇一個隨機元素。我們也可以打印出用戶和計算機的選擇。
print(f"\n你選擇了 {user_action},
計算機選擇了 {computer_action}.\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
進行兩次條件檢查。
所以完整代碼現在應該是這樣的:
上下滑動查看更多源碼
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("石頭砸碎剪刀!你輸了。")
現在我們已經寫好了代碼,可以接受用戶的輸入,並為計算機選擇一個隨機動作,最後決定勝負!這個初級代碼只能讓我們和電腦玩一局。
雖然單一的剪刀石頭布游戲比較有趣,但如果我們能連續玩幾場,不是更好嗎?此時我們想到 循環 是創建重復性事件的一個好方法。我們可以用一個 while循環 來無限期地玩這個游戲。
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"
,期望再次游戲,卻被踢出游戲,他們可能不會太高興。
我們在之前的示意代碼中,定義的是中文字符串,但實際使用python開發時,代碼裡一般不使用中文(除了注釋),因此了解這一節還是很有必要的。
所以我們將把石頭剪刀布翻譯成:"rock", "scissors", "paper"
。
字符串比較可能導致像我們上面看到的問題,所以需要盡可能避免。然而,我們的程序要求的第一件事就是讓用戶輸入一個字符串!如果用戶錯誤地輸入了 "Rock "或 "rOck "怎麼辦?如果用戶錯誤地輸入 "Rock "或 "rOck "怎麼辦?大寫字母很重要,所以它們不會相等。
>>> print("rock" == "Rock")
False
由於大寫字母很重要,所以 "r"
和 "R"
並不相等。一個可能的解決方案是用數字代替。給每個動作分配一個數字可以為我們省去一些麻煩。
ROCK_ACTION = 0
SCISSORS_ACTION = 1
PAPER_ACTION = 2
我們通過分配的數字來引用不同的行動,整數不存在與字符串一樣的比較問題,所以這是可行的。現在讓用戶輸入一個數字,並直接與這些值進行比較。
user_input = input("輸入您的選擇 (石頭[0], 剪刀[1], 布[2]): ")
user_action = int(user_input)
if user_action == ROCK_ACTION:
# 處理 ROCK_ACTION
因為input()
返回一個字符串,需要用int()
把返回值轉換成一個整數。然後可以將輸入值與上面的每個動作進行比較。雖然這樣做效果很好,但它可能依賴於對變量的正確命名。其實有一個更好的方法是使用**enum.IntEnum
**來自定義動作類。
我們使用 enum.IntEnum
創建屬性並給它們分配類似於上面所示的值,將動作歸入它們自己的命名空間,使代碼更有表現力。
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
這創建了一個自定義Action
,可以用它來引用我們支持的不同類型的Action
。它的工作原理是將其中的每個屬性分配給我們指定的值。
兩個動作的比較是直截了當的,現在它們有一個有用的類名與之相關。
>>> Action.Rock == Action.Rock
True
因為成員的值是相同的,所以比較的結果是相等的。類的名稱也使我們想比較兩個動作的意思更加明顯。
注意:要了解更多關於enum的信息,請查看官方文檔[1]。
我們甚至可以從一個 int
創建一個 Action
。
>>> Action.Rock == Action(0)
True
>>> Action(0)
<Action.Rock: 0>
Action 查看傳入的值並返回適當的 Action。因此現在可以把用戶的輸入作為一個int
,並從中創建一個Action,媽媽再也不用擔心拼寫問題了!
雖然剪刀石頭布看起來並不復雜,但仔細考慮玩剪刀石頭布的步驟是很重要的,這樣才能確保我們的程序涵蓋所有可能的情況。對於任何項目,即使是小項目,我們有必要創建一個所需行為的流程圖並圍繞它實現代碼。我們可以用一個列表來達到類似的效果,但它更難捕捉到諸如循環和條件等相關邏輯。
流程圖不需要過於復雜,甚至不需要使用真正的代碼。只要提前描述所需的行為,就可以幫助我們在問題發生之前解決問題
這裡有一個流程圖,描述了一個單一的剪刀石頭布游戲。
每個玩家選擇一個行動,然後確定一個贏家。這個流程圖對於我們所編碼的單個游戲來說是准確的,但對於現實生活中的游戲來說卻不一定准確。在現實生活中,玩家會同時選擇他們的行動,而不是像流程圖中建議的那樣一次一個。
然而,在編碼版本中,這一點是可行的,因為玩家的選擇對電腦是隱藏的,而電腦的選擇對玩家也是隱藏的。兩個玩家可以在不同的時間做出選擇而不影響游戲的公平性。
流程圖可以幫助我們在早期發現可能的錯誤,也可以讓我們看到是否要增加更多的功能。例如這個流程圖,描述了如何重復玩游戲,直到用戶決定停止。
如果不寫代碼,我們可以看到第一個流程圖沒有辦法重復玩。我們可以使用這種繪制流程圖的方法在編程前解決類似的問題,這有助於我們碼出更整潔、更易於管理的代碼!
現在我已經用流程圖概述了程序的流程,我們可以試著組織我們的代碼,使它更接近於所確定的步驟。一個方法是為流程圖中的每個步驟創建一個函數。 其實函數是一種很好的方法,可以將大塊的代碼拆分成更小的、更容易管理的部分。
我們不一定需要為條件檢查的再次播放創建一個函數,但如果我們願意,我們可以。如果我們還沒有,我們可以從導入隨機開始,並定義我們的Action類。
import random
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
接下來定義 get_user_selection()
的代碼,它不接受任何參數並返回一個 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
現在不再需要擔心將來添加或刪除動作的問題了!接下來測試一下,我們可以看到代碼是如何提示用戶並返回一個與用戶輸入值相關的動作。
>>> get_user_selection()
輸入您的選擇 (石頭[0], 剪刀[1], 布[2]): 0
<Action.Rock: 0>
現在我們需要一個函數來獲取計算機的動作。和 get_user_selection()
一樣,這個函數應該不需要參數,並返回一個 Action
。因為 Action
的值范圍是0到2
,所以使用 random.randint()
幫助我們在這個范圍內生成一個隨機數。
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
。
測試該函數,它簡單地返回與隨機數相關的動作。
>>> get_computer_selection()
<Action.Scissors: 2>
看起來還不錯!接下來,需要一個函數來決定輸贏,這個函數將接受兩個參數,用戶的行動和計算機的行動。它只需要將結果顯示在控制台上,而不需要返回任何東西。
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("石頭砸碎剪刀!你輸了。")
這裡決定勝負的寫法與剛開始的代碼非常相似。而現在可以直接比較行動類型,而不必擔心那些討厭的字符串!
我們甚至可以通過向 determinal_winner()
傳遞不同的參數來測試函數,看看會打印出什麼。
>>> determine_winner(Action.Rock, Action.Scissors)
石頭砸碎剪刀!你贏了!
既然我們要從一個數字創建一個動作,如果用戶想用數字3創建一個動作,會發生什麼?(我們定義的最大數字是2)。
>>> Action(3)
ValueError: 3 is not a valid Action
報錯了!這並不是我們希望發生這種情況。接下來可以在流程圖上添加一些邏輯,來補充這個 bug,以確保用戶始終輸入一個有效的選擇。
在用戶做出選擇後立即加入檢查是有意義的。
如果用戶輸入了一個無效的值,那麼我們就重復這個步驟來獲得用戶的選擇。對用戶選擇的唯一真正要求是它在【0, 1, 2】之間的一個數。如果用戶的輸入超出了這個范圍,那麼就會產生一個ValueError異常。我們可以處理這個異常,從而不會向用戶顯示默認的錯誤信息。
現在我們已經定義了一些反映流程圖中的步驟的函數,我們的游戲邏輯就更有條理和緊湊了。這就是我們的while循環現在需要包含的所有內容。
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
。這使得代碼繼續到循環的下一個迭代,而不是跳出該循環。
如果我們看過《生活大爆炸》,那麼我們可能對石頭剪子布蜥蜴斯波克很熟悉。如果沒有,那麼這裡有一張圖,描述了這個游戲和決定勝負的規則。
我們可以使用我們在上面學到的同樣的工具來實現這個游戲。例如,我們可以在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.")
現在,我們可以有一個描述勝利條件的字典,而不是與每個行動相比較。
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]
}
注意,現在每個 Action
都有一個包含可以擊敗的兩個元素的列表。而在基本的 "剪刀石頭布 "
實現中,只有一個元素。
我們寫了 get_user_selection()
來適應新的動作,所以不需要改變該代碼的任何內容。get_computer_selection()
的情況也是如此。由於 Action
的長度發生了變化,隨機數的范圍也將發生變化。
看看現在的代碼有多簡潔,有多容易維護管理!完整程序的完整代碼:
上下滑動查看更多源碼
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
到這裡我們已經用Python代碼實現了rock paper scissors lizard Spock
。接下來你就可以仔細檢查一下,確保我們沒有遺漏任何東西,然後進行一次游戲。
看到這裡,必須點個贊,因為我們剛剛完成了第一個Python游戲。現在,我們知道了如何從頭開始創建剪刀石頭布游戲,而且我可以以最小的代價擴展游戲中可能的行動數量。
[1]
官方文檔: https://docs.python.org/3/library/enum.html
往期精彩回顧
適合初學者入門人工智能的路線及資料下載(圖文+視頻)機器學習入門系列下載中國大學慕課《機器學習》(黃海廣主講)機器學習及深度學習筆記等資料打印《統計學習方法》的代碼復現專輯機器學習交流qq群955171419,加入微信群請掃碼