屏風式四子棋(Connect Four),簡稱四子棋,是Howard Wexler在1974年推出的連棋類遊戲。

Connect 40. 本章重點1. 建立spot class, gameBoard class和遊戲畫面2. 加入mouse操控3. 測試落子4. 檢查是否勝出5. 加入game over和you win
怎樣用演算法去製這一個會捉棋的AI。
spot class, gameBoard class和遊戲畫面connectFour.pyde:
x1from spot import *2from gameBoard import *3
4gameBoard = 05
6def setup():7 global grids, gameBoard8
9 size(700, 600)10 ellipseMode(CENTER)11 frameRate(10)12
13 gameBoard = GameBoard()14
15def draw():16 background(200)17 gameBoard.display()gameBoard.py:
xxxxxxxxxx151from spot import *2
3class GameBoard(object):4 def __init__(self ): 5 self.grids = []6 for i in range(6):7 temp = []8 for j in range(7):9 temp.append(Spot(i, j, width/7*(j+.5),height/6*(i+.5), ''))10 self.grids.append(temp)11 12 def display(self):13 for i in range(6):14 for j in range(7):15 self.grids[i][j].display()spot.py:
xxxxxxxxxx211class Spot(object):2 def __init__(self, _i, _j, _x, _y, _value):3 self.i = _i4 self.j = _j5 self.x = _x6 self.y = _y7 self.value = ''8
9 def display(self):10 stroke('#000000')11 strokeWeight(2)12 fill(self.matchColor(self.value))13 ellipse(self.x, self.y, 80, 80)14
15 def matchColor(self, _value):16 if _value == '':17 return '#FFFFFF'18 elif _value == 'R':19 return '#FF5641'20 elif _value == 'Y':21 return '#FFDF37' 
跟之前的俄羅斯方塊一樣,我們用一個叫GameBoard的class來裝起整個遊戲。之後要寫人工智能去跟玩家遊玩的話會容易後多。
xxxxxxxxxx71class Spot(object):2 def __init__(self, _i, _j, _x, _y, _value):3 self.i = _i4 self.j = _j5 self.x = _x6 self.y = _y7 self.value = ''在Spot這個class中,i和j是用來紀錄畫面的col和row的,而x和y則是其座標,方便直接顯示;而value則是用來紀錄其內容,沒有填入任任何顏色時就是'',填入黃色和紅色的話則分別是Y和R,用字符去紀錄。
connect.pyde:
xxxxxxxxxx311from spot import *2from gameBoard import *3
4gameBoard = 05
6
7def setup():8 global grids, gameBoard9
10 size(700, 600)11 ellipseMode(CENTER)12 frameRate(10)13
14 gameBoard = GameBoard()15
16
17def draw():18 background(200)19 gameBoard.display()20
21
22def mousePressed():23 global gameBoard24 for i in range(7):25 if (mouseX > width/7*i and mouseX < width/7*(i+1)):26 if gameBoard.colHeight[i] < 6:27 gameBoard.colHeight[i] += 128 gameBoard.currentCol = i29 gameBoard.currentRow = 0 30 31 print(gameBoard.colHeight)gameBoard.py:
xxxxxxxxxx211from spot import *2
3
4class GameBoard(object):5
6 def __init__(self):7 self.grids = []8 for i in range(6):9 temp = []10 for j in range(7):11 temp.append(Spot(i, j, width/7*(j+.5), height/6*(i+.5), ''))12 self.grids.append(temp)13
14 self.currentRow = 015 self.currnetCol = 016 self.colHeight = [0, 0, 0, 0, 0, 0, 0]17
18 def display(self):19 for i in range(6):20 for j in range(7):21 self.grids[i][j].display()spot.py沒有改變。

