當(dāng)前位置:首頁 > 芯聞號 > 充電吧
[導(dǎo)讀]其實我不玩網(wǎng)絡(luò)游戲的,所以對于外掛類程序也沒什么好或壞的感覺,甚至平時基本不會想到這個概念。這個實驗項目一開始稱為“掃雷助手”,而究其技術(shù)含量,與現(xiàn)在真正實用的游戲外掛程序也沒有可比性。設(shè)計它的意義,

其實我不玩網(wǎng)絡(luò)游戲的,所以對于外掛類程序也沒什么好或壞的感覺,甚至平時基本不會想到這個概念。這個實驗項目一開始稱為“掃雷助手”,而究其技術(shù)含量,與現(xiàn)在真正實用的游戲外掛程序也沒有可比性。設(shè)計它的意義,無非在于對某些Windows API的的進一步熟悉,使從未涉足過此領(lǐng)域的新手(當(dāng)初的我自己)能夠揭開某些“神秘的面紗”。之所以選擇“掃雷”這個最經(jīng)典的小游戲進行hack,一方面,使這個項目更貼近生活,更容易被理解,另一方面也可以最大化本人的成就感^_^,畢竟,在新手中,對掃雷的hack的探索似乎從來都沒有停止過,從早期的修改.ini文件和注冊表,到后來的Esc和xyzzy,無不體現(xiàn)著一代代人對這個目標(biāo)的不懈追求^_^。而對于我們這些搞專業(yè)的來說,則應(yīng)該有專業(yè)一點的方式,要有點技術(shù)含量!于是我做了這個叫MineAssistant的項目。

它的原理說來也不難,無非實時地替人做兩件事:1。把一定是雷的方塊標(biāo)上雷的記號,2。把一定不是雷的方塊點擊開。至于輕易判斷不了是不是雷的,還得靠你的運氣。其實,掃雷游戲中,這兩項最簡單的操作絕對是占了絕大多數(shù)時間的,不妨看看實測的效果:我使用掃雷外掛的紀(jì)錄是:初級、中級都1秒,高級4秒。(和修改注冊表的相比還是差了一點,不過那個一點技術(shù)含量都沒有,呵呵)

顯然,這個實現(xiàn)過程涉及到了從一個程序操作另一個程序的窗口。這必須使用Windows API,主要的有下面幾個:

FindWindow

GetDC

GetPixel

PostMessage

?

稍微有點基礎(chǔ)的看也看得出來,本人對于掃雷進程的內(nèi)部數(shù)據(jù)是一無所知的,對于當(dāng)前雷區(qū)的狀態(tài)信息的取得,都是通過最淺顯最笨的方法,就是直接讀取屏幕像素。而反饋的操作,也都是通過PostMessage簡單的發(fā)送一條信息來模仿鼠標(biāo)點擊。這樣,本程序就自然的分成了兩個層次,一個是與掃雷窗口的接口層,一個是實現(xiàn)判斷的算法層。若是有基礎(chǔ)的人,在提示到這以后,應(yīng)該就可以自己來實現(xiàn)它了^_^,但就本人的經(jīng)驗來看,還有一個難點,就是如何從一個個像素的取值來分析出每個方塊是什么狀態(tài)。難不成用模式識別?!!別緊張,當(dāng)初本人看了別人寫的象棋對戰(zhàn)程序,也忐忑地問了一句:難道要用人工智能?對方笑答,沒那么嚴(yán)重,說這個算法你也寫得出來?!獙嶋H上,只要檢查每個方塊上一兩個特定位置像素,就可以得知這個方塊的性質(zhì)。當(dāng)然,怎樣找到這一兩個像素的位置,還是要費一番周折的。為此,我寫了好幾個臨時程序,專門用來測量“掃雷”窗口的各種尺寸參數(shù)?;驹硎牵肍indWindow和GetDC得到掃雷窗口的HDC,然后把整個畫面寫到一個.bmp文件中去,再用Windows畫圖將其打開,用放大鏡放到最大倍數(shù),然后……一個個像素地數(shù)!另外,還用ResHacker把掃雷程序中的位圖資源提取出來,把小方塊中所有像素用程序進行掃描,以找到方塊內(nèi)某個特定像素,靠其顏色足以區(qū)分各種方塊(未挖開的空白,未挖開的插旗,未挖開的問號,和挖開的1~8的數(shù)字,和挖開的空白)。在這其中我郁悶地發(fā)現(xiàn),任何一個像素都不可能完全區(qū)分它們,因為掃雷窗口上的顏色總共還沒有那么多種。引入第二個像素是不可避免的,于是我用另一個位置的像素,首先區(qū)分是挖開的還是沒挖開的,才算解決了這個問題。

