2024-08-12|閱讀時間 ‧ 約 24 分鐘

📝⭐回憶殺 python實現 賓果(Bingo)連線遊戲 可線上玩

raw-image

賓果的遊戲描述


在一個5x5的方陣上隨機填充1~25的數字。

玩家(使用者) 和 電腦(AI)輪流叫一個號碼最先占據一整條直線連線的獲勝


就像小時候玩的bingo 賓果連線遊戲一樣!

(可以是占據兩條對角線的其中一條,可以是占據水平直線,可以是占據垂直直線)


賓果的遊戲場景:


分別為玩家和電腦建立兩個5x5的板子,各自隨機填充1~25的數字。


玩家(人) 和 電腦(AI)輪流叫一個號碼。

號碼必須是有效號碼1~25之間的數字,而且不可重複。

被叫到的號碼就在各自的板子上做一個記號。


先占據一整條直線連線的那一方宣告獲勝。



上層思考邏輯與框架


建立遊戲場景,分別給電腦和玩家建立兩張5x5的板子,填充1~25的隨機數


玩家 和 電腦AI輪流叫號

號碼必須是有效號碼1~25之間的數字,而且不可重複。

被叫到的號碼就在個子的板子上做一個記號


每回合都會檢查是否有其中一方獲勝。如果還沒有,就繼續輪流叫號的過程。


先占據一整條直線連線的那一方宣告獲勝。


中層功能元件分析


1.怎麼建立5x5的遊戲版,並且填充隨機數?


透過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)]

2.如何讓使用者扮演的玩家叫號?


讓玩家透過鍵盤輸入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


3.如何讓電腦扮演的AI叫號?


讓電腦掃描當下的狀態,
計算哪個位置的連線數目最多,優先選擇連線數目最多的那個數字,提高對戰勝率


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


4.如何記錄賓果遊戲狀態?


透過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)

5.如何顯示賓果遊戲狀態?


由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


6.如何判斷已經有一方獲勝?


檢查每一條直線,看哪一方玩家先取得完整的一條連線。

(可以是占據對角線,可以是占據水平直線,可以是占據垂直直線)


  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



7.如何在兩個玩家之間切換,實現輪流的機制?


由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

8.如何使用OOP物件導向程式設計,實現模組化和程式碼共用?


BingoCard 定義一塊5x5的遊戲版,並且帶有對應的遊戲操作函數。
負責隨機生成遊戲版,標記被叫過的號碼,檢查是否已經連成一直線。


Agent代表 遊戲玩家,並且擁有一塊專屬的5x5遊戲版。

Agent定義 choose_number 叫號的抽象函數介面要求繼承者實作


HumanPlayer繼承 Agent,代表 人扮演的玩家,透過鍵盤輸入叫號。


SmartAIPlayer繼承 Agent,代表 電腦AI,但過演算法計算,
選擇連線數目最高的號碼叫號。


底層的完整實作: 賓果遊戲 (Bingo連線遊戲)

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()

試玩畫面


點這行 線上執行 與 試玩(在新的頁面按Run開始玩)


延伸思考:


如果對程式或者演算法有興趣的同學,

可以試著觀察bingo遊戲的規律,

開發更強的AI叫號演算法!

甚至可以建立兩個AI,讓電腦去對戰,看哪種叫號演算法的勝率較高。

很有趣喔~

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.