- 相關(guān)推薦
免費vc中國象棋軟件(一)
【摘要】:人機博弈是人工智能研究的經(jīng)典課題之一。憑借設計優(yōu)良的算法和計算機的快速運算能力,計算機可以在人機對弈中表現出相當高的“智能”。通常,一款象棋程序的實(shí)現可以被分為下棋引擎(人工智能)和外殼(界面及程序輔助)兩大部分。本文將介紹如何實(shí)現一款中國象棋對弈程序。
【關(guān)鍵詞】:中國象棋;人工智能;博弈樹(shù);Alpha-Beta搜索;歷史啟發(fā);界面;多線(xiàn)程;計時(shí)器;列表框;MFC。
一、前 言
我們的目標是實(shí)現一款有著(zhù)一定下棋水平且交互友好的中國象棋人機對弈程序。
該程序功能包括:
*人機對弈;
*盲棋模式;
(注:此功能為創(chuàng )新功能)
*搜索深度設定;
(電腦棋力選擇)
*棋子、棋盤(pán)樣式選擇;
*悔棋、還原;
*著(zhù)法名稱(chēng)顯示;
*下棋雙方計時(shí);
整個(gè)程序的實(shí)現可分為兩大部分:
一、人工智能部分(計算機下棋引擎)
該部分實(shí)現了如何讓計算機下中國象棋,其中涉及人機博弈的基本理論及思想,是該程序的核心部分,同時(shí)也是本項目研究的重點(diǎn)所在。
二、界面及程序輔助部分
光有下棋引擎尚不能滿(mǎn)足人機交互的基本要求,因此我們還需要一個(gè)框架(界面)來(lái)作為引擎的載體,同時(shí)提供一些諸如悔棋,計時(shí)之類(lèi)的附屬功能(程序輔助)來(lái)為程序增色添彩。
下面分別介紹各部分實(shí)現。由于界面及程序輔助部分涉及內容寬泛而又繁瑣,因而本文只介紹其中重點(diǎn)部分以及我們在開(kāi)發(fā)過(guò)程中曾經(jīng)遇到過(guò)困難的地方。
二、人工智能部分(計算機下棋引擎)
1、概 述
程序的基本框架:
從程序的結構上講,大體上可以將引擎部分劃分為四大塊:
棋局表示;
著(zhù)法生成;
搜索算法;
局面評估。
程序的大概的思想是:
首先使用一個(gè)數據結構來(lái)描述棋局信息,對某一特定的棋局信息由著(zhù)法生成器生成當前下棋方所有合法的著(zhù)法并依次存入著(zhù)法隊列。然后通過(guò)搜索算法來(lái)逐一讀取著(zhù)法并調用局面評估函數對該著(zhù)法所產(chǎn)生的后繼局面進(jìn)行評估打分,從中選出一個(gè)最有可能導致走棋方取勝的著(zhù)法。在搜索的過(guò)程中還可以采用一些輔助手段來(lái)提高搜索的效率。其過(guò)程如下圖所示:
下面將分別介紹各個(gè)部分。
2、棋局表示
計算機下棋的前提是要讓計算機讀懂象棋。所謂讀懂,即計算機應該能夠清楚地了解到棋盤(pán)上的局面(棋盤(pán)上棋子的分布情況)以及下棋方所走的每一種著(zhù)法。因而首先我們需要有一套數據結構來(lái)表示棋盤(pán)上的局面以及著(zhù)法。
對于棋盤(pán)局面的表示我們采用了最傳統的同時(shí)也是最為簡(jiǎn)單的“棋盤(pán)數組”。即用一個(gè)9*10的數組來(lái)存儲棋盤(pán)上的信息,數組的每個(gè)元素存儲棋盤(pán)上相應位置是何種棋子。這種表示方法簡(jiǎn)單易行(缺點(diǎn)是效率不是很高)。按此方法棋盤(pán)的初始情形如下所示:
BYTE CChessBoard[9][10] = {
R, 0, 0, P, 0, 0, p, 0, 0, r,
H, 0, C, 0, 0, 0, 0, c, 0, h,
E, 0, 0, P, 0, 0, p, 0, 0, e,
A, 0, 0, 0, 0, 0, 0, 0, 0, a,
K, 0, 0, P, 0, 0, p, 0, 0, k,
A, 0, 0, 0, 0, 0, 0, 0, 0, a,
E, 0, 0, P, 0, 0, p, 0, 0, e,
H, 0, C, 0, 0, 0, 0, c, 0, h,
R, 0, 0, P, 0, 0, p, 0, 0, r
};
其中“0”表示無(wú)棋子,大寫(xiě)字母表示紅方棋子,小寫(xiě)字母表示黑方棋子(所有這些大小寫(xiě)字母都是用宏定義的整數)。具體如下:
“R”表示紅車(chē);“H”表示紅馬;“E”表示紅相;“A”表示紅仕;“K”表示紅帥;“C”表示紅炮;“P”表示紅兵。
“r”表示黑車(chē);“h”表示黑馬;“e”表示黑象;“a”表示黑士;“k”表示黑將;“c”表示黑炮;“p”表示黑卒。
此外這個(gè)數組也表明了我們對棋盤(pán)進(jìn)行了如右圖所示的編號,并約定紅方棋子總處于棋盤(pán)的下方。
對于著(zhù)法的表示,我們直接借用棋盤(pán)數組的下標來(lái)記錄著(zhù)法的起點(diǎn)和目標點(diǎn)。至于是什么棋子在走,以及是否吃子、吃的是什么子,我們在著(zhù)法結構中并不記錄。這些信息由外部讀取棋盤(pán)上起點(diǎn)、終點(diǎn)的數據獲得。著(zhù)法結構定義如下,其中還包含了對著(zhù)法的歷史得分的記錄項,以供后面要講到的“歷史啟發(fā)”所用。
typedef struct _cchessmove{
POINT ptFrom; // 起點(diǎn)
POINT ptTo; // 目標點(diǎn)
int nScore; // 該走法的歷史得分
} CCHESSMOVE ; // 走法結構
有了對棋盤(pán)局面和著(zhù)法的表示之后,程序才能夠完成以下操作:
生成所有合法著(zhù)法;
執行著(zhù)法、撤銷(xiāo)著(zhù)法;
針對某一局面進(jìn)行評估。
因而,棋局表示好比是整個(gè)程序(計算機下棋引擎部分)的地基,之后所有的操作都將建立在其基礎上。
3、著(zhù)法生成
我們的程序需要讓計算機在輪到它走子的時(shí)候能夠執行一步它認為對它最有利的著(zhù)法,那前提就是它要有諸多(也可能是唯一)可供選擇的著(zhù)法,提供所有候選著(zhù)法的“清單”就是我們的著(zhù)法生成器所要完成的。之后用搜索函數來(lái)搜索“清單”,并用局面評估函數來(lái)逐一打分,最后就可以選擇出“最佳著(zhù)法”并執行了。
在著(zhù)法生成器中,我們采用的基本思想就是遍歷整個(gè)棋盤(pán)(一個(gè)接一個(gè)地查看棋盤(pán)上的每個(gè)位置點(diǎn)),當發(fā)現有當前下棋方的棋子時(shí)先判斷它是何種類(lèi)型的棋子,然后根據其棋子類(lèi)型而相應地找出其所有合法著(zhù)法并存入著(zhù)法隊列。
這里談到的“合法著(zhù)法”包括以下幾點(diǎn):
1、 各棋子按其行子規則行子。諸如馬跳“日”字、象走“田”字、士在九宮內斜行等等(這里需要特別注意的是卒(兵)的行子規則會(huì )隨其所在位置的不同而發(fā)生變化——過(guò)河后可以左右平移)。
2、 行子不能越出棋盤(pán)的界限。當然所有子都不能走到棋盤(pán)的外面,同時(shí)某些特定的子還有自己的行棋界限,如將、士不能出九宮,象不能過(guò)河。
3、 行子的半路上不能有子阻攔(除了炮需要隔一個(gè)子才能打子之外)以及行子的目的點(diǎn)不能有本方棋子(當然不能自己吃自己了)。
4、 將帥不能碰面(本程序中只在生成計算機的著(zhù)法時(shí)認為將帥碰面是非法的,而對用戶(hù)所走的導致將帥碰面的著(zhù)法并不認為其非法,而只是產(chǎn)生敗局罷了)。
產(chǎn)生了著(zhù)法后要將其存入著(zhù)法隊列以供搜索之用,由于搜索會(huì )搜索多層(即考慮雙方你來(lái)我往好幾步,這樣才有利于對局面進(jìn)行評估以盡可能避免“目光短淺”),所以在把著(zhù)法存入著(zhù)法隊列的時(shí)候還要同時(shí)存儲該著(zhù)法所屬的搜索層數。因此我們將著(zhù)法隊列定義為二維數組MoveList[12][80],其中第一個(gè)數組下標為層數,第二個(gè)數組下標為每一層的全部著(zhù)法數。
關(guān)于搜索層數,我將數組下標設定為12,實(shí)際使用的是1到11(在界面中我又將其限定為1—10)。搜索層數的增加會(huì )顯著(zhù)提高電腦的下棋水平(當然計算機的棋力在很大程度上也依賴(lài)于局面評估)。在我的迅馳1.5,736M內存的筆記本上最多只能搜索5層,再多將導致搜索時(shí)間達到令人無(wú)法容忍的地步(這里還需要特別說(shuō)明的是,搜索的速度也和著(zhù)法生成的效率以及局面評估的復雜度有關(guān),因為每分析一個(gè)結點(diǎn)都要執行這兩種操作)。
對于每一層的著(zhù)法數,也就是當前下棋方針對當前局面的所有可選的合法著(zhù)法,據有關(guān)數據統計在象棋實(shí)戰中一般最多情況下也就五六十種。定義第二個(gè)數組下標為80,應當可以保證十分的安全。
著(zhù)法生成為搜索部分提供了“原料”,接下來(lái)的任務(wù)就交給搜索和局面評估了。
4、搜索算法
搜索算法對于整個(gè)下棋引擎來(lái)說(shuō)都是至關(guān)重要的。它如同程序的心臟,驅動(dòng)著(zhù)整個(gè)程序。搜索算法的好壞直接影響著(zhù)程序執行的效率(從某種角度上,它影響著(zhù)計算機的下棋水平。因為,計算機必須在有限的時(shí)間內完成思考,搜索速度快意味著(zhù)在相同的時(shí)間內程序可以“看”得更遠,“想”的更多)。關(guān)于棋類(lèi)對弈程序中的搜索算法,經(jīng)前人的努力已形成了非常成熟的Alpha-Beta搜索算法[ Alpha-beta算法,該算法是由匹茲堡大學(xué)的三位科學(xué)家Newell, Shaw and Simon于1958年提出的。]以及其它一些輔助增強算法(還有眾多基于A(yíng)lpha-Beta算法的派生、變種算法)。鑒于目前我們的知識儲備、時(shí)間、精力等均達不到推陳出新、另開(kāi)爐灶的要求,再加之前人的算法著(zhù)實(shí)已相當完善,所以我們在自己的程序中直接借鑒了Alpha-Beta搜索算法并輔以了歷史啟發(fā)。本節先介紹Alpha-Beta搜索算法:
在中國象棋里,雙方棋手獲得相同的棋盤(pán)信息。他們輪流走棋,目的就是將死對方,或者避免被將死。
由此,我們可以用一棵“博弈樹(shù)”(一棵n叉樹(shù))來(lái)表示下棋的過(guò)程——樹(shù)中每一個(gè)結點(diǎn)代表棋盤(pán)上的一個(gè)局面,對每一個(gè)局面(結點(diǎn))根據不同的走法又產(chǎn)生不同的局面(生出新的結點(diǎn)),如此不斷直到再無(wú)可選擇的走法,即到達葉子結點(diǎn)(棋局結束)。中國象棋的博弈樹(shù)的模型大概如下圖所示,我們可以把其中連接結點(diǎn)的線(xiàn)段看作是著(zhù)法,不同的著(zhù)法自然產(chǎn)生不同的局面。
該樹(shù)包含三種類(lèi)型的結點(diǎn):
奇數層的中間結點(diǎn)(以及根結點(diǎn)),表示輪到紅方走棋;
偶數層的中間結點(diǎn),表示輪到黑方走棋;
葉子結點(diǎn),表示棋局結束。
現在讓計算機來(lái)下中國象棋,它應當選擇一步對它最有利的著(zhù)法(最終導致它取勝的著(zhù)法)。獲得最佳著(zhù)法的方法就是“試走”每一種可能的著(zhù)法,比較它們所產(chǎn)生的不同后果,然后從中選出能夠產(chǎn)生對自己最有利的局面的著(zhù)法。
結合上面所講的博弈樹(shù),如果我們給每個(gè)結點(diǎn)都打一個(gè)分值來(lái)評價(jià)其對應的局面(這一任務(wù)由后面所講的局面評估來(lái)完成),那么我們可以通過(guò)比較該分值的大小來(lái)判斷局面的優(yōu)劣。假定甲乙兩方下棋,甲勝的局面是一個(gè)極大值(一個(gè)很大的正數),那么乙勝的局面就是一個(gè)極小值(極大值的負值),和棋的局面則是零值(或是接近零的值)。如此,當輪到甲走棋時(shí)他會(huì )盡可能地讓局面上的分值大,相反輪到乙走棋時(shí)他會(huì )選盡可能地讓局面上的分值小。反映到博弈樹(shù)上,即如果我們假設奇數層表示輪到甲方走棋,偶數層表示輪到乙方走棋。那么由于甲方希望棋盤(pán)上的分值盡可能大,則在偶數層上我們會(huì )挑選分值最大的結點(diǎn)——偶數層的結點(diǎn)是甲走完一步棋之后的棋盤(pán)局面,反映了甲方對棋局形勢的要求。同樣道理,由于乙方希望棋盤(pán)上的分值盡可能小,那么在奇數層上我們會(huì )選擇分值最小的結點(diǎn)。這就是“最小-最大”(Minimax)[ “最小-最大”(Minimax)最早是由John von Nuoma(1903-1957,美籍匈牙利數學(xué)家)在60多年前完整描述的:
1、假設有對局面評分的方法,來(lái)預測棋手甲(我們稱(chēng)為最大者)會(huì )贏(yíng),或者對手(最小者)會(huì )贏(yíng),或者是和棋。評分用數字表示,正數代表最大者領(lǐng)先,負數代表最小者領(lǐng)先,零代表誰(shuí)也不占便宜;
2、最大者的任務(wù)是增加棋盤(pán)局面的評分(即盡量讓評分最大);
3、最小者的任務(wù)是減少棋盤(pán)局面的評分(即盡量讓評分最。;
4、假設誰(shuí)也不會(huì )犯錯誤,即他們都走能讓使局面對自己最有利的著(zhù)法。]的基本思想。這樣搜索函數在估值函數的協(xié)助下可以通過(guò)在奇數層選擇分值最大(最。┑慕Y點(diǎn),在偶數層選擇分值最。ㄗ畲螅┑慕Y點(diǎn)的方式來(lái)搜索以當前局面為根結點(diǎn)、限定搜索層數以?xún)鹊恼脴?shù)來(lái)獲得一個(gè)最佳的著(zhù)法。然而不幸的是,博弈樹(shù)相當龐大(它會(huì )成指數增長(cháng)),因而搜索(限定層數以?xún)鹊模┱脴?shù)是一件相當費時(shí)的工作——其時(shí)間復雜度為O(bn)。其中b是分枝因子,即針對各種局面的合法著(zhù)法的數目的平均值,n是搜索的深度。對于中國象棋而言,在中盤(pán)時(shí)平均著(zhù)法數目大約是40種左右,那么搜索4層需要檢查250萬(wàn)條路線(xiàn),搜索5層需要檢查1億條路線(xiàn),搜索6層需要檢查40億條路線(xiàn)。!
幸運的是,Alpha-Beta搜索使得我們能在不影響搜索精度的前提下大幅減少工作量。
因為,如果考慮到下棋是一個(gè)你來(lái)我往的交替進(jìn)行并且相互“較勁”的過(guò)程。由于每一方都會(huì )盡可能將局面導向對自己有利而對對方不利的方向(我們假定下棋雙方對棋局有著(zhù)同樣的認知,即你認為對你很糟糕的局面,在你的對手看來(lái)則是對他很有利的局面),那么某些局面由于能夠產(chǎn)生出很糟糕的局面因而根本沒(méi)有再繼續考慮的價(jià)值。所以當你看到某個(gè)局面有可能產(chǎn)生很糟糕的局面時(shí)(確切地說(shuō)這里的“很糟糕”是與之前分析的情況相比較而言的),你應當立刻停止對其剩余子結點(diǎn)的分析——不要對它再抱任何幻想了,如果你選擇了它,那么你必將得到那個(gè)很糟糕的局面,甚至可能更糟……這樣一來(lái)便可以在很大程度上減少搜索的工作量,提高搜索效率,這稱(chēng)為“樹(shù)的裁剪”。
下面用圖來(lái)進(jìn)一步說(shuō)明“樹(shù)的裁剪”。為了簡(jiǎn)便起見(jiàn),我將博弈樹(shù)進(jìn)行了簡(jiǎn)化——每個(gè)結點(diǎn)只有三個(gè)分支,實(shí)際情況中,剛才講過(guò)在中盤(pán)應有大約40個(gè)分支。
我們假定棋盤(pán)上的局面發(fā)展到了結點(diǎn)A(見(jiàn)下圖),現在輪到你走棋了,你是“最大的一方”——即你希望棋局的分值盡可能的高。讓我們試著(zhù)搜索兩層來(lái)看一看“樹(shù)的裁剪”對提高搜索效率的幫助。
圖中表示該結點(diǎn)要取子結點(diǎn)中的最大值;表示該結點(diǎn)要取子結點(diǎn)中的最小值。
首先,我們考察結點(diǎn)A的子結點(diǎn)B。結點(diǎn)B所屬的這一層是輪到你的對手——“最小者”來(lái)走棋了,他的目的是使得棋局的分值盡可能的小。依次考察結點(diǎn)B的各個(gè)子結點(diǎn),查看它們的分值(因為我們事先約定好了搜索兩層,現在已達到搜索深度的要求了,所以就停下來(lái)調用局面評估函數來(lái)給它打分)。結點(diǎn)B的第一個(gè)子結點(diǎn)(從左到右算起)返回10,第二個(gè)子結點(diǎn)返回了-5,第三個(gè)子結點(diǎn)返回了2。由于結點(diǎn)B這層是你的對手來(lái)做選擇,我們假設他一定會(huì )做出明智的選擇(你不能寄希望于你的對手會(huì )走出一步“昏招”),那么他會(huì )選擇返回值為-5的那個(gè)結點(diǎn)。-5最終也就成了從結點(diǎn)B傳遞回的值,即倘若你(現在位于結點(diǎn)A)選擇了產(chǎn)生結點(diǎn)B的走法,使得局面發(fā)展到了結點(diǎn)B。那么下一步,你的對手的選擇就會(huì )使得棋局發(fā)展成為分值為-5的那個(gè)結點(diǎn)所表示的局面。
我們再來(lái)分析結點(diǎn)A的第二個(gè)子結點(diǎn)C,結點(diǎn)C與結點(diǎn)B同屬一層,它依然是輪到你的對手作選擇。依次查看結點(diǎn)C的各個(gè)子結點(diǎn)的分值,其第一個(gè)子結點(diǎn)返回了-8……
好了,該是“裁剪”登場(chǎng)的時(shí)候了。你已經(jīng)不必再繼續考察結點(diǎn)C的剩余子結點(diǎn)了,因為結點(diǎn)C已經(jīng)夠糟糕的了,不管結點(diǎn)C的剩余子結點(diǎn)有怎樣的分值,它最多只能傳回-8(有可能其剩余子結點(diǎn)中還有分值更小的結點(diǎn),因而結點(diǎn)C還有可能傳回更小的值)。而與前面已經(jīng)分析過(guò)的結點(diǎn)B所傳回-5相比較,作為“最大一方”的你顯然更不愿意看到-8的局面。所以,你當然不會(huì )選擇相應的著(zhù)法使得局面發(fā)展成為結點(diǎn)C。因為那樣的話(huà),下一步你的對手就會(huì )帶給你一個(gè)分值不高于-8的局面。
由此,我們就在不影響搜索質(zhì)量的前提下避免了搜索“無(wú)價(jià)值的”結點(diǎn)C的剩余子結點(diǎn)的大量工作,從而節省了寶貴時(shí)間,為在同樣機器配置下搜索更多的層數提供了可能。
“最小-最大”的思想再加上“對樹(shù)的裁剪”,這就是Alpha-Beta搜索算法的核心。最基本的Alpha-Beta算法的代碼如下:
int AlphaBeta(int depth, int alpha, int beta)
{
if (depth == 0) //如果是葉子節點(diǎn)(到達搜索深度要求)
return Evaluate(); //則由局面評估函數返回估值
GenerateLegalMoves(); //產(chǎn)生所有合法著(zhù)法
while (MovesLeft()) //遍歷所有著(zhù)法
{
MakeNextMove(); //執行著(zhù)法
int val = -AlphaBeta(depth - 1, -beta, -alpha); //遞歸調用 UnmakeMove(); //撤銷(xiāo)著(zhù)法
if (val >= beta) //裁剪
return beta;
if (val > alpha) //保留最大值
alpha = val;
}
return alpha;
}
5、歷史啟發(fā)及著(zhù)法排序(搜索輔助)
既然Alpha-Beta搜索算法是在“最小-最大”的基礎上引入“樹(shù)的裁剪”的思想以期提高效率,那么它的效率將在很大程度上取決于樹(shù)的結構——如果搜索了沒(méi)多久就發(fā)現可以進(jìn)行“裁剪”了,那么需要分析的工作量將大大減少,效率自然也就大大提高;而如果直至分析了所有的可能性之后才能做出“裁剪”操作,那此時(shí)“裁剪”也已經(jīng)失去了它原有的價(jià)值(因為你已經(jīng)分析了所有情況,這時(shí)的Alpha-Beta搜索已和“最小-最大”搜索別無(wú)二致了)。因而,要想保證Alpha-Beta搜索算法的效率就需要調整樹(shù)的結構,即調整待搜索的結點(diǎn)的順序,使得“裁剪”可以盡可能早地發(fā)生。
我們可以根據部分已經(jīng)搜索過(guò)的結果來(lái)調整將要搜索的結點(diǎn)的順序。因為,通常當一個(gè)局面經(jīng)過(guò)搜索被認為較好時(shí),其子結點(diǎn)中往往有一些與它相似的局面(如個(gè)別無(wú)關(guān)緊要的棋子位置有所不同)也是較好的。由J.Schaeffer所提出的“歷史啟發(fā)”(History Heuristic)就是建立在這樣一種觀(guān)點(diǎn)之上的。在搜索的過(guò)程中,每當發(fā)現一個(gè)好的走法,我們就給該走法累加一個(gè)增量以記錄其“歷史得分”,一個(gè)多次被搜索并認為是好的走法的“歷史得分”就會(huì )較高。對于即將搜索的結點(diǎn),按照“歷史得分”的高低對它們進(jìn)行排序,保證較好的走法(“歷史得分”高的走法)排在前面,這樣Alpha-Beta搜索就可以盡可能早地進(jìn)行“裁剪”,從而保證了搜索的效率。
對于著(zhù)法的排序可以使用各種排序算法,在我們的程序中采用了歸并排序。歸并排序的空間復雜度為O(n),時(shí)間復雜度為O(nlog2n),具有較高的效率。
6、局面評估
前面已經(jīng)講過(guò)了棋局表示、著(zhù)法生成、搜索算法(包括搜索輔助), 在象棋程序中如果說(shuō)搜索算法是心臟,那么局面評估就是大腦。搜索算法負責驅動(dòng)整個(gè)程序,而局面評估則負責對搜索的內容進(jìn)行判斷和評價(jià)。因而搜索與局面評估是整個(gè)下棋引擎的核心。
首先,先介紹一下在局面評估中需要考慮的因素。就不同的棋類(lèi)可能要考慮的因素略有差異。在中國象棋中所要考慮的最基本的幾個(gè)因素包括如下四點(diǎn):
1、子力總和
子力是指某一棋子本身所具有的價(jià)值。通俗地講就是一個(gè)棋子它值個(gè)什么價(jià)。例如,車(chē)值500的話(huà),那可能馬值300,卒值80等等。所以在評估局面時(shí),我們首先要考慮雙方的子力總和的對比。比如紅方擁有士象全加車(chē)馬炮,而黑方只有殘士象加雙馬,則紅方明顯占優(yōu)。
2、棋子位置(控制區域)
棋子位置,或稱(chēng)控制區域,是指某一方的棋子在棋盤(pán)上所占據(控制)的位置。例如,沉底炮、過(guò)河卒、以及車(chē)占士角等都是較好的棋子位置狀態(tài),而窩心馬、將離開(kāi)底線(xiàn)等則屬較差的棋子位置狀態(tài)。
3、棋子的機動(dòng)性
棋子的機動(dòng)性指棋子的靈活度(可移動(dòng)性)。例如,起始位置的車(chē)機動(dòng)性較差,所以我們下棋講究早出車(chē)。同樣四面被憋馬腿的死馬機動(dòng)性也較差(對于一步也不能走的棋子,可以認為其機動(dòng)性為零)。
4、棋子的相互關(guān)系(包括攻擊關(guān)系和保護關(guān)系)
這一點(diǎn)的分析較為復雜,因為一個(gè)棋子與其它子之間往往存在多重關(guān)系。如:一個(gè)馬可能在對方的炮的攻擊之下同時(shí)它又攻擊著(zhù)對方的車(chē)。
在我的程序中,估值函數最后返回的是每一方的總分的差值,而各方的總分就是上面所提到的四個(gè)因素的打分的總和。
對于子力打分和控制區域打分,只要遍歷棋盤(pán),當遇到棋子時(shí)簡(jiǎn)單地去查事先定義好的“子力價(jià)值表”和“控制區域價(jià)值表”,取出相對應的值進(jìn)行累加即可(這些值的具體設定參考了前人的程序并作了適當的調整,今后仍應根據電腦下棋所反映出的實(shí)際問(wèn)題對這些值作適當修改)。
對于機動(dòng)性打分,需要求出各個(gè)子總共有多少種走法,然后根據各個(gè)子所不同的機動(dòng)性?xún)r(jià)值每多一種走法就加一次相應的分數。
對棋子間相互關(guān)系的打分,我先定義了一個(gè)關(guān)系表的結構類(lèi)型
typedef struct _relationtable{
BYTE nCChessID ;
int nUAttackCount ;
int nUGuardCount ;
BYTE UnderAttack[5];
BYTE UnderGurad[5];
} RelationTable;
RelationTable RelationOfMan[9][10]; // 關(guān)系表
其中nCChessID給出棋子的類(lèi)型,nUAttackCount和nUGuardCount分別記錄正攻擊該子的棋子數量和正保護該子的棋子數量,UnderAttack[5]和UnderGuard[5]則存放攻擊該子和保護該子的具體棋子(的類(lèi)型)。因為考慮到實(shí)戰中幾乎不可能出現同時(shí)有超過(guò)五個(gè)棋子攻擊/保護一個(gè)子的情況,故數組下標設定為5。
當遍歷一遍棋盤(pán)之后,子力打分、控制區域打分和機動(dòng)性打分都可以完成,而關(guān)系表也可以填完。之后,再根據關(guān)系表來(lái)具體考察棋子的相互關(guān)系,進(jìn)行關(guān)系打分。
分析關(guān)系時(shí),首先,對王的攻擊保護應分離出來(lái)單獨考慮,因為對王的保護沒(méi)有任何意義,一旦王被吃掉整個(gè)游戲就結束了。
其次,對一個(gè)普通子,當它既受到攻擊又受到保護的時(shí)候要注意如下幾個(gè)問(wèn)題:
1、攻擊者子力小于被攻擊者子力,攻擊方將愿意換子。比如,一個(gè)車(chē)正遭受一個(gè)炮的攻擊,那么任何對車(chē)的保護都將失去意義——對方肯定樂(lè )意用一個(gè)炮來(lái)?yè)Q一個(gè)車(chē)。
2、多攻擊\單保護的情況,并且攻擊者最小子力小于被攻擊者子力與保護者子力之和,則攻擊方可能以一子換兩子。
3、三攻擊\兩保護的情況,并且攻擊者子力較小的二者之和小于被攻擊者子力與保護者子力之和,則攻擊方可能以?xún)勺訐Q三子。
4、攻擊方與保護方數量相同,并且攻擊者子力小于被攻擊者子力與保護者子力之和再減去保護者中最大子力,則攻擊方可能以n子換n子。
當然,上述四條只是覆蓋了最常見(jiàn)的幾種情況,覆蓋并不全面。而且,在我們的程序中并沒(méi)有直接地重新考慮雙方兌子之后的控制區域及機動(dòng)性變化情況(之所以說(shuō)沒(méi)有“直接地重新考慮”,是因為搜索繼續展開(kāi)結點(diǎn)后仍會(huì )考慮這些因素,只是目前我們尚不知這樣效果是否受影響——考察這兩種方法在效果上的差異需要一定數量的試驗數據的支持)。所以,如果今后要對引擎進(jìn)行改進(jìn),提高程序的下棋水平的話(huà),還應當在此多做文章……
7、程序組裝
至此,我們已具備了實(shí)現一款中國象棋對弈程序引擎部分的所有要素,我將上述模塊分別寫(xiě)作.h頭文件。如下:
CChessDef.h
——象棋相關(guān)定義。包括棋盤(pán)局面和著(zhù)法的表示。
CChessMove.h
——著(zhù)法生成器。就當前局面生成某一方所有合法著(zhù)法。
CChessSearch.h
——搜索部分。使用Alpha-Beta搜索求出最佳著(zhù)法。
HistoryHeuristic.h
——歷史啟發(fā)。Alpha-Beta搜索之補充,以提高搜索效率。
SortMove.h
——著(zhù)法排序。對著(zhù)法按其歷史得分進(jìn)行降序排序,以提高搜索效率。
CChessEvaluate.h
——局面評估。為某一特定局面進(jìn)行評分。
當實(shí)現了引擎部分的各要素時(shí),程序的界面部分還尚未開(kāi)工。因此我們暫時(shí)先建了一個(gè)Win32控制臺項目(這是我們所最熟悉的,也是我們在課堂上一直用的),之后只要再添加一個(gè).cpp文件負責接受用戶(hù)的輸入、調用搜索函數、顯示搜索結果,便可簡(jiǎn)單的測試引擎了(我們采用輸入著(zhù)法的起點(diǎn)坐標和終點(diǎn)坐標的方式來(lái)傳送用戶(hù)走棋的信息。同樣,程序顯示計算機走棋的起點(diǎn)坐標和終點(diǎn)坐標來(lái)做出回應)。
此后,等到界面部分初步完成以后,引擎的上述各模塊無(wú)需作任何改動(dòng),仍以.h頭文件的形式加入界面工程,只要由界面中的某個(gè).cpp文件調用搜索函數即可。這種連接方式實(shí)現起來(lái)非常簡(jiǎn)單,基本上不需要其它額外作業(yè)。
三、界面及程序輔助部分
1、界面基本框架
關(guān)于界面,我們建了一個(gè)基于對話(huà)框的MFC應用程序。之后主要工作都在對話(huà)框類(lèi)的兩個(gè)文件CChessUIDlg.h和CChessUIDlg.cpp下展開(kāi)。
我們所寫(xiě)的代碼主要分布于以下三大部分:
一、初始化部分
BOOL CCChessUIDlg::OnInitDialog()
{
}
OnInitDialog()負責的是對話(huà)框的初始化。我們把有關(guān)中國象棋的棋局初始化情況也放在了這里面。初始化的內容包括:
對引擎部分所用到的變量的初始化。包括對棋盤(pán)上的棋子位置進(jìn)行初始化(棋盤(pán)數組的初始化),對搜索深度、當前走棋方標志、棋局是否結束標志等的初始化;
對棋盤(pán)、棋子的貼圖位置(即棋盤(pán)、棋子在程序中實(shí)際顯示位置)的初始化;
對程序輔助部分所用到的一些變量的初始化。包括對悔棋、還原隊列的清空,棋盤(pán)、棋子樣式的默認選擇,下棋模式的默認選擇,計時(shí)器顯示的初始化,以及著(zhù)法名稱(chēng)列表的初始化等。
二、繪圖部分
void CCChessUIDlg::OnPaint()
{
}
OnPaint()函數負責的是程序界面的繪圖。因此,在這里我們要完成棋盤(pán)、棋子的顯示(如果用戶(hù)選擇了盲棋模式,則不進(jìn)行棋子的繪圖,而是在屏幕中央給出提示信息表明當前為盲棋模式),走棋起始位置和目標位置的提示框的顯示。
由于我們的棋盤(pán)、棋子等都是以位圖的形式給出的。所以在OnPaint()函數里我們做的工作主要都是在貼位圖。
需要注意的是由于位圖文件不能像GIF文件那樣有透明的背景并且棋子是圓形的而位圖文件只能是矩形的,所以如果直接貼圖的話(huà)會(huì )在棋盤(pán)上留下一塊白色的邊框——棋子的背景。因此,要想讓棋子文件的背景“隱藏”需要通過(guò)一些“與”和“異或”操作來(lái)屏蔽掉棋子的背景。
三、走棋部分(用戶(hù)動(dòng)作響應部分)
為WM_LBUTTONDOWN消息添加消息響應事件,可得到如下函數:
void CCChessUIDlg::OnLButtonDown(UINT nFlags,CPoint point)
{
}
當用戶(hù)在窗口客戶(hù)區按下鼠標左鍵時(shí),程序就會(huì )調用OnLButtonDown(UINT nFlags, CPoint point)函數來(lái)進(jìn)行響應。其中第二個(gè)參數CPoint point是我們在本程序中所要用到的,它給出了當鼠標左鍵被按下時(shí),鼠標指針的位置坐標。我們可以通過(guò)這一信息來(lái)得知用戶(hù)的走法。
在OnLButtonDown函數里我們處理如下兩種操作:
1、如果用戶(hù)點(diǎn)擊鼠標的位置落在己方的棋子上,表示用戶(hù)選中了該棋子,下一步將移動(dòng)該子進(jìn)行走棋(也可能用戶(hù)下一步將會(huì )選擇己方另外的棋子,總之這一操作會(huì )記錄下用戶(hù)所選的將要走的棋子)。
2、如果之前用戶(hù)已經(jīng)選過(guò)了棋子,那么這一次的點(diǎn)擊(如果不是另選本方的其它棋子的話(huà))表達了用戶(hù)的一次走棋過(guò)程。在收到用戶(hù)傳達的走棋信息后,我們先判斷該著(zhù)法是否合法(是否符合中國象棋的游戲規則),如果合法,則執行之。緊接著(zhù)我們調用引擎的搜索函數計算出計算機對用戶(hù)著(zhù)法的應著(zhù),然后執行該應著(zhù)。
如此,在OnLButtonDown函數里,我們實(shí)現了人與機器的對弈(當然每走一步棋,也還需要繪圖函數來(lái)顯示棋盤(pán)局面的更新)。
以上三部分并非界面程序的全部,而僅僅是與我們的程序密切相關(guān)的部分。此外還有其它部分對程序同樣必不可少,但這些部分主要由MFC自動(dòng)生成,無(wú)需人為改動(dòng),故在此不多做介紹。
2、多線(xiàn)程(程序輔助)
最初,我們的程序中沒(méi)有給計算機的“思考”另外開(kāi)辟新的線(xiàn)程。而僅僅是簡(jiǎn)單地按照如下順序編寫(xiě)代碼:
用戶(hù)走棋 —〉計算機思考并走棋
按這種方式編寫(xiě)的程序似乎毫無(wú)問(wèn)題,程序運行一切正常。
然而,在我們給程序加入計時(shí)功能(后面將會(huì )在講到其實(shí)現)后,程序出現了異常:對用戶(hù)方的計時(shí)功能完全正確,而對電腦方的計時(shí)功能卻根本不起作用!后來(lái),經(jīng)指導老師點(diǎn)撥,我們找到了問(wèn)題的所在以及相應的解決方案——由于程序在進(jìn)行搜索時(shí)會(huì )占用大量的CPU時(shí)間,因而阻塞了位于同一線(xiàn)程內的計時(shí)器,使之無(wú)法正常工作。解決方案就是另外開(kāi)一個(gè)線(xiàn)程,讓搜索與計時(shí)器分處兩個(gè)線(xiàn)程。
啟動(dòng)一個(gè)新的線(xiàn)程的方法非常簡(jiǎn)單,只需調用API函數AfxBeginThread即可,函數原型:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);[ 該函數原型的描述摘自Visual Studio .NET MSDN 版權所有 1987-2002 Microsoft Corporation]
該函數啟動(dòng)一個(gè)新的線(xiàn)程并返回一個(gè)指向該新線(xiàn)程對象的指針,然后新的線(xiàn)程與啟動(dòng)該新線(xiàn)程的線(xiàn)程同時(shí)運行。該函數的第一個(gè)參數AFX_THREADPROC pfnThreadProc指定了線(xiàn)程函數。線(xiàn)程函數的內容即為新線(xiàn)程所要執行的內容,線(xiàn)程函數執行完畢,新線(xiàn)程結束(自動(dòng)銷(xiāo)毀)。
線(xiàn)程函數必須被定義為全局函數,其返回值類(lèi)型必須是UINT,必須有一個(gè)LPVOID類(lèi)型的參數。我們可以把調用引擎部分的搜索函數的代碼以及完成走棋動(dòng)作的代碼放入我們所定義的思考線(xiàn)程內,如下:
UINT ThinkingThread(LPVOID pParam)
{
//計算機思考并走棋
}
然后,我們只要將原先調搜索函數并完成走棋的代碼代之以調用AfxBeginThread來(lái)啟動(dòng)新線(xiàn)程即可。這樣一來(lái),我們就實(shí)現了程序的多線(xiàn)程,計算機方的計時(shí)器不能正常工作的問(wèn)題也就隨之解決了。
3、計時(shí)器(程序輔助)
我們要給程序添加計時(shí)功能(分別記錄下棋雙方的“思考時(shí)間”),可以使用SetTimer函數、KillTimer函數以及OnTimer函數,SetTimer函數可寫(xiě)成如下:
SetTimer(1,1000,NULL);
其中第一個(gè)參數指明了計時(shí)器的ID(可以在同一個(gè)程序中建立多個(gè)計時(shí)器,用計時(shí)器ID來(lái)區別它們)。第二個(gè)參數給出了產(chǎn)生WM_TIMER消息的時(shí)間間隔,單位是毫秒。
當不想再繼續使用該計時(shí)器時(shí),可以通過(guò)調用函數KillTimer(計時(shí)器ID)來(lái)銷(xiāo)毀計時(shí)器。如銷(xiāo)毀上面所設的計時(shí)器可以寫(xiě)作如下:
KillTimer(1);
OnTimer函數是WM_TIMER消息的消息響應函數,通俗地講即每過(guò)SetTimer函數中指定的時(shí)間間隔,程序就調用一次OnTimer函數。它只有一個(gè)參數,即計時(shí)器的ID——當一個(gè)程序中有多個(gè)計時(shí)器時(shí),OnTimer函數可以通過(guò)識別不同的計時(shí)器ID號來(lái)完成不同的操作。
這樣要給程序增加對雙方下棋時(shí)間的計時(shí)功能,可以按如下流程編寫(xiě)程序:
0 - 棋局正式開(kāi)始,紅方先行;
1 - SetTimer(1,1000,NULL);
2 - 紅方思考;
3 - 紅方走棋;
4 - KillTimer(1);
5 - SetTimer(2,1000,NULL);
6 - 黑方思考;
7 - 黑方走棋;
8 - KillTimer(2);
9 - 跳轉至1,重復走棋過(guò)程
OnTimer函數則按如下編寫(xiě)代碼:
OnTimer(計時(shí)器ID)
{
if 計時(shí)器ID == 1 then 紅方計時(shí)+1;
if 計時(shí)器ID == 2 then 黑方計時(shí)+1;
}
當然,上面的流程及偽碼僅僅說(shuō)明編寫(xiě)象棋計時(shí)器大體思想,實(shí)際情況要比這復雜的多。在我們的實(shí)際的程序中還涉及了線(xiàn)程間的通信(因為我們把計算機方的思考和走棋的過(guò)程放在了另一個(gè)線(xiàn)程之內),時(shí)間的刷新顯示,分、秒、時(shí)的進(jìn)位換算等等。而且為了提高計時(shí)精度,我們還將SetTimer函數的第二個(gè)參數設為了100,即每0.1秒做一次計時(shí)(有時(shí)計算機思考和走棋的時(shí)間還不足1秒,如果按每秒鐘計一次時(shí),則該不足1秒的時(shí)間將被遺漏),當然實(shí)際顯示的時(shí)候還是只顯示到秒。
4、著(zhù)法名稱(chēng)顯示(程序輔助)
每當下棋方(用戶(hù)或是計算機)走一步棋,我們就在棋盤(pán)旁邊的一個(gè)列表框控件(List Box)中按照中國象棋關(guān)于著(zhù)法描述的規范要求顯示出該著(zhù)法的名稱(chēng)。如:紅炮二平五、黑馬8進(jìn)7此類(lèi)。為了獲得該著(zhù)法名稱(chēng),我們寫(xiě)了一個(gè)六百余行的函數。其功能就是將被移動(dòng)的棋子類(lèi)型以及走法的起點(diǎn)坐標、終點(diǎn)坐標這些信息轉換成中國象棋所規范的著(zhù)法名稱(chēng)。由于該函數主要涉及的是中國象棋關(guān)于著(zhù)法表示的規范要求,故在此我們不對其具體實(shí)現做額外的解釋。這里我們主要討論的是如何對列表框控件(List Box)進(jìn)行操作,以顯示或刪除著(zhù)法名稱(chēng)。
MFC為我們提供了一個(gè)CListBox類(lèi),使用該類(lèi)的成員函數我們可以非常容易地實(shí)現在List Box中添加與刪除“項(item)”。
首先,我們先要定義一個(gè)指向該類(lèi)的對象的指針:
CListBox* pLB;
然后,在進(jìn)行程序初始化(對話(huà)框的初始化)時(shí),我們使用如下語(yǔ)句來(lái)讓該指針與對話(huà)框中的List Box控件建立起聯(lián)系來(lái)(即讓該指針指向對話(huà)框中的List Box控件)。
pLB = (CListBox*)GetDlgItem(IDC_LISTCCHESS);
其中IDC_LISTCCHESS是所要建立關(guān)聯(lián)的控件的ID號。
之后,我們便可調用成員函數pLB->AddString(str);來(lái)向List Box控件中添加顯示字符串,str即為所要添加的字符串。
當列表框中的項的數目超過(guò)列表框的顯示范圍時(shí),列表框會(huì )自動(dòng)添加垂直滾動(dòng)條(前提是其VerticalScrollbar屬性要為T(mén)rue——該屬性默認即為T(mén)rue)。但是顯示的內容依然是最早加進(jìn)來(lái)的項。也就是說(shuō),垂直滾動(dòng)條不會(huì )自動(dòng)向下滾。為了能讓列表框中始終能自動(dòng)顯示出最新的著(zhù)法名稱(chēng)(所謂自動(dòng),即不需用戶(hù)去手動(dòng)地滾動(dòng)垂直滾動(dòng)條來(lái)查看最新的著(zhù)法名稱(chēng)),我們可以使用pLB->PostMessage(WM_VSCROLL,SB_BOTTOM,0);語(yǔ)句來(lái)發(fā)送一個(gè)讓垂直滾動(dòng)條處于最底端的消息,使得列表框自動(dòng)滾動(dòng)垂直滾動(dòng)條以顯示最新的著(zhù)法名稱(chēng)。
當我們想要從列表框中刪除項時(shí),我們可以使用pLB->DeleteString(n);參數n指明了要刪除的行數。最早加入列表框中的項記為第0行,以后逐次遞增。若要刪除最后一行內容(在悔棋功能中需要用到這一操作),則可以使用pLB->DeleteString(pLB->GetCount()-1);其中pLB->GetCount()返回的是列表框中的項的數目,減一之后正好是最后一項的行號。
5、悔棋、還原(程序輔助)
悔棋和還原是棋類(lèi)軟件中較為基本的功能。要實(shí)現悔棋和還原功能,首先我們要明確哪些信息應當被保存以供悔棋和還原所使用。
在我們的程序中保存了如下信息:
棋局表示中所定義的棋盤(pán)數組;
各棋子的貼圖位置。
這里需要特別說(shuō)明的是通常象棋程序處于程序效率的考慮并不保存所有棋子的信息,而只是保存之前一步的走棋信息。此后當悔棋的時(shí)候,需要撤銷(xiāo)著(zhù)法;還原的時(shí)候,需要執行著(zhù)法。然而,我們在編寫(xiě)自己的程序時(shí)一來(lái)考慮到程序的可讀性和不易出錯性,二來(lái)考慮到對當今的計算機的配置來(lái)說(shuō)這點(diǎn)開(kāi)銷(xiāo)基本上不會(huì )對程序的效率產(chǎn)生什么影響。因此索性保存了全部棋子的信息。
根據所要保存的數據我們定義了如下基本結構類(lèi)型:
typedef struct{
BYTE CChessBoard[9][10];
POINT ptChess[32];
} CCChess; // 供悔棋、還原保存用的信息結構
并隨之定義了兩個(gè)隊列以供悔棋和還原所用:
vector<CCChess> vBackQueue; //悔棋保存隊列
vector<CCChess> vForwardQueue;//還原保存隊列
在對弈過(guò)程中,每一回合我們都將棋局信息(這里指前面所說(shuō)的需要保存的信息)保存至vBackQueue隊列,以供悔棋所用。同時(shí),若vForwardQueue不為空的話(huà),我們還將清空它。因為還原功能是與悔棋功能相對應的,只有當產(chǎn)生了悔棋功能之后,還原功能才會(huì )被激活。一個(gè)回合的結束意味著(zhù)前一次操作沒(méi)有悔棋功能的產(chǎn)生,因此還原隊列也應被清空。
在悔棋中我們主要完成了以下任務(wù):
下棋回合數減一;
將當前局面信息保存至vForwardQueue隊列,以供還原所用;
從vBackQueue隊列中取出上一回合的棋局信息,恢復到當前局面,然后將其從vBackQueue隊列中剔除掉;
將顯示著(zhù)法名稱(chēng)的列表框中的本回合的著(zhù)法名稱(chēng)保存到一個(gè)著(zhù)法名稱(chēng)隊列(我們將其定義為vector<CString> vNameQueue;),以供還原所用。然后從列表框中刪除它。
而在還原中我們所做的剛好和悔棋相反:
下棋回合數加一;
將當前局面信息保存至vBackQueue隊列,以供悔棋所用;
從vForwardQueue隊列中取出最近一次悔棋前的棋局信息,恢復到當前局面,然后將其從vForwardQueue隊列中剔除;
從著(zhù)法名稱(chēng)隊列vNameQueue中取出最近一次存入的著(zhù)法名稱(chēng)(兩項,因為每回合會(huì )產(chǎn)生兩步著(zhù)法),將其重新顯示到列表框中。然后將其從vNameQueue中剔除。
以上便是悔棋和還原功能所完成的具體操作,其代碼分別寫(xiě)入悔棋和還原按鈕(Button)的BN_CLICKED事件處理函數中。
四、總 結
下面簡(jiǎn)單地總結一下。
首先,在指導老師的熱心幫助下,小組成員協(xié)同工作最終順利實(shí)現了程序。整個(gè)程序近6000行代碼,內容涉及人工智能的基本理論以及開(kāi)發(fā)MFC應用程序的一些基礎知識。無(wú)論是從程序難度上講還是從程序規模上講都是我們之前所未遇到過(guò)的。該程序的順利完成為我們積累了相當可觀(guān)的運用理論知識解決實(shí)際問(wèn)題的經(jīng)驗。特別是當程序運行過(guò)程中發(fā)現錯誤的時(shí)候,找出問(wèn)題所在并解決問(wèn)題更是一個(gè)積累經(jīng)驗,提高實(shí)際編程能力的過(guò)程。得益于此次編寫(xiě)該中國象棋對弈程序,我們對人工智能有了一個(gè)初步的認識——這為我們以后選擇進(jìn)一步學(xué)習或研究的方向也提供了一定的參考價(jià)值。此外,我們還實(shí)踐了用MFC編寫(xiě)基本的Windows應用程序,為今后從事開(kāi)發(fā)Windows應用程序也起到了一定的入門(mén)幫助作用。
然而,我們的程序也存在著(zhù)幾點(diǎn)遺憾:
第一、由于我們對使用MFC編寫(xiě)Windows程序的不熟悉,導致我們在界面及附屬功能部分花費了大量的時(shí)間和精力。因而沒(méi)能夠對計算機下棋引擎部分作更深一步的挖掘和研究。對于諸如位棋盤(pán)(BitBoard)、置換表(Hash Table)、迭代加深(Iterative Deepening)、機器學(xué)習(Machine Learning)等當今棋類(lèi)對弈程序中所采用的先進(jìn)技術(shù)和思想,在我們的程序中并未涉及。這在一定程度上影響了我們的程序中下棋引擎的工作效率。
第二、之前我們寫(xiě)程序都是單兵作戰,缺乏團隊合作的經(jīng)驗。這使得我們在協(xié)同開(kāi)發(fā)的過(guò)程中遇到了一些問(wèn)題。其中包括:
1、起初對小組各成員的分工上不夠明確;
2、由于事先沒(méi)有嚴格規定好編寫(xiě)代碼的規范,以及對各部分的接口的規范強調的力度不夠,導致不同人員所完成的作業(yè)在組裝和銜接時(shí)需要花費額外的工作。
盡管,這些問(wèn)題最終都得以解決,但卻影響了程序開(kāi)發(fā)的進(jìn)程。
第三、程序總體缺乏對面向對象思想的貫徹和使用。在我們的程序當中,在界面和程序輔助部分,由于要使用MFC,因而“不可避免”地使用了類(lèi)(class)。然而,整個(gè)下棋引擎部分沒(méi)有一點(diǎn)類(lèi)的影子。這也是由于我本人最初在編寫(xiě)引擎部分時(shí)一心只想盡快看到成果,因而從一開(kāi)始就按照自己先前的程序習慣(——面向過(guò)程)來(lái)編寫(xiě)代碼。到了開(kāi)發(fā)后期,又缺少時(shí)間和精力來(lái)用面向對象思想重新改寫(xiě)程序,最終導致我們的引擎部分與類(lèi)“無(wú)緣”。
第四、在本文即將交稿之際,我們程序仍在勝利局面檢測和貼圖刷新上存在著(zhù)隨機性的出錯可能(出錯幾率很。。本文交稿后我們會(huì )繼續努力查找出錯原因,爭取在程序發(fā)布時(shí)消除掉這些bug。
最后,特別感謝指導老師對我們科技小組的支持。最終我們能夠順利完成程序,離不開(kāi)組員們的合作,更離不開(kāi)指導老師的指導與幫助!
【免費vc中國象棋軟件(一)】相關(guān)文章:
免費vc++航空客運訂票系統+論文(一)11-22
免費vc++網(wǎng)上尋呼QICQ源代碼(附帶論文)(一)11-22
游戲軟件開(kāi)發(fā)VC++12-27
采用VC 面向對象技術(shù)構建巖土工程勘察軟件03-19
知識的一致性檢測研究與實(shí)現VC11-23
課堂點(diǎn)名軟件(一)11-22
在Windows系統中用VC 實(shí)現鉤子機制03-18
人臉的檢測定位MFC+VC++03-08