另外,關(guān)于開發(fā)工具的選擇。“程序員就像男人,編程語言就像女人,一般男人都想要很多女人,可沒幾個男人能真正了解一個女人”,這句話引自CSDN首頁上曾看到的網(wǎng)友評論,本人當(dāng)初也曾有過少年輕狂的時候,要了不少“女人”,以為女人越多真的越牛X,導(dǎo)致現(xiàn)在還對不少編程語言都一知半解。在做這個程序的時候,最了解的是Delphi,所以就用Delphi實現(xiàn)了。現(xiàn)在想來,Delphi確實是做此類程序的較好的選擇。它和Windows底層有良好的互操作性,唯一不便則是,從MSDN中看到的WinAPI的聲明都是C的,在Delphi中要稍微變換一下長相,而這些資料則是Borland所缺乏的。另外,再吹毛求疵一點,就是Delphi屏蔽了太多底層細節(jié),使很多初學(xué)者冷丁與HDC等概念打交道時會陌生。但一般來說,這更像是使用者的毛病,而不是開發(fā)工具的。另外,C++也是個好選擇,如果拿到今天,也許我更愿意用C++去實現(xiàn)它,C++配上wxWidgets,用來開發(fā)Windows應(yīng)用程序真不是鬧著玩的。但我一定不會喜歡用Java來實現(xiàn)它的,原因我想不必說大家也知道!

好,現(xiàn)在用Delphi開始掃雷外掛的開發(fā)之旅。首先,來看一下前面提到的,本人跋山涉水啊,翻山越嶺啊,費盡心思搜集來的掃雷窗口資料。所有資料取自Windows XP下帶的掃雷“版本5.1”。注意,WinXP下的掃雷與Win9x中的,窗口圖形有點細微差別,這足以使該外掛在Win9x下無法正確運行。不過我最終還是不打算加進判斷OS版本并采用兩套不同參數(shù)的代碼了,畢竟只是個實驗項目!

實測數(shù)據(jù)(單位為像素):

每個小方塊:寬=16,高=16

雷區(qū)的最大可能大小:寬=30個方塊, 高=24個方塊

從雷區(qū)的四邊,到窗口客戶區(qū)的四邊,各有間隔,分別為:左=12,右=8,上=55,下=8

在每一個方塊內(nèi),如下兩個像素可用來判斷該方塊的性質(zhì)(坐標(biāo)為相對方塊左上角得值):

首先判斷(0,0)處,若為clWhite,則為未挖開的方塊,若為clGray,則為挖開的。

若是未挖開的方塊,可再判斷(5,4),對應(yīng)關(guān)系如下:

clSilver:空白

clRed:插旗

clBlack:問號

若是挖開的方塊,可再判斷(7,4),對應(yīng)關(guān)系如下:

clSilver:空白(相當(dāng)于數(shù)字0)

clBlue:數(shù)字1

clGreen:數(shù)字2

clRed:數(shù)字3

clNavy:數(shù)字4

clMaroon:數(shù)字5

clTeal:數(shù)字6

clBlack:數(shù)字7

clGray:數(shù)字8

?

另外,雷區(qū)上方的“重開始”按鈕的狀態(tài)也是很重要的數(shù)據(jù),對于它的判斷,后面遇到的時候有詳述。

有了前述的數(shù)據(jù),編寫接口層似乎已經(jīng)不成問題了?,F(xiàn)在來構(gòu)思一下程序運行的過程。說句實話,那種對于某種變化實時作出響應(yīng)的程序,一般都用什么方法編成,本人并不很了解,推測無非是兩種思想,一種是用一個線程不斷地對這個變化進行檢查,發(fā)現(xiàn)改變則動作;另一種是注冊一個類似于鉤子的東西,用回調(diào)函數(shù)來處理。無疑第二種方式要更節(jié)省系統(tǒng)資源,只是難在本人對Windows的鉤子掌握的還一塌糊涂。若換了個高手,無疑應(yīng)該用這種方式,而我卻只有望著Spy++中迅速滾動的消息發(fā)呆的份。也許等我學(xué)好了鉤子之后,會寫個0.2版,而在0.1版中我已經(jīng)決定了使用丑陋的反復(fù)檢查的方式。于是,用了一個TTimer控件,定時間隔設(shè)在了30左右,用它的OnTimer事件來進行一次檢查和反饋操作。時間間隔是反復(fù)試驗確定的,太小,會很占CPU,太大則反應(yīng)會變慢。

