在一個5x5的方陣上隨機填充1~25的數字。
玩家(使用者) 和 電腦(AI)輪流叫一個號碼,最先占據一整條直線連線的獲勝。
就像小時候玩的bingo 賓果連線遊戲一樣!
(可以是占據兩條對角線的其中一條,可以是占據水平直線,可以是占據垂直直線)
分別為玩家和電腦建立兩個5x5的板子,各自隨機填充1~25的數字。
玩家(人) 和 電腦(AI)輪流叫一個號碼。
號碼必須是有效號碼1~25之間的數字,而且不可重複。
被叫到的號碼就在各自的板子上做一個記號。
最先占據一整條直線連線的那一方宣告獲勝。
建立遊戲場景,分別給電腦和玩家建立兩張5x5的板子,填充1~25的隨機數。
讓玩家 和 電腦AI輪流叫號。
號碼必須是有效號碼1~25之間的數字,而且不可重複。
被叫到的號碼就在個子的板子上做一個記號。
每回合都會檢查是否有其中一方獲勝。如果還沒有,就繼續輪流叫號的過程。
最先占據一整條直線連線的那一方宣告獲勝。
透過List comprehension列表生成式來建立固定大小的5x5 二維陣列,
並且使用python內建的random.sample()方法,
來對1~25的數字隨機採樣,進行填充。
class BingoCard:
numbers_drawn = set()
def __init__(self, name):
self.name = name
# 5x5 賓果遊戲的數字版
self.card = self.generate_card()
# 記錄這個位置的數字有沒有被叫過
self.marked = [[False for _ in range(5)] for _ in range(5)]
def generate_card(self):
# 隨機生成1~25的數字,並且填充
numbers = random.sample(range(1, 26), 25)
card = [numbers[i*5:(i+1)*5] for i in range(5)]
讓玩家透過鍵盤輸入1~25之間的數字。
為了確保遊戲正確執行,會檢查是否為1~25區間內的數字,而且不可重複。
如果有非法輸入,會輸出提示訊息,要求使用者重新輸入!
背後的檢查機制依賴python內建的try ... except ... 的例外處理。
class HumanPlayer(Agent):
def choose_number(self, numbers_drawn):
number = -1
while True:
try:
number = int( input("Please select a number: ") )
except:
print("Invalid input!")
continue
if number == -1 or ( (25 >= number >= 1) and(number not in numbers_drawn) ):
break
elif number in numbers_drawn:
print("Repeated!")
else:
print("Invalid input! Number out of Range 1 ~ 25.")
return number
讓電腦掃描當下的狀態,
計算哪個位置的連線數目最多,優先選擇連線數目最多的那個數字,提高對戰勝率。
class SmartAIPlayer(Agent):
def choose_number(self, numbers_drawn):
best_choice = -1
max_marks = -1
# Greedy strategy, select the number with higher win rate based on connection
for number in range(1, 26, 1):
if number not in numbers_drawn:
i, j = self.board.find_number(number)
self.board.marked[i][j] = True
marks = self.count_marks(i, j)
self.board.marked[i][j] = False
if marks > max_marks:
max_marks = marks
best_choice = number
return best_choice
def count_marks(self, i, j):
# Calculate the number of connection grids
marks = 0
for col in range(5):
marks += self.board.marked[i][col]
for row in range(5):
marks += self.board.marked[row][j]
for k in range(5):
marks += self.board.marked[k][k]
for k in range(5):
marks += self.board.marked[k][k-1]
return marks
透過BingoCard.mark_number()去紀錄被叫的數字,儲存在雙方的板子上。
另外,會依照順序紀錄每個回合被叫到的數字,
儲存在pick_sequence和numbers_drawn。
最後,遊戲結束時,會回放雙方遊戲對戰過程所叫的數字。
# 依照順序紀錄每個回合被叫到的數字
pick_sequence.append(number)
numbers_drawn.add(number)
# 顯示哪一方玩家 叫了哪個數字
print(f"{cur_player.name} pick {number}.")
# 雙方玩家把這次叫到的數字做個記號
human_player.board.mark_number(number)
ai_player.board.mark_number(number)
由BingoCard.display()和BingoCard.show_status()這兩之function負責。
display()會顯示開局的初始狀態
show_status()會顯示當下的遊戲狀態,被叫過的號碼會以★號顯示。
def display(self):
print(f"{self.name}'s Bingo Card:")
for row in self.card:
print("\t".join(str(num).rjust(2) for num in row))
print()
return
def show_status(self):
print(f"\n{self.name}'s status")
for y in range(5):
for x in range(5):
if self.marked[y][x]:
print("★".rjust(2), end="\t")
else:
print(str(self.card[y][x]).rjust(2), end="\t")
print()
print()
return
檢查每一條直線,看哪一方玩家先取得完整的一條連線。
(可以是占據對角線,可以是占據水平直線,可以是占據垂直直線)
def check_bingo(self):
for row in self.marked:
if all(row):
return True
for col in zip(*self.marked):
if all(col):
return True
if all(self.marked[i][i] for i in range(5)) or all(self.marked[i][4-i] for i in range(5)):
return True
return False
由turn 回和數決定,0代表使用者扮演的玩家,1代表電腦AI。
每回合turn輪流在0, 1之間。
while True:
# Player's input
cur_player = players[turn]
print(f"{cur_player.name}'s turn")
number = cur_player.choose_number(numbers_drawn)
# ... 遊戲的叫號、標記遊戲版、和檢查連線的處理機制 ...
# Go to next turn
turn = (turn + 1) % 2
BingoCard 定義一塊5x5的遊戲版,並且帶有對應的遊戲操作函數。
負責隨機生成遊戲版,標記被叫過的號碼,檢查是否已經連成一直線。
Agent代表 遊戲玩家,並且擁有一塊專屬的5x5遊戲版。
Agent定義 choose_number 叫號的抽象函數介面,要求繼承者實作。
HumanPlayer繼承 Agent,代表 人扮演的玩家,透過鍵盤輸入叫號。
SmartAIPlayer繼承 Agent,代表 電腦AI,但過演算法計算,
選擇連線數目最高的號碼叫號。
import random
import abc
class BingoCard:
numbers_drawn = set()
def __init__(self, name):
self.name = name
self.card = self.generate_card()
self.marked = [[False for _ in range(5)] for _ in range(5)]
def generate_card(self):
numbers = random.sample(range(1, 26), 25)
card = [numbers[i*5:(i+1)*5] for i in range(5)]
return card
def mark_number(self, number):
for i in range(5):
for j in range(5):
if self.card[i][j] == number:
self.marked[i][j] = True
return self.marked[i][j]
def find_number(self, number):
for i in range(5):
for j in range(5):
if self.card[i][j] == number:
return i, j
return
def check_bingo(self):
for row in self.marked:
if all(row):
return True
for col in zip(*self.marked):
if all(col):
return True
if all(self.marked[i][i] for i in range(5)) or all(self.marked[i][4-i] for i in range(5)):
return True
return False
def display(self):
print(f"{self.name}'s Bingo Card:")
for row in self.card:
print("\t".join(str(num).rjust(2) for num in row))
print()
return
def show_status(self):
print(f"\n{self.name}'s status")
for y in range(5):
for x in range(5):
if self.marked[y][x]:
print("★".rjust(2), end="\t")
else:
print(str(self.card[y][x]).rjust(2), end="\t")
print()
print()
return
class Agent:
def __init__(self, name):
self.name = name
self.board = BingoCard(name)
@abc.abstractmethod
def choose_number(self, numbers_drawn):
pass
raise NotImplementedError
return
class HumanPlayer(Agent):
def choose_number(self, numbers_drawn):
number = -1
while True:
try:
number = int( input("Please select a number: ") )
except:
print("Invalid input!")
continue
if number == -1 or ( (25 >= number >= 1) and(number not in numbers_drawn) ):
break
elif number in numbers_drawn:
print("Repeated!")
else:
print("Invalid input! Number out of Range 1 ~ 25.")
return number
class SmartAIPlayer(Agent):
def choose_number(self, numbers_drawn):
best_choice = -1
max_marks = -1
# Greedy strategy, select the number with higher win rate based on connection
for number in range(1, 26, 1):
if number not in numbers_drawn:
i, j = self.board.find_number(number)
self.board.marked[i][j] = True
marks = self.count_marks(i, j)
self.board.marked[i][j] = False
if marks > max_marks:
max_marks = marks
best_choice = number
return best_choice
def count_marks(self, i, j):
# Calculate the number of connection grids
marks = 0
for col in range(5):
marks += self.board.marked[i][col]
for row in range(5):
marks += self.board.marked[row][j]
for k in range(5):
marks += self.board.marked[k][k]
for k in range(5):
marks += self.board.marked[k][k-1]
return marks
def check_game_finished(player_board, ai_board):
for board in [player_board, ai_board]:
if board.check_bingo():
print(f"Congratulation! {board.name} got Bingo!")
print()
ai_board.show_status()
ai_board.display()
return True
return False
def play_bingo():
human_player = HumanPlayer("Player")
ai_player = SmartAIPlayer("AI")
pick_sequence = []
numbers_drawn = BingoCard.numbers_drawn
human_player.board.display()
players = [human_player, ai_player]
turn = 0
while True:
# Player's input
cur_player = players[turn]
print(f"{cur_player.name}'s turn")
number = cur_player.choose_number(numbers_drawn)
if number == -1:
if cur_player == human_player:
print("You give up")
else:
print("Tie")
print('Game over')
break
pick_sequence.append(number)
numbers_drawn.add(number)
print(f"{cur_player.name} pick {number}.")
human_player.board.mark_number(number)
ai_player.board.mark_number(number)
# Show player's Status
human_player.board.show_status()
# Check game is finished or not
if check_game_finished(human_player.board, ai_player.board):
print(f"Game sequence replay: {pick_sequence}")
break
# Go to next turn
turn = (turn + 1) % 2
if __name__ == "__main__":
play_bingo()
如果對程式或者演算法有興趣的同學,
可以試著觀察bingo遊戲的規律,
開發更強的AI叫號演算法!
甚至可以建立兩個AI,讓電腦去對戰,看哪種叫號演算法的勝率較高。
很有趣喔~