iT邦幫忙

3

【人機對戰】用python打造經典黑白棋遊戲

黑白棋是一個經典的棋盤遊戲,
只要下棋的地方可以夾住對手的棋子,
就可以把對手的棋子變成自己的顏色,
可參考黑白棋- wiki的介紹,

我的程式實作主要是參考「參考資料一」,
其實蠻久以前就讀過這支程式了,
只是現在自己對python還蠻熟悉的,
發現程式碼有非常多可以簡化的地方,
決定來改寫成自己的版本

雖然原程式目前是可以玩黑白棋的,
但是我覺得它的邏輯還不夠簡明,
原程式將所有的邏輯都混在一起寫。

並且我想要寫可以隨意改變棋盤大小的一般化版本,
故我又將程式邏輯整個重新改寫

重新改寫整個架構

我的版本會將程式邏輯拆成三個部分:

  1. 黑白棋遊戲的基本邏輯- 知道一個盤面可以走的棋步有哪些
  2. 電腦ai下棋的邏輯
  3. 互動程式的邏輯

並且「電腦ai下棋的邏輯」和「互動程式的邏輯」都繼承「黑白棋遊戲的基本邏輯」

因為「電腦ai下棋的邏輯」就是從所有可以走的棋步裡面,
選擇最好的一步來走,
自然電腦應該要知道哪些是「可以走的棋步」。

而「互動程式的邏輯」需要判斷玩家輸入的棋步是否合法,
也需要知道黑白棋規則

基礎架構如下:

# 寫黑白棋遊戲的基本邏輯,棋子共'X','O'兩種
class Reversi():
    pass

# 電腦ai下棋的邏輯
class ReversiAI(Reversi):
    pass

# 寫互動程式的邏輯
class Game(Reversi):
    pass

我用物件導向的方向,
將程式邏輯拆解成三塊,
我認為會樣程式邏輯會清楚一些

以下針對三塊邏輯來講

1. 黑白棋遊戲的基本邏輯

基礎的class是Reversi這個class,
主要用來寫黑白棋遊戲的基本邏輯,
就是專心寫黑白棋遊戲的規則,
譬如說現在看到一個棋盤盤面,輪到黑棋下,
有沒有辦法知道所有黑棋可以下棋的地方?

至於現在是誰的回合(換玩家?電腦?輪黑棋走?輪白棋走?),
這部分就留到互動程式的邏輯來寫就可以

程式:

# 寫黑白棋遊戲的基本邏輯,棋子共'X','O'兩種
class Reversi():
    def __init__(self, height, width):
        self.width = width
        self.height = height
        self.board = [[' ']*self.height for i in range(self.width)]
    
    # 初始化棋盤
    def iniBoard(self):
        for i in range(self.width):
            for j in range(self.height):
                self.board[i][j]=' '
        W, H = self.width//2 , self.height//2
        self.board[W-1][H-1]='X'
        self.board[W-1][H]='O'
        self.board[W][H-1]='O'
        self.board[W][H]='X'
        
    def drawBoard(self, hints = None) -> None:
        HLINE =  ' ' * 3 + '+---' * self.width  + '+'
        VLINE = (' ' * 3 +'|') *  (self.width +1)
        title = '     1'
        for i in range(1,self.width):
            title += ' ' * 3 +str(i+1)
        print(title)
        print(HLINE)
        for y in range(self.height):
            print(VLINE)
            print(y+1, end='  ')
            for x in range(self.width):
                if hints and [x,y] in hints:
                    print(f'| *', end=' ')
                else:
                    print(f'| {self.board[x][y]}', end=' ')
            print('|')
            print(VLINE)
            print(HLINE)
    
    def isOnBoard(self, x, y):
        return 0 <= x < self.width and 0 <= y < self.height

    #檢查tile放在某個座標是否為合法棋步,如果是則回傳被翻轉的棋子
    def isValidMove(self, tile, xstart, ystart):
        if not self.isOnBoard(xstart, ystart) or self.board[xstart][ystart]!=' ':
            return []
        self.board[xstart][ystart] = tile # 暫時放置棋子
        otherTile = 'O'  if tile == 'X' else 'X'
        tilesToFlip = [] # 合法棋步
        dirs = [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]] # 定義八個方向
        for xdir, ydir in dirs:
            x, y = xstart+xdir, ystart+ydir
            while self.isOnBoard(x, y) and self.board[x][y] == otherTile:
                x += xdir
                y += ydir
                # 夾到對手的棋子了,回頭記錄被翻轉的對手棋子
                if self.isOnBoard(x, y) and self.board[x][y] == tile:
                    while True:
                        x -= xdir
                        y -= ydir
                        if x == xstart and y == ystart:
                            break
                        tilesToFlip.append([x, y])
                        
        self.board[xstart][ystart] = ' ' # 重設為空白
        return tilesToFlip

    # 若將tile放在xstart, ystart是合法行動,放置棋子
    # 回傳被翻轉的棋子(用來電腦算棋時可以把棋子翻回來)
    def makeMove(self, tile, xstart, ystart):
        tilesToFlip = self.isValidMove(tile, xstart, ystart)
        if tilesToFlip:
            self.board[xstart][ystart] = tile
            for x, y in tilesToFlip:
                self.board[x][y] = tile
        return tilesToFlip

    # 回傳現在盤面輪到tile走的所有合法棋步
    def getValidMoves(self, tile):
        return [[x, y] for x in range(self.width) for y in range(self.height) if self.isValidMove(tile, x, y)]
    
    # 計算當前比分
    def getScoreOfBoard(self)-> dict:
        scores = {'X':0, 'O':0}
        for x in range(self.width):
            for y in range(self.height):
                tile = self.board[x][y]
                if tile in scores:
                    scores[tile] += 1
        return scores