這樣,運行過程已經(jīng)確定下來:

OnTimer事件->判斷當(dāng)前可否進行操作->取得整個雷區(qū)當(dāng)前狀況->用算法進行分析->反饋操作

所謂不可進行操作的時候,無非是指:根本沒有掃雷窗口,或者窗口部分被遮擋(此時無法取得正確的像素值),或者掃雷游戲沒有開始。

在我的代碼中,OnTimer事件處理過程的核心就是如下簡單的幾句:

if GetMineWindow then

begin

FetchCells;

AnalyzeCells;

OperateCells;

end;

?

?

其中:

GetMineWindow函數(shù)返回一個Boolean值,表示可否進行操作。如果可以,同時將關(guān)于掃雷窗口的一些參數(shù)存放進全局變量中。

FetchCells過程取得整個雷區(qū)所有方塊的信息,填入輸入緩沖區(qū)。

AnalyzeCells過程對輸入緩沖區(qū)中的數(shù)據(jù)進行分析,將反饋操作填入輸出緩沖區(qū)。

OperateCells過程根據(jù)輸出緩沖區(qū)中的數(shù)據(jù)對掃雷窗口進行反饋操作。

上述輸入緩沖區(qū)和輸出緩沖區(qū),各是一個二維數(shù)組,直觀地對應(yīng)了掃雷窗口上的每一個方塊。前者保存每個方塊的當(dāng)前狀態(tài)供分析,后者保存分析完畢后,將要實施到每一個方塊的操作。雷區(qū)的寬和高都不是固定的,而這兩個二維數(shù)組,則無論何時都要能夠保存所有方塊的信息。這時有兩個選擇,一是定義足以容下最大情況的靜態(tài)數(shù)組,二是使用動態(tài)數(shù)組。為了簡單,我采用了前者,畢竟最大情況也不是大得難以忍受。這樣,還需要一對整型變量保存實際雷區(qū)的寬和高。

現(xiàn)在可以來看一下全部需要的全局常量,變量和類型:

const

MINE_WINDOW_TITLE = ‘掃雷’;???? //窗口標(biāo)題,供尋找掃雷窗口用

//以下四個,為雷區(qū)四邊到窗口客戶區(qū)四邊的距離

? TOP_MARGIN = 55;??????????????? //上邊距

? BOTTOM_MARGIN = 8;????????????? //下邊距

? LEFT_MARGIN = 12;?????????????? //左邊距

? RIGHT_MARGIN = 8;?????????????? //右邊距

?

? CELL_WIDTH = 16;??????????????? //每個方塊寬度

? CELL_HEIGHT = 16;?????????????? //每個方塊高度

?

? MAX_COLUMN_COUNT = 30;????????? //雷區(qū)最大可能的列數(shù)

? MAX_ROW_COUNT = 24;???????????? //雷區(qū)最大可能的行數(shù)

?

type

//每個方塊的可能狀態(tài),包括0~8的數(shù)字(0不顯示),未知的,已插旗標(biāo)記為雷的,和標(biāo)記問號的

TCellState = (cs0, cs1, cs2, cs3, cs4, cs5, cs6, cs7, cs8, csUnknown, csMarked, csPossible);

//TCellState的集合類型,供分析算法使用

TCellStates = set of TCellState;

?

//對每個方塊的操作種類,包括無操作,左鍵單擊,右鍵單擊,左右鍵同時單擊,和右鍵雙擊(用于將問號標(biāo)記成旗)

TOperation = (opNone, opLeftClick, opRightClick, opBothClick, opRightDoubleClick);

var

? MineWnd: HWND;????????????????? //保存掃雷窗口的句柄

? MineDC: HDC;????????????? ??????//保存掃雷窗口的設(shè)備上下文

?

//雷區(qū)的實際寬度和高度(方塊數(shù))

AreaWidth: Integer

AreaHeight: Integer;

?

