鐘闖
(湖北大學(xué)計算機(jī)與信息工程學(xué)院,湖北武漢430062)
黑白棋,又名翻轉(zhuǎn)棋。黑白棋由于規(guī)則簡單,易于上手,逐漸在各個國家流行起來。游戲通過翻轉(zhuǎn)對方的棋子成為自己的棋子,最后以棋盤上誰的棋子多來判斷勝負(fù)。這個棋的規(guī)則雖然簡單易懂,上手容易,但是它的變化又非常復(fù)雜。黑白棋的棋盤是64(8×8) 個格子,初始會有2 黑2 白放在棋盤的正中央。黑白棋的游戲規(guī)則是:1)不管黑方或白方,下的棋子必須要造成棋子的翻轉(zhuǎn),如果兩個相同的棋子相鄰中間沒有可以翻轉(zhuǎn)的棋子則一方不可以下子。2)如果一方的下一步無法造成棋子的翻轉(zhuǎn)就將下棋權(quán)移交給另一方。3)當(dāng)棋盤下滿或者兩方都沒棋子可以轉(zhuǎn)換的時候,游戲結(jié)束,統(tǒng)計棋盤上雙方的棋子,誰剩余棋子多誰勝利。由于黑白棋的特殊性,就導(dǎo)致下子的時候不能像圍棋或五子棋那樣隨意下子到棋盤邊緣沒有棋子的位置,這就給算法的實現(xiàn)增加了難度。
MFC 是微軟公司創(chuàng)建的C++的基礎(chǔ)類庫,目的是減少Win32編程開發(fā)的難度。這個類庫將Windows編程中的API封裝成C++類,這些封裝是輕量級封裝,與原來的C 語言的Win‐dows編程速度相比并沒損失多少,所以MFC在出來的時候,企業(yè)對其很是追捧,但過了幾年后C#中的Winform、C++圖形框架Qt 等對MFC 的沖擊,又由于MFC 本身的缺陷導(dǎo)致MFC 逐漸沒落,但想弄清楚Windows 中的消息和繪制機(jī)制就必須學(xué)習(xí)
MFC。
GDI是圖形設(shè)備接口英文的縮寫,它存在的目的是減少程序員繪圖的工作量。原來的程序員在程序中繪圖時,必須考慮底層硬件的驅(qū)動,這無疑加大了程序員的工作量和思考時間。GDI出現(xiàn)后讓程序員的工作成本大大減少,讓程序員不用再考慮底層的硬件驅(qū)動,直接使用簡便的圖形接口來操作復(fù)雜的底層繪制[1]。
雖然現(xiàn)在MFC和GDI很少人直接使用,但它們在計算機(jī)技術(shù)發(fā)展的歷史上有著舉足輕重的基石作用,很多桌面開發(fā)技術(shù)都是由這兩者拓展而來,所以本文將用MFC和GDI來實現(xiàn)黑白棋游戲。
游戲功能分為3個模塊:
1)繪制模塊,主要包括初始化繪制棋盤,棋子和游戲提示信息。
2)游戲判斷模塊,主要包括對雙方下子位置判斷,判斷游戲是否結(jié)束,判斷一方能否下子。
3)游戲控制模塊,主要包括鍵盤交互,人機(jī)對戰(zhàn),統(tǒng)計雙方棋子數(shù)量和重置游戲。
其功能圖如圖1。
圖1 黑白棋功能圖
繪制模塊。功能主要包括初始化棋盤大小,用GDI的繪制函數(shù)繪制棋子,游戲提示信息等。首先在800×680大小的窗口中將窗口640×640的區(qū)域分割成64個格子,并將格子的坐標(biāo)信息放在一個二維數(shù)組m_rcSquares里方便使用繪制函數(shù)進(jìn)行繪制,窗口底部的空位用于顯示游戲的基本信息。接著用一個二維數(shù)組表示對應(yīng)格子里是否下了棋子,1 代表黑棋,2 代表白棋,0則表示沒有下棋,再用一個二維數(shù)組表示其余的空格能否下棋子,能則為1,不能則為0,初始棋盤中央是有2黑2白。在棋盤底端顯示游戲的基本信息,用來顯示當(dāng)前棋盤黑白雙方棋子的數(shù)量等信息。
游戲判斷模塊。此模塊主要是為了判斷雙方下子位置的合法性,眾所周知,黑白棋一旦下子就必須造成棋子的轉(zhuǎn)換。所以首先需要判斷下子位置能不能造成棋子的轉(zhuǎn)換。其次,還要判斷雙方是否可以下棋,一旦雙方的下一步都無法造成棋子的轉(zhuǎn)換,那么游戲結(jié)束。如果只是一方無法下棋而另一方可以,那么要交換下棋方。最后,棋盤的64個格子放滿了棋子則游戲結(jié)束。
游戲控制模塊。這個模塊主要的功能是監(jiān)視鍵盤和鼠標(biāo)的輸入,根據(jù)用戶的輸入來進(jìn)行相應(yīng)的反應(yīng),在MFC中使用消息映射來實現(xiàn)對用戶鍵盤或者鼠標(biāo)輸入的響應(yīng)[2]。游戲中,鼠標(biāo)左鍵是黑棋下子,鼠標(biāo)右鍵是白棋下子(一開始是黑棋先下),鼠標(biāo)中鍵是開啟人機(jī)模式,在進(jìn)入人機(jī)模式后,人機(jī)是白子,如果想推出人機(jī)模式就點擊鼠標(biāo)右鍵退出人機(jī)模式。F1鍵是重置游戲,F(xiàn)2鍵是游戲規(guī)則說明。該程序中的人機(jī)對戰(zhàn)主要是采用的是貪心算法,所謂貪心算法,就是每一次的行動都是選擇最優(yōu)解[3],在這個游戲中就是人機(jī)每次下棋的時候選擇盡可能多地轉(zhuǎn)換棋子的位置,而不考慮之后的結(jié)果,直到游戲結(jié)束。
1)程序結(jié)構(gòu)
該游戲程序并沒有使用MFC經(jīng)典的文檔視圖結(jié)構(gòu),而是使用了比較簡便的方法。通過繼承MFC 中的CWinApp 類和CWnd類來簡化程序。在繼承CWinApp類后,需要在實現(xiàn)類中重寫它的虛函數(shù)InitInstance[4]。虛函數(shù)是實現(xiàn)多態(tài)的必要條件,可以通過父類的指針來找到具體實現(xiàn)虛函數(shù)的子類,如果子類沒有實現(xiàn),則只調(diào)用父類的定義虛函數(shù)[5]。程序內(nèi),類和類中函數(shù)概述如表1。
表1 程序中核心函數(shù)和類介紹
2)初始化和繪制功能詳述
在窗口類CMyMainWindow 的構(gòu)造函數(shù)中,首先加入了初始化棋盤的代碼,這個時候只能先構(gòu)建棋盤在窗口中位置坐標(biāo)并將其放到二維數(shù)組中,因為此時窗口還沒有建立所以不能在此類的構(gòu)造函數(shù)中直接加入繪制圖形函數(shù),同時將棋盤中間的4個格子設(shè)置為已有棋子放置,保存在二維數(shù)組中。之后還是在構(gòu)造函數(shù)加入窗口的初始化代碼,設(shè)置該窗口的大小為800×600。最后在類CMainWindow 中的消息響應(yīng)函數(shù)OnPaint 中添加對棋盤線,4個初始棋子(2黑2白,位置在棋盤中央)的繪制。
除了上面初始化繪制的時候不需要判斷,其余每次繪制都需要通過二維數(shù)組m_iChess來判斷對應(yīng)棋盤位置有沒有棋子。有時需要棋盤位置上有棋子的時候才能繪制,因為這個時候要通過繪制來實現(xiàn)棋子顏色反轉(zhuǎn)的效果,有時也需要棋盤位置上沒有棋子的時候才能繪制,因為這個時候是下子,必須保證下子位置沒有棋子。
該程序通過成員變量m_iBlackChess 和m_iWhiteChess 來統(tǒng)計棋盤上黑棋和白棋的數(shù)量,通過成員變量m_iChessTotal統(tǒng)計棋子總數(shù)。這3 個成員變量通過函數(shù)void DisplayGameInfo()來繪制到窗口下方,來實時顯示當(dāng)前游戲信息。
3)游戲判斷功能詳述
這個功能可以說是游戲核心中的核心了,有了這個功能黑白棋的規(guī)則才能付諸實現(xiàn)。首先當(dāng)一方下子的時候,要判斷他下子的8個方向(上,下,左,右,上左,下左,上右,下右)有沒有和下棋方一樣的棋子同時中間必須夾著另一方的棋子,在這種情況下才可以下棋,否則就無法下棋。當(dāng)然不是所有的格子都需要判斷8 個方向,比如棋盤的4 個角只用判斷3 個方向(以右上角為例,只用判斷左,下左,下3個方向),除了4個角,棋盤邊緣的格子也只用判斷3 個方向(以最右一列為例,只用判斷左,上左,下左3 個方向)。表1 中的函數(shù)BOOL FindRightChess(int iLine,int iVertical,int ChessType)函數(shù)中的代碼是查找落子位置右邊方向的相同棋子,其他位置的查找函數(shù)也是一樣的。其次,前一棋子落完子后,需要立即判斷當(dāng)前棋盤空位能不能再下接下來一方的棋子(比如前一方是白子,接下來就要判斷黑子有沒有地方下了),如果沒有合法的位置,跳過當(dāng)期下棋方,回到前一下棋方繼續(xù)下棋,這個功能所對應(yīng)的函數(shù)就是表1中的BOOL ChessRevarsal(int iLine,int iVertical)函數(shù)。除此之外已經(jīng)下過棋子的位置不能再下子,通過類成員變量m_iChess,這個變量是個8×8的bool類型的二維數(shù)組,用于存儲64個棋盤位置有沒有棋子,所以每次下子前需先遍歷該二維數(shù)組,然后再判斷下棋位置是否合法。當(dāng)兩方的下一步都不能造成棋子轉(zhuǎn)換或者棋盤下滿了的時候游戲結(jié)束。通過成員變量m_iChess‐Total統(tǒng)計棋子總數(shù),如果棋子總數(shù)達(dá)到了64個,則游戲結(jié)束。
4)游戲控制功能
這個功能主要是管理鍵盤交互,人機(jī)對戰(zhàn)和游戲基本信息的顯示。在MFC中鍵盤和鼠標(biāo)所響應(yīng)的函數(shù)是已經(jīng)規(guī)定好的,只需要在窗口類中重寫對應(yīng)的方法就可以了。在該程序中,需重寫鍵盤F1,F(xiàn)2按鍵被按下的消息事件,鼠標(biāo)左鍵,鼠標(biāo)右鍵,鼠標(biāo)中鍵被按下的事件。注意,在鼠標(biāo)點擊的事件響應(yīng)中要加入對鼠標(biāo)點擊時坐標(biāo)的判斷,有可能鼠標(biāo)點擊的時候剛好點擊到了棋盤線上,通過MFC 中CRect 類中的PtInRect 方法就可以進(jìn)行判斷。在之前程序的初始化中,已經(jīng)將棋盤中每個格子的坐標(biāo)放在了二維數(shù)組中,這個數(shù)組類型是CRect 所以通過遍歷數(shù)組然后調(diào)用該方法。
通過按下鼠標(biāo)中鍵進(jìn)入人機(jī)對戰(zhàn),在人機(jī)對戰(zhàn)中,人機(jī)使用的是白色棋子,玩家使用的是黑色棋子,人機(jī)下子所采用的算法是比較經(jīng)典的貪心算法,就是在玩家下完棋子后查找轉(zhuǎn)換棋子最多的位置,所用的函數(shù)是將上面的BOOL FindRightChess(int iLine, int iVertical,int ChessType)等7 個函數(shù)進(jìn)行改造就可以了。上面的FindRightChess函數(shù)是在查找的時候同時進(jìn)行棋子的翻轉(zhuǎn),而對于人機(jī)來說則是先去找能轉(zhuǎn)換棋子最多的位置,這個過程不需要進(jìn)行翻轉(zhuǎn),等找到了合適的位置,再調(diào)用繪制函數(shù),除了某些細(xì)節(jié)不一樣,其余代碼都是和人下棋的代碼是一樣的。重置函數(shù)ResetGame(),這個函數(shù)在棋盤滿了,雙方無法下子和玩家按下F1鍵的時候啟用。在函數(shù)內(nèi)部將二維數(shù)組初始化到游戲開始的狀態(tài)然后調(diào)用MFC的窗口更新函數(shù)進(jìn)行窗口圖形刷新,當(dāng)窗口刷新時程序會自動調(diào)用繪制函數(shù)。
該程序基本實現(xiàn)了黑白棋游戲的基本功能,運(yùn)行界面如圖2所示。
圖2 黑白棋運(yùn)行界面
可以看到現(xiàn)在是由白棋下,之前的黑棋已經(jīng)造成了翻轉(zhuǎn),底部的信息提示現(xiàn)在棋盤上的情況和人機(jī)對戰(zhàn)是否開啟。當(dāng)玩家覺得自己下得不好時,可以按下鍵盤上的F1鍵重置游戲,但前提是你要和你的對手商量好。當(dāng)你找不到對手跟你一起下時可以按下鼠標(biāo)中鍵進(jìn)行人機(jī)對戰(zhàn)。當(dāng)游戲雙方不清楚游戲規(guī)則時可以按下鍵盤上的F2鍵了解游戲規(guī)則。
該程序通過繼承MFC 提供的類和實現(xiàn)MFC 消息響應(yīng)函數(shù),設(shè)計并實現(xiàn)了一個黑白棋游戲。程序的總體結(jié)構(gòu)并不復(fù)雜,但內(nèi)容卻集合了數(shù)據(jù)結(jié)構(gòu)、算法、圖形繪制等復(fù)雜內(nèi)容。選擇一個好的數(shù)據(jù)結(jié)構(gòu)能提升數(shù)據(jù)組織效率,選擇一個好的算法能提升程序運(yùn)行速度。除此之外,擁有良好的代碼風(fēng)格能夠使程序言簡意賅同時在程序出BUG 的時候能快速找到位置進(jìn)而提升工作時的效率。