2. 電腦ai下棋的邏輯

這部分也很單純,
就是看到一個棋盤盤面,
設計一個演算法讓電腦決定下一步要走的棋

這樣如果你想要寫一個厲害的黑白棋ai,
就改這部分的邏輯就好

目前實作最簡單、單純的演算法:
若電腦的下一步可以佔據角落,
就優先占角,
否則選擇可以翻轉對手最多棋子的一步

程式:

# 電腦ai下棋的邏輯
class ReversiAI(Reversi):
    def __init__(self, board, height, width):
        super().__init__(height, width)
        self.board = board

    def isOnCorner(self, x, y):
        return x in {0, self.width-1} and y in {0, self.height-1}
 
    # 給定盤面board,回傳電腦的選擇
    def getComputerMove(self, computerTile):
        possibleMoves = self.getValidMoves(computerTile)
        random.shuffle(possibleMoves) # 隨機性
        
        # 若能占角為優先
        for x, y in possibleMoves:
            if self.isOnCorner(x, y):
                return [x, y]
            
        # 找能夠吃子最多的棋步
        bestScore, bestMove = -1, None
        for x, y in possibleMoves:
            flips = self.makeMove(computerTile, x, y)
            score = self.getScoreOfBoard()[computerTile]
            if score > bestScore:
                bestScore, bestMove = score, [x, y]
            # 還原棋盤
            self.board[x][y] = ' '
            otherTile = 'O'  if computerTile == 'X' else 'X'
            for x, y in flips:
                self.board[x][y] = otherTile
        return bestMove

3. 互動程式的邏輯

這部分用來寫玩家跟電腦對戰的邏輯(目前總是讓玩家先走),
像是現在是誰的回合,
以及取得使用者輸入的棋步,
判斷棋局是否結束、判斷勝負都寫在這邊

這邊要強調「棋局結束」的條件是「雙方都沒辦法走棋了」,
並非「棋盤空格全部填滿」,
有可能棋盤空格沒有填滿,雙方也都有棋子,
但雙方就是沒辦法走了

至於為什麼「判斷勝負」不在黑白棋遊戲的基本邏輯的範疇,
是因為可能你想做變形規則,
譬如說規定棋局結束時,「棋子最少的一方贏」,
或者「先占到角落的人獲勝」等等,
但是基本走棋的規則都是一樣的,
因此我覺得黑白棋遊戲的基本邏輯都負責告訴我們一個盤面的合法棋步有哪些就好

程式:

# 寫互動程式的邏輯
class Game(Reversi):
    def __init__(self, height, width):
        super().__init__(height, width)
        self.turn = 'player'
        self.ai = ReversiAI(self.board,self.height, self.width)

    # 詢問玩家是否再玩一次
    def playAgain(self)-> bool:
        return input('你想再玩一次嗎?(輸入y或n)').lower().startswith('y')

    # 取得玩家的行動,回傳棋步[x, y](或'hints', 'quit'))
    def getPlayerMove(self, playerTile):
        DIGITS = [str(i) for i in range(1,10)]
        while True:
            move = input('請輸入棋步(先輸入x座標再輸入y座標),例如11是左上角。(或輸入hints或quit)').lower()
            if move in {'quit', 'hints'}:
                return move
            if len(move) == 2 and move[0] in DIGITS and move[1] in DIGITS:
                x = int(move[0]) - 1
                y = int(move[1]) - 1
                if self.isValidMove(playerTile, x, y):
                    break
            print('非合法棋步,請再試一次')
        return [x, y]

    # 顯示目前比分
    def showPoints(self, playerTile, computerTile):
        scores = self.getScoreOfBoard()
        print(f'You have {scores[playerTile]} points. The computer has {scores[computerTile]} points.')

    def gameloop(self):
        print("歡迎玩黑白棋(玩家的棋子為'X')")

        while True:
            # 初始化棋盤
            self.iniBoard()
            playerTile, computerTile = ['X', 'O']
            showHints = False
            print('玩家先手' if self.turn == 'player' else '電腦先手')
            
            while True:
                playerValidMoves = self.getValidMoves(playerTile)
                computerValidMoves = self.getValidMoves(computerTile)
                # 若無人可行動,結束遊戲
                if not playerValidMoves and not computerValidMoves:
                    break
                
                if self.turn == 'player' and playerValidMoves:
                    if showHints:
                        self.drawBoard(playerValidMoves)
                    else:
                        self.drawBoard()
                    self.showPoints(playerTile, computerTile)
                    move = self.getPlayerMove(playerTile)
                    if move == 'quit':
                        print('Thanks for playing!')
                        sys.exit() # terminate the program
                    elif move == 'hints':
                        showHints = not showHints
                        continue
                    else:
                        self.makeMove(playerTile, move[0], move[1])
                elif self.turn == 'computer' and computerValidMoves:
                    self.drawBoard()
                    self.showPoints(playerTile, computerTile)
                    input('按enter看電腦的下一步')
                    x, y = self.ai.getComputerMove(computerTile)
                    self.makeMove(computerTile, x, y)
                self.turn = 'player' if self.turn=='computer' else 'computer'
                        
            
            # 顯示最後結果
            self.drawBoard()
            scores = self.getScoreOfBoard()
            print(f"X scored {scores['X']} points. O scored {scores['O']} points.")
            if scores[playerTile] > scores[computerTile]:
                print("恭喜你贏電腦了")
            elif scores[playerTile] < scores[computerTile]:
                print("你輸了")
            else:
                print('平手')
                
            if not self.playAgain():
                break

主程式

主程式非常單純,
宣告一個「遊戲」執行它即可

譬如說你想玩棋盤大小6x6的黑白棋就宣告reversi = Game(6,6)
想玩棋盤大小8x8的黑白棋就宣告reversi = Game(8,8)
相當方便操作

reversi = Game(6,6)
reversi.gameloop()

完整程式碼

import random
import sys