//輸入緩沖區(qū)

Cells: array[0..MAX_COLUMN_COUNT-1, 0..MAX_ROW_COUNT-1] of TCellState;

//輸出緩沖區(qū)

Operations: array[0..MAX_COLUMN_COUNT-1, 0..MAX_ROW_COUNT-1] of TOperation;

如上述,本程序分為了接口層和算法層。上述全局變量和常量,基本都屬于接口層的內(nèi)容。下面,來看接口層的具體實現(xiàn)。其工作的第一步,是要捕獲掃雷窗口并取得其信息。這由函數(shù)GetMineWindow來完成:

//試圖取得可用的掃雷窗口,返回值表示是否成功。若成功,則全局變量

//MineWnd、MineDC、AreaHeight、AreaWidth都得到相應(yīng)的填充。若失敗,則以上變量的值無意義。

function GetMineWindow: Boolean;

var

clientRect: TRect;

begin

result := false;

MineWnd := FindWindow(nil, MINE_WINDOW_TITLE);?????????? //檢查是否存在“掃雷”窗口,并且必須為當(dāng)前窗口

if (MineWnd = 0) or (GetForegroundWindow <> MineWnd) then

Exit;

MineDC := GetDC(MineWnd);??????????????????????????????? //取得“掃雷”窗口的設(shè)備上下文

if MineDC = 0 then

Exit;

GetClientRect(MineWnd, clientRect);????????????????????? //檢查“掃雷”窗口的內(nèi)容是否全部顯示在屏幕上

with TCanvas.Create do

try

Handle := MineDC;

if (ClipRect.Left <> clientRect.Left) or

(ClipRect.Right <> clientRect.Right) or

(ClipRect.Top <> clientRect.Top) or

(ClipRect.Bottom <> clientRect.Bottom) then

Exit;

finally

Free;

end;

//從已獲得的clientRect中的數(shù)值,根據(jù)實測數(shù)據(jù)計算AreaWidth和AreaHeight的值。

AreaWidth := (clientRect.Right - LEFT_MARGIN - RIGHT_MARGIN) div CELL_WIDTH;

AreaHeight := (clientRect.Bottom - TOP_MARGIN - BOTTOM_MARGIN) div CELL_HEIGHT;

//檢查游戲是否在進行中,原理為判斷“重開始”按鈕的圖標(biāo)上的

//某一像素是否是指定的值。該經(jīng)驗由實測得到,只有游戲進行中,該像素才為該值。

if TColor(GetPixel(MineDC, AreaWidth*8 + 8, 30)) <> clBlack then

Exit;

result := true; end;

理解這個函數(shù)的工作過程,有幾個要點:

WinAPI函數(shù)FindWindow:用來查找當(dāng)前桌面上的某個窗口。第一個參數(shù)是指定該窗口的“窗口類”的名字,這個稍微高深了一點,只有研究過Windows SDK編程才會理解。當(dāng)它為nil的時候,使用第二個參數(shù),也就是窗口標(biāo)題欄的字符串來查找。若找到這樣一個窗口,則返回值為其窗口句柄,否則為0。

WinAPI函數(shù)GetForegroundWindow:無參數(shù),返回桌面上的當(dāng)前窗口,也就是標(biāo)題條加亮的窗口的句柄。

WinAPI函數(shù)GetDC:給定一個窗口句柄,返回它的設(shè)備上下文句柄?!霸O(shè)備上下文”實際上就是一個“畫布”,在Delphi中,被封裝成了TCanvas類。獲得了某個設(shè)備上下文句柄,就可以用一個TCanvas型的對象指向它(這個過程是,把句柄賦給TCanvas對象的Handle屬性),從而實現(xiàn)畫布的各種操作。

WinAPI函數(shù)GetClientRect:給定某個窗口句柄,取得它的客戶區(qū)矩形,這個矩形是一個TRect類型的變量。調(diào)用這個函數(shù),要用一個TRect型的變量來接收結(jié)果,而不是用返回值。這個結(jié)果的Left和Top成員都必定是0,而Right和Bottom成員其實就是窗口客戶區(qū)的寬和高。

TCanvas類的屬性ClipRect:簡單的說,在此處,該TRect型屬性取得的是該畫布實際上被顯示在屏幕上的矩形部分。只有該畫布不被其它窗口遮擋,并且沒有移出桌面邊界的時候,這個矩形才完全等于等于窗口的客戶區(qū)矩形。這用來判斷掃雷窗口是否全部可見。