首先在gameBoard中,
xxxxxxxxxx61class GameBoard(object):2 def __init__(self):3 #===============other codes==============4 self.currentRow = 05 self.currnetCol = 06 self.colHeight = [0, 0, 0, 0, 0, 0, 0]加入3個變數,currnetCol為用來紀錄之後選擇要入棋的欄,而currentRow是用來紀錄該疊高到哪一個格,colHeight則用來紀錄每一欄的高度,每填一粒棋的話該行的高度就加一。
在主程式中,
xxxxxxxxxx101def mousePressed():2 global gameBoard3 for i in range(7):4 if (mouseX > width/7*i and mouseX < width/7*(i+1)):5 if gameBoard.colHeight[i] < 6:6 gameBoard.colHeight[i] += 17 gameBoard.currentCol = i8 gameBoard.currentRow = 0 9 10 print(gameBoard.colHeight)加入一個滑鼠函數,如果滑鼠在相對應的位置按下按鍵,則將currentCol設為i,即你按下的欄數,currentRow暫時設定為0,而colHeight則將相對應的欄加1。之後print colHeight出來debug。
connectFour.pyde:
xxxxxxxxxx261from spot import *2from gameBoard import *3
4gameBoard = 05
6
7def setup():8 global grids, gameBoard9
10 size(700, 600)11 ellipseMode(CENTER)12 frameRate(10)13
14 gameBoard = GameBoard()15
16
17def draw():18 background(200)19 gameBoard.display()20
21
22def mousePressed():23 global gameBoard24 for i in range(7):25 if (mouseX > width/7*i and mouseX < width/7*(i+1)):26 gameBoard.trigger(i)gameBoard.py:
xxxxxxxxxx361from spot import *2
3
4class GameBoard(object):5
6 def __init__(self):7 self.grids = []8 for i in range(6):9 temp = []10 for j in range(7):11 temp.append(Spot(i, j, width/7*(j+.5), height/6*(i+.5), ''))12 self.grids.append(temp)13
14 self.currentRow = 015 self.currentCol = 016 self.colHeight = [0, 0, 0, 0, 0, 0, 0]17 self.currentPlayer = 'R'18
19 def display(self):20 for i in range(6):21 for j in range(7):22 self.grids[i][j].display()23
24 def swapPlayer(self):25 if self.currentPlayer == 'R':26 self.currentPlayer = 'Y'27 elif self.currentPlayer == 'Y':28 self.currentPlayer = 'R'29
30 def trigger(self, i):31 if self.colHeight[i] < 6:32 self.colHeight[i] += 133 self.currentCol = i34 self.currentRow = 6 - self.colHeight[i]35 self.grids[self.currentRow][self.currentCol].value = self.currentPlayer36 self.swapPlayer()spot.py沒有改變。

在主程式中,
xxxxxxxxxx51def mousePressed():2 global gameBoard3 for i in range(7):4 if (mouseX > width/7*i and mouseX < width/7*(i+1)):5 gameBoard.trigger(i)將原本直接測試colHeight是否能夠相加堆疊的功能,轉成一個gameBoard class的函數,方便之後跟電腦對玩時電腦操作。
在gameBoard.py中,先看看最下面的trigger()函數,
xxxxxxxxxx71def trigger(self, i):2 if self.colHeight[i] < 6:3 self.colHeight[i] += 14 self.currentCol = i5 self.currentRow = 6 - self.colHeight[i]6 self.grids[self.currentRow][self.currentCol].value = self.currentPlayer7 self.swapPlayer()承上面主程式的gameBoard.trigger(),在這個class中,開一個叫trigger()的函數,就是落棋落在哪一欄。如果該欄未滿(if self.colHeight[i] < 6:),就將該欄的高度self.colHeight[i]加一,currentCol指定在這一行,currentRow直接指定為6-colHeight[i],即該欄堆疊的高度,之後就將該個格填成currentPlayer,這個變數只有R和Y兩個值,用來紀錄現在的回合是哪個顏色,最後就用swapPlayer()交換顏色。
xxxxxxxxxx51def swapPlayer(self):2 if self.currentPlayer == 'R':3 self.currentPlayer = 'Y'4 elif self.currentPlayer == 'Y':5 self.currentPlayer = 'R'而上面的swapPlayer()函數,則是用來交換R和Y兩個值。
最後記得在最上面的__init()__中,加入self.currentPlayer = 'R'紅子先下棋。原本我的設計中,是有動畫做落子動作,模擬現實情況棋子會一步一步落下的,但為簡潔起見,這次的遊戲重點並不在此,就不做這個功能了,如果你有與趣做到一步一步落棋的效果,可以參考上一章tetris遊戲。
connectFour.pyde:
xxxxxxxxxx281from spot import *2from gameBoard import *3
4gameBoard = 05
6
7def setup():8 global grids, gameBoard9
10 size(700, 600)11 ellipseMode(CENTER)12 frameRate(10)13
14 gameBoard = GameBoard()15
16
17def draw():18 background(200)19 gameBoard.display()20 21
22def mousePressed():23 global gameBoard24 for i in range(7):25 if (mouseX > width/7*i and mouseX < width/7*(i+1)):26 gameBoard.trigger(i)27 winner = gameBoard.checkWin()28 print(winner)gameBoard.py:
xxxxxxxxxx671from spot import *2
3
4class GameBoard(object):5
6 def __init__(self):7 self.grids = []8 for i in range(6):9 temp = []10 for j in range(7):11 temp.append(Spot(i, j, width/7*(j+.5), height/6*(i+.5), ''))12 self.grids.append(temp)13
14 self.currentRow = 015 self.currentCol = 016 self.colHeight = [0, 0, 0, 0, 0, 0, 0]17 self.currentPlayer = 'R'18
19 def display(self):20 for i in range(6):21 for j in range(7):22 self.grids[i][j].display()23
24 def swapPlayer(self):25 if self.currentPlayer == 'R':26 self.currentPlayer = 'Y'27 elif self.currentPlayer == 'Y':28 self.currentPlayer = 'R'29
30 def trigger(self, i):31 if self.colHeight[i] < 6:32 self.colHeight[i] += 133 self.currentCol = i34 self.currentRow = 6 - self.colHeight[i]35 self.grids[self.currentRow][self.currentCol].value = self.currentPlayer36 self.swapPlayer()37
38 def checkWin(self):39 gs = self.grids40
41 # check horizontal42 for i in range(6):43 for j in range(4):44 if gs[i][j].value == gs[i][j+1].value == gs[i][j+2].value == gs[i][j+3].value\45 and gs[i][j].value != '':46 return gs[i][j].value47
48 # check vertical49 for i in range(3):50 for j in range(7):51 if gs[i][j].value == gs[i+1][j].value == gs[i+2][j].value == gs[i+3][j].value\52 and gs[i][j].value != '':53 return gs[i][j].value54
55 # check right cross56 for i in range(3):57 for j in range(4):58 if gs[i][j].value == gs[i+1][j+1].value == gs[i+2][j+2].value == gs[i+3][j+3].value\59 and gs[i][j].value != '':60 return gs[i][j].value61
62 # check left cross63 for i in range(3):64 for j in range(3, 7):65 if gs[i][j].value == gs[i+1][j-1].value == gs[i+2][j-2].value == gs[i+3][j-3].value\66 and gs[i][j].value != '':67 return gs[i][j].valuespot.py沒有改變。