# 寫黑白棋遊戲的基本邏輯,棋子共'X','O'兩種
class Reversi():
    def __init__(self, height, width):
        self.width = width
        self.height = height
        self.board = [[' ']*self.height for i in range(self.width)]
    
    # 初始化棋盤
    def iniBoard(self):
        for i in range(self.width):
            for j in range(self.height):
                self.board[i][j]=' '
        W, H = self.width//2 , self.height//2
        self.board[W-1][H-1]='X'
        self.board[W-1][H]='O'
        self.board[W][H-1]='O'
        self.board[W][H]='X'
        
    def drawBoard(self, hints = None) -> None:
        HLINE =  ' ' * 3 + '+---' * self.width  + '+'
        VLINE = (' ' * 3 +'|') *  (self.width +1)
        title = '     1'
        for i in range(1,self.width):
            title += ' ' * 3 +str(i+1)
        print(title)
        print(HLINE)
        for y in range(self.height):
            print(VLINE)
            print(y+1, end='  ')
            for x in range(self.width):
                if hints and [x,y] in hints:
                    print(f'| *', end=' ')
                else:
                    print(f'| {self.board[x][y]}', end=' ')
            print('|')
            print(VLINE)
            print(HLINE)
    
    def isOnBoard(self, x, y):
        return 0 <= x < self.width and 0 <= y < self.height

    #檢查tile放在某個座標是否為合法棋步,如果是則回傳被翻轉的棋子
    def isValidMove(self, tile, xstart, ystart):
        if not self.isOnBoard(xstart, ystart) or self.board[xstart][ystart]!=' ':
            return []
        self.board[xstart][ystart] = tile # 暫時放置棋子
        otherTile = 'O'  if tile == 'X' else 'X'
        tilesToFlip = [] # 合法棋步
        dirs = [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]] # 定義八個方向
        for xdir, ydir in dirs:
            x, y = xstart+xdir, ystart+ydir
            while self.isOnBoard(x, y) and self.board[x][y] == otherTile:
                x += xdir
                y += ydir
                # 夾到對手的棋子了,回頭記錄被翻轉的對手棋子
                if self.isOnBoard(x, y) and self.board[x][y] == tile:
                    while True:
                        x -= xdir
                        y -= ydir
                        if x == xstart and y == ystart:
                            break
                        tilesToFlip.append([x, y])
                        
        self.board[xstart][ystart] = ' ' # 重設為空白
        return tilesToFlip

    # 若將tile放在xstart, ystart是合法行動,放置棋子
    # 回傳被翻轉的棋子(用來電腦算棋時可以把棋子翻回來)
    def makeMove(self, tile, xstart, ystart):
        tilesToFlip = self.isValidMove(tile, xstart, ystart)
        if tilesToFlip:
            self.board[xstart][ystart] = tile
            for x, y in tilesToFlip:
                self.board[x][y] = tile
        return tilesToFlip

    # 回傳現在盤面輪到tile走的所有合法棋步
    def getValidMoves(self, tile):
        return [[x, y] for x in range(self.width) for y in range(self.height) if self.isValidMove(tile, x, y)]
    
    # 計算當前比分
    def getScoreOfBoard(self)-> dict:
        scores = {'X':0, 'O':0}
        for x in range(self.width):
            for y in range(self.height):
                tile = self.board[x][y]
                if tile in scores:
                    scores[tile] += 1
        return scores

# 電腦ai下棋的邏輯
class ReversiAI(Reversi):
    def __init__(self, board, height, width):
        super().__init__(height, width)
        self.board = board

    def isOnCorner(self, x, y):
        return x in {0, self.width-1} and y in {0, self.height-1}
 
    # 給定盤面board,回傳電腦的選擇
    def getComputerMove(self, computerTile):
        possibleMoves = self.getValidMoves(computerTile)
        random.shuffle(possibleMoves) # 隨機性
        
        # 若能占角為優先
        for x, y in possibleMoves:
            if self.isOnCorner(x, y):
                return [x, y]
            
        # 找能夠吃子最多的棋步
        bestScore, bestMove = -1, None
        for x, y in possibleMoves:
            flips = self.makeMove(computerTile, x, y)
            score = self.getScoreOfBoard()[computerTile]
            if score > bestScore:
                bestScore, bestMove = score, [x, y]
            # 還原棋盤
            self.board[x][y] = ' '
            otherTile = 'O'  if computerTile == 'X' else 'X'
            for x, y in flips:
                self.board[x][y] = otherTile
        return bestMove