WinAPI函數(shù)GetPixel:給定一個設(shè)備上下文(畫布)句柄和X,Y坐標(biāo),取得一個像素的值。這個值是整型的,可以簡單的強制轉(zhuǎn)換為TColor類型。

上述庫函數(shù),具體說明可以參考MSDN和Delphi自身的幫助文檔,可以得到最為權(quán)威、詳細、正確的說明。

不得不說一下GetMineWindow函數(shù)的最后幾行,它牽涉到了對“重開始”按鈕的hack。注意一下,可以發(fā)現(xiàn)那個簡單的臉譜總共有5種狀態(tài):平時的笑臉,自身被按下時的笑臉,在雷區(qū)中按下鼠標(biāo)時的緊張表情,觸雷時的衰臉和勝利時酷酷的表情~——顯然,只有在第一種情況時,掃雷外掛才應(yīng)該動作,其它四種時則應(yīng)該停止。我編了一個臨時程序,找到了一個像素位置,它只有在第一種情況下值為clBlack,其它情況都不是。它的坐標(biāo)為(AreaWidth*8 + 8, 30),橫坐標(biāo)是個隨方塊列數(shù)而變的變量,很好理解,因為無論窗口有多寬,該按鈕都是水平居中的。

不得不說,捕獲“掃雷”窗口以及取得它的數(shù)據(jù),是本程序的一個難點?,F(xiàn)在這個難點已經(jīng)解決,接下來,完成接口層已經(jīng)不是問題了。那么,來看接口層的兩個核心過程:

//取得整個雷區(qū)每個方塊的狀態(tài),填入Cells中供分析。

procedure FetchCells;

var

i, j: Integer;

begin

//掃描每個方塊,根據(jù)指定像素的顏色判斷該方塊的性質(zhì)。

//特定像素的顏色與方塊性質(zhì)的對應(yīng)關(guān)系歸納自“掃雷”程序本身的資源。

for i:=0 to AreaWidth-1 do

for j:=0 to AreaHeight-1 do

//首先判斷(0, 0)點的像素

case TColor(GetPixel(MineDC, LEFT_MARGIN + i*CELL_WIDTH, TOP_MARGIN + j*CELL_HEIGHT)) of

clWhite:

//是未挖開的方塊,再判斷(5, 4)點的像素

case TColor(GetPixel(MineDC, LEFT_MARGIN + i*CELL_WIDTH + 5, TOP_MARGIN + j*CELL_HEIGHT + 4)) of

????????? clSilver: Cells[i, j] := csUnknown;???? //未翻開的方塊

????????? clRed: Cells[i, j] := csMarked;?????????? //已標(biāo)記為雷的方塊

????????? clBlack: Cells[i, j] := csPossible;????? //標(biāo)問號的方塊

end;

clGray:

//是已挖開的方塊,再判斷(7, 4)點的像素

case TColor(GetPixel(MineDC, LEFT_MARGIN + i*CELL_WIDTH + 7, TOP_MARGIN + j*CELL_HEIGHT + 4)) of

????????? clSilver: Cells[i, j] := cs0;??? //空白,相當(dāng)于數(shù)字0

????????? clBlue: Cells[i, j] := cs1;????? //數(shù)字1

????????? clGreen: Cells[i, j] := cs2;???? //數(shù)字2

????????? clRed: Cells[i, j] := cs3;?????? //數(shù)字3

????????? clNavy: Cells[i, j] := cs4;????? //數(shù)字4

???? ?????clMaroon: Cells[i, j] := cs5;??? //數(shù)字5

????????? clTeal: Cells[i, j] := cs6;????? //數(shù)字6

????????? clBlack: Cells[i, j] := cs7;???? //數(shù)字7

????????? clGray: Cells[i, j] := cs8;????? //數(shù)字8

end;

end;

end;

以上程序中,三個包含case...of的行,都用到了前面實測到的數(shù)據(jù)。執(zhí)行完之后,掃雷窗口所有方塊的狀態(tài)就原原本本地在輸入緩沖區(qū)里了。

//將Operations中所記載的對每個方塊的操作真正作用于掃雷窗口。

procedure OperateCells;

var

i, j: Integer;

? downMsg, upMsg: Cardinal;???? //按下和抬起鼠標(biāo)按鈕時分別發(fā)送的消息