先看看gameBoard.py的class,主要是在最下方加入了checkWin()的函數,這部分我直接用圖片去說明:

xxxxxxxxxx61# check horizontal2for i in range(6):3 for j in range(4):4 if gs[i][j].value == gs[i][j+1].value == gs[i][j+2].value == gs[i][j+3].value\5 and gs[i][j].value != '':6 return gs[i][j].value如圖所示,我們首先檢查打橫是否有四個連續一樣,但又不是''空內容的格,由於打橫只有7欄,我們又連續檢查到j+3(即當格後的第4格),所以j只需要for j in range(4),例如上圖藍色的檢查條,只由j=3出發,去到j=6。

xxxxxxxxxx61# check vertical2for i in range(3):3 for j in range(7):4 if gs[i][j].value == gs[i+1][j].value == gs[i+2][j].value == gs[i+3][j].value\5 and gs[i][j].value != '':6 return gs[i][j].value同理,最後藍色的檢查條只有i=2到i=5,所以i只需要for i in range(3)就夠了。

xxxxxxxxxx61# check right cross2for i in range(3):3 for j in range(4):4 if gs[i][j].value == gs[i+1][j+1].value == gs[i+2][j+2].value == gs[i+3][j+3].value\5 and gs[i][j].value != '':6 return gs[i][j].value
xxxxxxxxxx61# check left cross2for i in range(3):3 for j in range(3, 7):4 if gs[i][j].value == gs[i+1][j-1].value == gs[i+2][j-2].value == gs[i+3][j-3].value\5 and gs[i][j].value != '':6 return gs[i][j].value其他我就不再冗述了。當然,我做我方法是分開4次for loop,分別檢查打橫、打直和兩個打斜。你也可以只用一個for loop,同時檢查4個分向。又或者,栱子是由最底部開始堆疊的,所以由最下方檢查到最上方,會比較快找出贏家,減少運算時間。
connectFour.pyde:
xxxxxxxxxx261from spot import *2from gameBoard import *3
4gameBoard = 05
6
7def setup():8 global grids, gameBoard9
10 size(700, 600)11 ellipseMode(CENTER)12 frameRate(10)13
14 gameBoard = GameBoard()15
16
17def draw():18 background(200)19 gameBoard.display()20 21
22def mousePressed():23 global gameBoard24 for i in range(7):25 if (mouseX > width/7*i and mouseX < width/7*(i+1)):26 gameBoard.trigger(i)gameBoard.py:
xxxxxxxxxx831from spot import *2
3
4class GameBoard(object):5
6 def __init__(self):7 self.grids = []8 for i in range(6):9 temp = []10 for j in range(7):11 temp.append(Spot(i, j, width/7*(j+.5), height/6*(i+.5), ''))12 self.grids.append(temp)13
14 self.currentRow = 015 self.currentCol = 016 self.colHeight = [0, 0, 0, 0, 0, 0, 0]17 self.currentPlayer = 'R'18 self.gameOver = False19 self.winner = None20
21 def display(self):22 for i in range(6):23 for j in range(7):24 self.grids[i][j].display()25 if self.gameOver == True: 26 textAlign(CENTER, CENTER)27 textSize(100)28 fill('#0000FF')29 if self.winner == 'R':30 text('YOU WIN!!!!', width/2, height/2)31 elif self.winner == 'Y':32 text('GAME OVER', width/2, height/2)33 34
35 def swapPlayer(self):36 if self.currentPlayer == 'R':37 self.currentPlayer = 'Y'38 elif self.currentPlayer == 'Y':39 self.currentPlayer = 'R'40
41 def trigger(self, i):42 if self.gameOver == False:43 if self.colHeight[i] < 6:44 self.colHeight[i] += 145 self.currentCol = i46 self.currentRow = 6 - self.colHeight[i]47 self.grids[self.currentRow][self.currentCol].value = self.currentPlayer48 self.winner = self.checkWin()49 if self.winner != None:50 self.gameOver = True51 return52 self.swapPlayer()53
54 def checkWin(self):55 gs = self.grids56
57 # check horizontal58 for i in range(6):59 for j in range(4):60 if gs[i][j].value == gs[i][j+1].value == gs[i][j+2].value == gs[i][j+3].value\61 and gs[i][j].value != '':62 return gs[i][j].value63
64 # check vertical65 for i in range(3):66 for j in range(7):67 if gs[i][j].value == gs[i+1][j].value == gs[i+2][j].value == gs[i+3][j].value\68 and gs[i][j].value != '':69 return gs[i][j].value70
71 # check right cross72 for i in range(3):73 for j in range(4):74 if gs[i][j].value == gs[i+1][j+1].value == gs[i+2][j+2].value == gs[i+3][j+3].value\75 and gs[i][j].value != '':76 return gs[i][j].value77
78 # check left cross79 for i in range(3):80 for j in range(3, 7):81 if gs[i][j].value == gs[i+1][j-1].value == gs[i+2][j-2].value == gs[i+3][j-3].value\82 and gs[i][j].value != '':83 return gs[i][j].valuespot.py沒有改變。