# 寫互動程式的邏輯
class Game(Reversi):
    def __init__(self, height, width):
        super().__init__(height, width)
        self.turn = 'player'
        self.ai = ReversiAI(self.board,self.height, self.width)

    # 詢問玩家是否再玩一次
    def playAgain(self)-> bool:
        return input('你想再玩一次嗎?(輸入y或n)').lower().startswith('y')

    # 取得玩家的行動,回傳棋步[x, y](或'hints', 'quit'))
    def getPlayerMove(self, playerTile):
        DIGITS = [str(i) for i in range(1,10)]
        while True:
            move = input('請輸入棋步(先輸入x座標再輸入y座標),例如11是左上角。(或輸入hints或quit)').lower()
            if move in {'quit', 'hints'}:
                return move
            if len(move) == 2 and move[0] in DIGITS and move[1] in DIGITS:
                x = int(move[0]) - 1
                y = int(move[1]) - 1
                if self.isValidMove(playerTile, x, y):
                    break
            print('非合法棋步,請再試一次')
        return [x, y]

    # 顯示目前比分
    def showPoints(self, playerTile, computerTile):
        scores = self.getScoreOfBoard()
        print(f'You have {scores[playerTile]} points. The computer has {scores[computerTile]} points.')

    def gameloop(self):
        print("歡迎玩黑白棋(玩家的棋子為'X')")

        while True:
            # 初始化棋盤
            self.iniBoard()
            playerTile, computerTile = ['X', 'O']
            showHints = False
            print('玩家先手' if self.turn == 'player' else '電腦先手')
            
            while True:
                playerValidMoves = self.getValidMoves(playerTile)
                computerValidMoves = self.getValidMoves(computerTile)
                # 若無人可行動,結束遊戲
                if not playerValidMoves and not computerValidMoves:
                    break
                
                if self.turn == 'player' and playerValidMoves:
                    if showHints:
                        self.drawBoard(playerValidMoves)
                    else:
                        self.drawBoard()
                    self.showPoints(playerTile, computerTile)
                    move = self.getPlayerMove(playerTile)
                    if move == 'quit':
                        print('Thanks for playing!')
                        sys.exit() # terminate the program
                    elif move == 'hints':
                        showHints = not showHints
                        continue
                    else:
                        self.makeMove(playerTile, move[0], move[1])
                elif self.turn == 'computer' and computerValidMoves:
                    self.drawBoard()
                    self.showPoints(playerTile, computerTile)
                    input('按enter看電腦的下一步')
                    x, y = self.ai.getComputerMove(computerTile)
                    self.makeMove(computerTile, x, y)
                self.turn = 'player' if self.turn=='computer' else 'computer'
                        
            
            # 顯示最後結果
            self.drawBoard()
            scores = self.getScoreOfBoard()
            print(f"X scored {scores['X']} points. O scored {scores['O']} points.")
            if scores[playerTile] > scores[computerTile]:
                print("恭喜你贏電腦了")
            elif scores[playerTile] < scores[computerTile]:
                print("你輸了")
            else:
                print('平手')
                
            if not self.playAgain():
                break

reversi = Game(4,4)
reversi.gameloop()

遊戲範例

這邊方便示範,將黑白棋的棋盤大小調成4x4:

歡迎玩黑白棋(玩家的棋子為'X')
玩家先手
     1   2   3   4
   +---+---+---+---+
   |   |   |   |   |
1  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
2  |   | X | O |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
3  |   | O | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
4  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
You have 2 points. The computer has 2 points.
請輸入棋步(先輸入x座標再輸入y座標),例如11是左上角。(或輸入hints或quit)31
     1   2   3   4
   +---+---+---+---+
   |   |   |   |   |
1  |   |   | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
2  |   | X | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
3  |   | O | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
4  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
You have 4 points. The computer has 1 points.
按enter看電腦的下一步
     1   2   3   4
   +---+---+---+---+
   |   |   |   |   |
1  |   |   | X | O |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
2  |   | X | O |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
3  |   | O | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
4  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
You have 3 points. The computer has 3 points.
請輸入棋步(先輸入x座標再輸入y座標),例如11是左上角。(或輸入hints或quit)42     
     1   2   3   4
   +---+---+---+---+
   |   |   |   |   |
1  |   |   | X | O |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
2  |   | X | X | X |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
3  |   | O | X |   |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
4  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
You have 5 points. The computer has 2 points.
按enter看電腦的下一步
     1   2   3   4
   +---+---+---+---+
   |   |   |   |   |
1  |   |   | X | O |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
2  |   | X | X | O |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
3  |   | O | O | O |
   |   |   |   |   |
   +---+---+---+---+
   |   |   |   |   |
4  |   |   |   |   |
   |   |   |   |   |
   +---+---+---+---+
You have 3 points. The computer has 5 points.
請輸入棋步(先輸入x座標再輸入y座標),例如11是左上角。(或輸入hints或quit)

參考資料

  1. THE REVERSEGAM GAME

尚未有邦友留言

立即登入留言