? wparam, lparam: Integer;????? //消息參數(shù)

? clickCount: Integer;????????? //按鍵次數(shù),只有取值1或2

begin

//掃描每個方塊

for i:=0 to AreaWidth-1 do

for j:=0 to AreaHeight-1 do

begin

if Operations[i, j] = opNone then

Continue;

//根據(jù)操作種類,設(shè)定發(fā)送的消息及其參數(shù)

lparam := ((TOP_MARGIN + j*CELL_HEIGHT) shl 16) + (LEFT_MARGIN + i*CELL_WIDTH);

wparam := IfThen(Operations[i, j] = opBothClick, MK_RBUTTON, 0);

downMsg := IfThen(Operations[i, j] in [opRightClick, opRightDoubleClick], WM_RBUTTONDOWN, WM_LBUTTONDOWN);

upMsg := IfThen(Operations[i, j] in [opRightClick, opRightDoubleClick], WM_RBUTTONUP, WM_LBUTTONUP);

//設(shè)定發(fā)送消息次數(shù),即單擊還是雙擊

clickCount := IfThen(Operations[i, j] = opRightDoubleClick, 2, 1);

//發(fā)送消息

repeat

PostMessage(MineWnd, downMsg, wparam, lparam);

PostMessage(MineWnd, upMsg, wparam, lparam);

Dec(clickCount);

until clickCount = 0;

end;

end;

這里需要說的是WM_LBUTTONDOWN、WM_LBUTTONUP、WM_RBUTTONDOWN和WM_RBUTTONUP四個消息。它們是在一個窗口客戶區(qū)內(nèi)按下或抬起鼠標(biāo)左或右按鈕時,發(fā)給這個窗口的消息。所以,手動地發(fā)送這些消息,其實就是模擬鼠標(biāo)的點擊。發(fā)送消息用到了WinAPI函數(shù)PostMessage,它和大家所熟悉的SendMessage函數(shù)的參數(shù)是相同的,作用也幾乎相同,主要區(qū)別是不等待消息的返回,詳見MSDN。上述四個消息的WParam和LParam都具有同樣的意義:WParam用來指定按下該鍵的時候,還有哪些其它特定的鍵(其它鼠標(biāo)鍵,或Ctrl,Shift等)被按下。0表示沒有其它鍵被按下,在這里還用到了值MK_RBUTTON,即按下左鍵時指定右鍵同時也被按下,用來模擬同時按下左右鍵的情況。當(dāng)然,發(fā)送RBUTTONDOWN和RBUTTONUP時指定MK_LBUTTON也是同樣的效果。而LParam則指定了按下或抬起鍵時鼠標(biāo)指針的坐標(biāo),它的高16位為Y坐標(biāo),低16位為X坐標(biāo)。給lparam賦值的那一行,就是把X,Y兩個值組裝成了一個lparam。

另外提一下IfThen函數(shù),這個函數(shù)平時并不見有多人使用,但它真的很方便,至少它解決了對于“Delphi中沒有C的 ? : 運算符”的抱怨^_^。不錯,這就是Delphi的問號運算符,三個參數(shù)中第一個是Boolean型,若為真,返回值就是第二個參數(shù),否則是第三個參數(shù)。第二、第三個參數(shù)類型相同,并且有多個重載版本。數(shù)值版本需要包含Math庫,而字符串版本需要StrUtils庫。顯然,編譯上述代碼是需要包含Math庫的。若不愿,當(dāng)然你也可以使用if...then。

至此,接口層全部實現(xiàn)完畢,接下來就可以安心的實現(xiàn)“數(shù)學(xué)模型”了。

如前所述,算法層的實現(xiàn),不外乎兩種操作:1。如果一個方塊的數(shù)值等于周圍未挖開的方塊數(shù)目,則把周圍所有方塊標(biāo)記為雷;2。如果一個方塊的數(shù)值等于周圍已經(jīng)標(biāo)記為雷的方塊個數(shù),則在該塊上同時單擊左右鍵。實際上,這只是最簡單的兩種判斷(簡單到甚至不該稱之為“判斷”,而只是例行公事而已),而比這更復(fù)雜的分析判斷還可以有很多,但現(xiàn)在我們追求的是程序的簡單易懂,而且,就這兩種最簡單的判斷,已經(jīng)可以達到很好的效果了,在實際中它們絕對占到了掃雷所用時間的一大多半。更高級的判斷,在掃雷外掛的0.2版本里也已經(jīng)實現(xiàn)了,但在此處若要加以敘述,不免還要大幅增加篇幅。