主要改變都是在gameBoard的class,但先說明一下主程式:
xxxxxxxxxx51def mousePressed():2 global gameBoard3 for i in range(7):4 if (mouseX > width/7*i and mouseX < width/7*(i+1)):5 gameBoard.trigger(i)將原本winner = gameBoard.checkWin()和print(winner)都刪除,一會兒gameBoard的class中,因我們要紀錄誰是贏家做出對應的反應。
在gameBoard.py中,
xxxxxxxxxx51 def __init__(self):2 #====other codes======3 self.currentPlayer = 'R'4 self.gameOver = False5 self.winner = None在初始化的最下,加入gameOver和winner兩個變數。
xxxxxxxxxx121def display(self):2 for i in range(6):3 for j in range(7):4 self.grids[i][j].display()5 if self.gameOver == True: 6 textAlign(CENTER, CENTER)7 textSize(100)8 fill('#0000FF')9 if self.winner == 'R':10 text('YOU WIN!!!!', width/2, height/2)11 elif self.winner == 'Y':12 text('GAME OVER', width/2, height/2)在顯示函數中,加入,如果gameOver的話,就看看winner是誰,是R的話即我方贏,如Y的話即電腦贏。
xxxxxxxxxx121def trigger(self, i):2 if self.gameOver == False:3 if self.colHeight[i] < 6:4 self.colHeight[i] += 15 self.currentCol = i6 self.currentRow = 6 - self.colHeight[i]7 self.grids[self.currentRow][self.currentCol].value = self.currentPlayer8 self.winner = self.checkWin()9 if self.winner != None:10 self.gameOver = True11 return12 self.swapPlayer()下棋的函數中,首先要加入一個限制,只有在self.gameOver == False的情況下才會下棋。其次,將原本在主程式的self.winner = self.checkWin()搬過來,同時保留winner變數,如果winner有內容的話,就將gameOver變成True。
到這裡,今次的遊戲,手動版就暫時作結,因遊戲規則和畫面都十分簡單,而且為聚焦於遊戲本身,我也刻意地減少了動畫效果和美化。下一節會首先介紹一下遊戲AI的演算法,之後就會教大家用minmax演算法製造一個AI跟玩家對奕。