就來看這個最簡單的算法:

//根據(jù)Cells中的數(shù)據(jù)進行判斷,把適當(dāng)?shù)牟僮魈钊隣perations中

procedure AnalyzeCells;

var

i, j: Integer;

neighborCount: Integer;????? //保存一個方塊周圍未挖開的方塊的數(shù)目

begin

//首先清空輸出緩沖區(qū)

for i:=0 to AreaWidth-1 do

for j:=0 to AreaHeight-1 do

Operations[i, j] := opNone;

//掃描輸入緩沖區(qū),執(zhí)行兩種最簡單的判斷

for i:=0 to AreaWidth-1 do

for j:=0 to AreaHeight-1 do

begin

//取得一個方塊周圍未挖開的方塊的數(shù)目

neighborCount := CountNeighbors(i, j, [csUnknown, csPossible]);

//只有1~8的數(shù)字,并且周圍存在未挖開的方塊,這樣的方塊才有分析價值

if (Cells[i, j]>cs0) and (Cells[i, j]<=cs8) and (neighborCount > 0) then

//第一種情況

if neighborCount = Ord(Cells[i, j])-CountNeighbors(i, j, [csMarked]) then

MarkAllNeighbors(i, j)

//第二種情況

else if Ord(Cells[i, j]) = CountNeighbors(i, j, [csMarked]) then

Operations[i, j] := opBothClick;

end;

end;

//將指定方塊周圍8個方塊中,未挖開的,包括已標(biāo)記問號的,都標(biāo)記為雷。

procedure MarkAllNeighbors(const x, y: Integer);

var

i,j: Integer;

begin

//掃描以某個坐標(biāo)為中心的9個方塊

for i:=x-1 to x+1 do

for j:=y-1 to y+1 do

begin

//去除中心塊,并避免數(shù)組越界

if ((i=x) and (j=y)) or (i<0) or (i>=AreaWidth) or (j<0) or (j>=AreaHeight) then

Continue;

//未挖開的空白則單擊右鍵,未挖開的標(biāo)問號的,則雙擊右鍵

if Cells[i, j] = csUnknown then

Operations[i, j] := opRightClick

else if Cells[i, j] = csPossible then

Operations[i, j] := opRightDoubleClick;

end;

end;

//取得指定方塊周圍8個方塊中等于任一個指定狀態(tài)的方塊的個數(shù)。

function CountNeighbors(const x, y: Integer; const targetStates: TCellStates): Integer;

var

i,j: Integer;

begin

result := 0;

//掃描以某個坐標(biāo)為中心的9個方塊

for i:=x-1 to x+1 do

for j:=y-1 to y+1 do

begin

//去除中心塊,并避免數(shù)組越界

if ((i=x) and (j=y)) or (i<0) or (i>=AreaWidth) or (j<0) or (j>=AreaHeight) then

Continue;

//計數(shù)指定狀態(tài)的方塊

if Cells[i, j] in targetStates then

Inc(result);

end;

end;

其中,由于枚舉TCellState的常量位置的安排,Ord函數(shù)對cs0~cs8所取得的值正是0~8,即等于該方塊的數(shù)值。這個算法可以算是中規(guī)中矩,沒什么取巧的地方,因此應(yīng)該不那么難懂。不錯,至此掃雷外掛已經(jīng)完全實現(xiàn)完畢。把上述所有函數(shù)和全局內(nèi)容放在一個單元(可以是一個窗體)里,設(shè)好TTimer控件的間隔,就可以很理想的工作了。在外掛類程序的開發(fā)中,本例用到的也許是最“笨”的一種方法,但對于平面方格類游戲,其原理具有通用性。它不需對游戲底層數(shù)據(jù)、協(xié)議之類有什么了解,只需要了解游戲的屏幕圖形就可以了。本例對于Windows窗口相關(guān)的某些API,也是一個較好的熟悉機會,對于初學(xué)者會有其意義。這個例子本身并不是最完善的,了解了思想,每個人自可以做出更加完善的程序。比如,應(yīng)用鉤子,這會大幅度減少該程序占用的系統(tǒng)資源。

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